Skip to main content

qualia_core_db/inference/
inference_path_selector.rs

1//! Device-optimal inference path selection.
2//!
3//! Design intent (Timothy): a **benchmark utility** identifies the fastest method on
4//! *this* machine (wgpu DX12 / Vulkan / Metal / GL, optional CUDA lane, CPU) and the
5//! product **picks that path** — not a static hierarchy.
6//!
7//! ## What is selected
8//! | Axis | Candidates | Measured by |
9//! |------|------------|-------------|
10//! | **API backend** | dx12, vulkan, metal, gl | Hardware passport GEMV + decode-proxy |
11//! | **Compute lane** | portable (resident wgpu), cuda (forge TC + Q4 SoA GEMV) | Capability flags + optional micro-policy |
12//! | **Quant profile** | INT4 SoA weights, INT8 KV, quant-graph quality | A2000-class bandwidth + rights mode |
13//! | **Prefill vs decode** | TC GEMM when batch/dims allow; GEMV for m=1 decode | Shape policy, not GEMV score alone |
14//!
15//! Vulkan and DX12 both use the **same resident weight plan** (VRAM-sticky multi-weight);
16//! there is no separate "Vulkan multi-weight" — multi-weight without host RT is the
17//! **resident decode** path. CUDA multi-weight is the optional densify/Q4 device slab.
18//!
19//! Env pins always win: `QUALIA_WGPU_BACKEND`, `QUALIA_INFERENCE_MODE`, `QUALIA_PATH_AUTO=0`.
20
21#![cfg(not(target_arch = "wasm32"))]
22
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::sync::OnceLock;
25
26use crate::inference_modes::{set_inference_mode, InferenceMode};
27use crate::llm_bench::{
28    set_coop_gemv, set_ffn_fusion, set_kv_int8, set_resident_decode, set_resident_prefill,
29    set_resident_weights,
30};
31
32/// How decode math is executed after the wgpu API backend is chosen.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ComputeLane {
35    /// Single-fence resident decode/prefill on wgpu (DX12/Vulkan/Metal).
36    PortableResident,
37    /// Prefer CUDA TC prefill + Q4 device GEMV when eligible; else portable.
38    CudaAccelerated,
39}
40
41impl ComputeLane {
42    pub fn as_str(self) -> &'static str {
43        match self {
44            Self::PortableResident => "portable-resident",
45            Self::CudaAccelerated => "cuda-accelerated",
46        }
47    }
48}
49
50/// Quantization / quality profile for consumer GPUs.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum QuantProfile {
53    /// INT4 SoA weights + INT8 KV (A2000 bandwidth sweet spot).
54    Int4SoaInt8Kv,
55    /// Same as above + quant-graph hybrid quality net.
56    Int4SoaGraphHybrid,
57}
58
59impl QuantProfile {
60    pub fn as_str(self) -> &'static str {
61        match self {
62            Self::Int4SoaInt8Kv => "int4-soa+int8-kv",
63            Self::Int4SoaGraphHybrid => "int4-soa+int8-kv+graph",
64        }
65    }
66}
67
68/// Full selected plan for this process / host.
69#[derive(Debug, Clone, PartialEq)]
70pub struct InferencePathPlan {
71    /// `dx12` | `vulkan` | `metal` | `gl` | unknown
72    pub wgpu_backend: Option<String>,
73    pub compute_lane: ComputeLane,
74    pub quant: QuantProfile,
75    /// Use tensor-core dense GEMM for prefill when dims allow (CUDA lane).
76    pub prefill_prefer_tc: bool,
77    /// Decode uses GEMV (m=1); never pretend TC replaces decode.
78    pub decode_is_gemv: bool,
79    /// Human-readable reason chain.
80    pub rationale: String,
81    /// Passport circuit label that won, if any.
82    pub winning_circuit: Option<String>,
83    pub decode_proxy_tok_s: Option<f64>,
84    pub gemv_ms: Option<f64>,
85}
86
87static APPLIED: AtomicBool = AtomicBool::new(false);
88static LAST_PLAN: OnceLock<std::sync::Mutex<Option<InferencePathPlan>>> = OnceLock::new();
89
90fn last_plan_slot() -> &'static std::sync::Mutex<Option<InferencePathPlan>> {
91    LAST_PLAN.get_or_init(|| std::sync::Mutex::new(None))
92}
93
94/// Last plan applied (or resolved without apply).
95pub fn last_inference_path_plan() -> Option<InferencePathPlan> {
96    last_plan_slot().lock().ok().and_then(|g| g.clone())
97}
98
99/// Whether auto path selection is enabled (`QUALIA_PATH_AUTO` default on).
100pub fn path_auto_enabled() -> bool {
101    match std::env::var("QUALIA_PATH_AUTO").ok().as_deref() {
102        Some("0") | Some("false") | Some("off") => false,
103        _ => true,
104    }
105}
106
107/// Resolve the optimal plan from passport + host capabilities (does not mutate process yet).
108pub fn resolve_inference_path_plan() -> InferencePathPlan {
109    let mut rationale = Vec::new();
110
111    // ── 1. wgpu API backend from passport (or env pin) ─────────────────────
112    let env_backend = std::env::var("QUALIA_WGPU_BACKEND")
113        .ok()
114        .map(|s| s.trim().to_ascii_lowercase())
115        .filter(|s| !s.is_empty());
116
117    let (wgpu_backend, winning, decode_tok, gemv_ms) = if let Some(ref b) = env_backend {
118        rationale.push(format!("wgpu backend pinned by QUALIA_WGPU_BACKEND={b}"));
119        (Some(b.clone()), None, None, None)
120    } else if let Some(p) =
121        crate::hardware_passport::read_passport(&crate::hardware_passport::default_cache_path())
122    {
123        let best = p.matrix.best().cloned();
124        let token = p.preferred_inference_backend.clone().or_else(|| {
125            best.as_ref().and_then(|c| {
126                crate::hardware_passport::backend_env_token(&c.backend).map(str::to_string)
127            })
128        });
129        if let Some(ref c) = best {
130            rationale.push(format!(
131                "passport winner: {} [{}/{}] gemv={:.3}ms decode={:?}",
132                c.label,
133                format!("{:?}", c.kind),
134                c.backend,
135                c.ms_per_gemv,
136                c.decode_proxy_tok_s
137            ));
138        } else {
139            rationale.push("passport present but empty matrix".into());
140        }
141        (
142            token,
143            best.as_ref().map(|c| c.label.clone()),
144            best.as_ref().and_then(|c| c.decode_proxy_tok_s),
145            best.as_ref().map(|c| c.ms_per_gemv),
146        )
147    } else {
148        rationale.push(
149            "no hardware passport — run `qualia-cli llm passport --reprobe --decode-proxy <model> --apply-env-hint`"
150                .into(),
151        );
152        // Platform defaults: Metal on Apple, else leave unset (gpu_context picks).
153        let def = if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
154            rationale.push("default metal on Apple".into());
155            Some("metal".into())
156        } else {
157            None
158        };
159        (def, None, None, None)
160    };
161
162    // ── 2. Compute lane: CUDA only when toolkit present + not slower policy ─
163    let cuda_caps = crate::wgsl_forge::dispatch::caps().cuda;
164    let mode_pin = std::env::var("QUALIA_INFERENCE_MODE").ok();
165    let compute_lane = if let Some(ref m) = mode_pin {
166        if matches!(
167            m.to_ascii_lowercase().as_str(),
168            "cuda" | "cuda-tc" | "cudatc" | "tc" | "1"
169        ) {
170            rationale.push("compute lane pinned by QUALIA_INFERENCE_MODE=cuda".into());
171            ComputeLane::CudaAccelerated
172        } else {
173            rationale.push(format!("compute lane portable (mode pin {m})"));
174            ComputeLane::PortableResident
175        }
176    } else if cuda_caps && prefer_cuda_lane_heuristic() {
177        rationale.push(
178            "CUDA toolkit detected — lane=cuda-accelerated for prefill TC / Q4 device GEMV; decode still GEMV-primary"
179                .into(),
180        );
181        ComputeLane::CudaAccelerated
182    } else {
183        if cuda_caps {
184            rationale.push(
185                "CUDA present but portable-resident preferred (set QUALIA_INFERENCE_MODE=cuda to force)"
186                    .into(),
187            );
188        } else {
189            rationale.push("no CUDA — portable resident (wgpu multi-weight in VRAM)".into());
190        }
191        ComputeLane::PortableResident
192    };
193
194    // ── 3. Quant + quality ────────────────────────────────────────────────
195    let rights = crate::inference_modes::rights_mode_enabled()
196        || matches!(
197            mode_pin
198                .as_deref()
199                .map(|s| s.to_ascii_lowercase())
200                .as_deref(),
201            Some("quant-graph")
202                | Some("graph")
203                | Some("hybrid")
204                | Some("2")
205                | Some("fast-verify")
206                | Some("fast_verify")
207                | Some("3")
208        );
209    // Prefer FastVerify when operator asks for ollama-like + post heal without mid-decode tax.
210    if matches!(
211        mode_pin
212            .as_deref()
213            .map(|s| s.to_ascii_lowercase())
214            .as_deref(),
215        Some("fast-verify") | Some("fast_verify") | Some("ollama-like") | Some("3")
216    ) {
217        rationale.push(
218            "mode pin fast-verify: full-speed decode then post-turn CML/graph self-heal".into(),
219        );
220    }
221    let quant = if rights {
222        rationale.push("quant profile: INT4 SoA + INT8 KV + quant-graph hybrid".into());
223        QuantProfile::Int4SoaGraphHybrid
224    } else {
225        rationale.push("quant profile: INT4 SoA weights + INT8 KV (consumer bandwidth)".into());
226        QuantProfile::Int4SoaInt8Kv
227    };
228
229    let prefill_prefer_tc = matches!(compute_lane, ComputeLane::CudaAccelerated);
230    if prefill_prefer_tc {
231        rationale.push("prefill: prefer TC dense GEMM when m,n,k multiples of 16".into());
232    }
233    rationale
234        .push("decode: always GEMV (m=1) — tensor cores do not replace single-token GEMV".into());
235    rationale.push(
236        "multi-weight: wgpu resident plan keeps layer weights in VRAM (Vulkan/DX12/Metal); CUDA slab is optional densify path"
237            .into(),
238    );
239
240    InferencePathPlan {
241        wgpu_backend,
242        compute_lane,
243        quant,
244        prefill_prefer_tc,
245        decode_is_gemv: true,
246        rationale: rationale.join(" | "),
247        winning_circuit: winning,
248        decode_proxy_tok_s: decode_tok,
249        gemv_ms,
250    }
251}
252
253/// Heuristic: enable CUDA lane when not explicitly disabled and runtime path exists.
254fn prefer_cuda_lane_heuristic() -> bool {
255    // Default OFF for CUDA as primary decode — measured A2000 still favored portable resident.
256    // Opt-in via QUALIA_PREFER_CUDA=1 or mode=cuda. Prefill can still use forge when mode=cuda.
257    matches!(
258        std::env::var("QUALIA_PREFER_CUDA").ok().as_deref(),
259        Some("1") | Some("true") | Some("on")
260    )
261}
262
263/// Apply plan to process toggles (once per process unless `force`).
264///
265/// Sets inference mode / bench flags. Does **not** set `QUALIA_WGPU_BACKEND` if already
266/// set (gpu_context already honors env + passport). Optionally writes passport preferred
267/// fields for the next process.
268pub fn apply_inference_path_plan(plan: &InferencePathPlan, force: bool) -> bool {
269    if force {
270        APPLIED.store(true, Ordering::SeqCst);
271    } else if APPLIED.swap(true, Ordering::SeqCst) {
272        return false; // already applied this process
273    }
274
275    // Resident always on for product path.
276    set_resident_decode(true);
277    set_resident_prefill(true);
278    set_resident_weights(true);
279    set_coop_gemv(true);
280    set_ffn_fusion(true);
281    set_kv_int8(true);
282
283    match plan.quant {
284        QuantProfile::Int4SoaInt8Kv => {
285            // Leave mode as portable unless compute lane says cuda.
286        }
287        QuantProfile::Int4SoaGraphHybrid => {
288            if std::env::var("QUALIA_INFERENCE_MODE").is_err() {
289                // Prefer post-turn FastVerify for consumer rights path when not pinned.
290                if crate::inference_modes::rights_mode_enabled()
291                    || matches!(
292                        std::env::var("QUALIA_PREFER_FAST_VERIFY").ok().as_deref(),
293                        Some("1") | Some("true")
294                    )
295                {
296                    set_inference_mode(InferenceMode::FastVerify);
297                } else {
298                    set_inference_mode(InferenceMode::QuantGraph);
299                }
300            }
301        }
302    }
303
304    match plan.compute_lane {
305        ComputeLane::PortableResident => {
306            if std::env::var("QUALIA_INFERENCE_MODE").is_err()
307                && !matches!(plan.quant, QuantProfile::Int4SoaGraphHybrid)
308            {
309                set_inference_mode(InferenceMode::Portable);
310            }
311        }
312        ComputeLane::CudaAccelerated => {
313            if std::env::var("QUALIA_INFERENCE_MODE").is_err() {
314                set_inference_mode(InferenceMode::CudaTc);
315            }
316            crate::wgsl_forge::dispatch::ensure_cuda_runtime_path();
317        }
318    }
319
320    // Soft-set backend for child processes / logs if unset.
321    if std::env::var("QUALIA_WGPU_BACKEND").is_err() {
322        if let Some(ref b) = plan.wgpu_backend {
323            // SAFETY: process-local config before GPU init; documented operator surface.
324            std::env::set_var("QUALIA_WGPU_BACKEND", b);
325            log::info!("path_select|set_env|QUALIA_WGPU_BACKEND={b}");
326        }
327    }
328
329    log::info!(
330        "path_select|applied|backend={:?}|lane={}|quant={}|prefill_tc={}|{}",
331        plan.wgpu_backend,
332        plan.compute_lane.as_str(),
333        plan.quant.as_str(),
334        plan.prefill_prefer_tc,
335        plan.rationale
336    );
337
338    if let Ok(mut g) = last_plan_slot().lock() {
339        *g = Some(plan.clone());
340    }
341    true
342}
343
344/// Resolve + apply if `QUALIA_PATH_AUTO` enabled. Called from `bootstrap_inference_mode`.
345pub fn bootstrap_optimal_inference_path() -> InferencePathPlan {
346    let plan = resolve_inference_path_plan();
347    if path_auto_enabled() {
348        apply_inference_path_plan(&plan, false);
349    } else {
350        log::info!("path_select|skipped|QUALIA_PATH_AUTO=0|{}", plan.rationale);
351        if let Ok(mut g) = last_plan_slot().lock() {
352            *g = Some(plan.clone());
353        }
354    }
355    plan
356}
357
358/// Operator-facing summary (CLI / doctor).
359pub fn format_path_plan(plan: &InferencePathPlan) -> String {
360    format!(
361        "InferencePathPlan\n  wgpu_backend:     {}\n  compute_lane:     {}\n  quant:            {}\n  prefill_TC:       {}\n  decode:           GEMV (m=1)\n  winning_circuit:  {}\n  decode_proxy:     {}\n  gemv_ms:          {}\n  rationale:\n    {}\n",
362        plan.wgpu_backend.as_deref().unwrap_or("(platform default)"),
363        plan.compute_lane.as_str(),
364        plan.quant.as_str(),
365        plan.prefill_prefer_tc,
366        plan.winning_circuit.as_deref().unwrap_or("—"),
367        plan.decode_proxy_tok_s
368            .map(|t| format!("{t:.2} tok/s"))
369            .unwrap_or_else(|| "—".into()),
370        plan.gemv_ms
371            .map(|m| format!("{m:.3}"))
372            .unwrap_or_else(|| "—".into()),
373        plan.rationale.replace(" | ", "\n    "),
374    )
375}
376
377/// Probe passport (optional re-probe) and print/apply plan. Used by CLI.
378pub fn run_path_select_cli(reprobe: bool, apply: bool) -> InferencePathPlan {
379    if reprobe {
380        let _ = crate::hardware_passport::load_or_probe(
381            &crate::hardware_passport::default_cache_path(),
382            crate::hardware_passport::PASSPORT_GEMV_N,
383        );
384    }
385    let plan = resolve_inference_path_plan();
386    if apply {
387        apply_inference_path_plan(&plan, true);
388        // Persist preferred backend into passport if we have one.
389        if let Some(mut p) =
390            crate::hardware_passport::read_passport(&crate::hardware_passport::default_cache_path())
391        {
392            if let Some(ref b) = plan.wgpu_backend {
393                p.preferred_inference_backend = Some(b.clone());
394            }
395            let _ = crate::hardware_passport::write_passport(
396                &p,
397                &crate::hardware_passport::default_cache_path(),
398            );
399        }
400    }
401    plan
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn resolve_does_not_panic() {
410        let p = resolve_inference_path_plan();
411        assert!(p.decode_is_gemv);
412        assert!(!p.rationale.is_empty());
413    }
414
415    #[test]
416    fn format_contains_axes() {
417        let p = InferencePathPlan {
418            wgpu_backend: Some("dx12".into()),
419            compute_lane: ComputeLane::PortableResident,
420            quant: QuantProfile::Int4SoaInt8Kv,
421            prefill_prefer_tc: false,
422            decode_is_gemv: true,
423            rationale: "test".into(),
424            winning_circuit: Some("A2000".into()),
425            decode_proxy_tok_s: Some(2.5),
426            gemv_ms: Some(0.11),
427        };
428        let s = format_path_plan(&p);
429        assert!(s.contains("dx12"));
430        assert!(s.contains("portable-resident"));
431        assert!(s.contains("GEMV"));
432    }
433}