Skip to main content

qualia_core_db/gpu_context/
caps.rs

1//! Native wgpu adapter capability reporting.
2//!
3//! This module is intentionally diagnostic/policy-facing rather than hot-path code:
4//! it records what the selected adapter actually exposes so benchmark rows, logs,
5//! and future feature negotiation can agree on the same facts.
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8#[cfg(not(target_arch = "wasm32"))]
9pub struct GpuFeatureCaps {
10    pub timestamp_query: bool,
11    pub timestamp_query_inside_passes: bool,
12    pub pipeline_statistics_query: bool,
13    pub pipeline_cache: bool,
14    pub shader_f16: bool,
15    pub shader_int64: bool,
16    pub subgroup: bool,
17    pub subgroup_barrier: bool,
18    pub cooperative_matrix: bool,
19    pub ray_query: bool,
20}
21
22#[cfg(not(target_arch = "wasm32"))]
23impl GpuFeatureCaps {
24    pub fn from_features(features: wgpu::Features) -> Self {
25        Self {
26            timestamp_query: features.contains(wgpu::Features::TIMESTAMP_QUERY),
27            timestamp_query_inside_passes: features
28                .contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES),
29            pipeline_statistics_query: features.contains(wgpu::Features::PIPELINE_STATISTICS_QUERY),
30            pipeline_cache: features.contains(wgpu::Features::PIPELINE_CACHE),
31            shader_f16: features.contains(wgpu::Features::SHADER_F16),
32            shader_int64: features.contains(wgpu::Features::SHADER_INT64),
33            subgroup: features.contains(wgpu::Features::SUBGROUP),
34            subgroup_barrier: features.contains(wgpu::Features::SUBGROUP_BARRIER),
35            cooperative_matrix: features.contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX),
36            ray_query: features.contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY),
37        }
38    }
39
40    pub fn compact_flags(&self) -> String {
41        let mut out = String::with_capacity(96);
42        push_flag(&mut out, "ts", self.timestamp_query);
43        push_flag(&mut out, "ts-pass", self.timestamp_query_inside_passes);
44        push_flag(&mut out, "stats", self.pipeline_statistics_query);
45        push_flag(&mut out, "cache", self.pipeline_cache);
46        push_flag(&mut out, "f16", self.shader_f16);
47        push_flag(&mut out, "i64", self.shader_int64);
48        push_flag(&mut out, "subgroup", self.subgroup);
49        push_flag(&mut out, "subgroup-barrier", self.subgroup_barrier);
50        push_flag(&mut out, "coop-matrix", self.cooperative_matrix);
51        push_flag(&mut out, "ray-query", self.ray_query);
52        out
53    }
54}
55
56pub fn requested_native_llm_features(available: wgpu::Features) -> wgpu::Features {
57    let mut desired = wgpu::Features::TIMESTAMP_QUERY
58        | wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES
59        | wgpu::Features::PIPELINE_STATISTICS_QUERY
60        | wgpu::Features::PIPELINE_CACHE
61        | wgpu::Features::SHADER_F16
62        | wgpu::Features::SUBGROUP
63        | wgpu::Features::SUBGROUP_BARRIER;
64    if experimental_features_allowed() {
65        desired |= wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX;
66    }
67    available & desired
68}
69
70pub fn experimental_features_allowed() -> bool {
71    matches!(
72        std::env::var("QUALIA_WGPU_EXPERIMENTAL_FEATURES")
73            .ok()
74            .as_deref(),
75        Some("1") | Some("true") | Some("yes")
76    )
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80#[cfg(not(target_arch = "wasm32"))]
81pub struct GpuLimitCaps {
82    pub max_buffer_size: u64,
83    pub max_storage_buffer_binding_size: u64,
84    pub max_compute_workgroup_storage_size: u32,
85    pub max_compute_invocations_per_workgroup: u32,
86    pub max_compute_workgroup_size_x: u32,
87    pub max_compute_workgroups_per_dimension: u32,
88}
89
90#[cfg(not(target_arch = "wasm32"))]
91impl GpuLimitCaps {
92    pub fn from_limits(limits: &wgpu::Limits) -> Self {
93        Self {
94            max_buffer_size: limits.max_buffer_size,
95            max_storage_buffer_binding_size: limits.max_storage_buffer_binding_size,
96            max_compute_workgroup_storage_size: limits.max_compute_workgroup_storage_size,
97            max_compute_invocations_per_workgroup: limits.max_compute_invocations_per_workgroup,
98            max_compute_workgroup_size_x: limits.max_compute_workgroup_size_x,
99            max_compute_workgroups_per_dimension: limits.max_compute_workgroups_per_dimension,
100        }
101    }
102}
103
104#[derive(Debug, Clone)]
105#[cfg(not(target_arch = "wasm32"))]
106pub struct GpuAdapterCaps {
107    pub name: String,
108    pub backend: wgpu::Backend,
109    pub device_type: wgpu::DeviceType,
110    pub vendor: u32,
111    pub device: u32,
112    pub driver: String,
113    pub driver_info: String,
114    pub subgroup_min_size: u32,
115    pub subgroup_max_size: u32,
116    pub cooperative_matrix_tile_count: usize,
117    pub features: GpuFeatureCaps,
118    pub limits: GpuLimitCaps,
119}
120
121#[cfg(not(target_arch = "wasm32"))]
122impl GpuAdapterCaps {
123    pub fn from_adapter(adapter: &wgpu::Adapter) -> Self {
124        let info = adapter.get_info();
125        let features = adapter.features();
126        let limits = adapter.limits();
127        Self {
128            name: info.name,
129            backend: info.backend,
130            device_type: info.device_type,
131            vendor: info.vendor,
132            device: info.device,
133            driver: info.driver,
134            driver_info: info.driver_info,
135            subgroup_min_size: info.subgroup_min_size,
136            subgroup_max_size: info.subgroup_max_size,
137            cooperative_matrix_tile_count: adapter.cooperative_matrix_properties().len(),
138            features: GpuFeatureCaps::from_features(features),
139            limits: GpuLimitCaps::from_limits(&limits),
140        }
141    }
142
143    #[inline]
144    pub fn is_integrated_gpu(&self) -> bool {
145        matches!(self.device_type, wgpu::DeviceType::IntegratedGpu)
146    }
147
148    #[inline]
149    pub fn backend_label(&self) -> &'static str {
150        match self.backend {
151            wgpu::Backend::Noop => "noop",
152            wgpu::Backend::Vulkan => "vulkan",
153            wgpu::Backend::Metal => "metal",
154            wgpu::Backend::Dx12 => "dx12",
155            wgpu::Backend::Gl => "gl",
156            wgpu::Backend::BrowserWebGpu => "browser-webgpu",
157        }
158    }
159
160    #[inline]
161    pub fn device_type_label(&self) -> &'static str {
162        match self.device_type {
163            wgpu::DeviceType::Other => "other",
164            wgpu::DeviceType::IntegratedGpu => "integrated",
165            wgpu::DeviceType::DiscreteGpu => "discrete",
166            wgpu::DeviceType::VirtualGpu => "virtual",
167            wgpu::DeviceType::Cpu => "cpu",
168        }
169    }
170
171    pub fn summary_line(&self) -> String {
172        format!(
173            "{} | backend={} | type={} | vendor=0x{:04x} | device=0x{:04x} | driver={} {}",
174            self.name,
175            self.backend_label(),
176            self.device_type_label(),
177            self.vendor,
178            self.device,
179            self.driver,
180            self.driver_info
181        )
182    }
183
184    pub fn llm_feature_line(&self) -> String {
185        format!(
186            "features=[{}] subgroup={}..{} coop_tiles={} max_storage_binding={}MiB max_buffer={}MiB",
187            self.features.compact_flags(),
188            self.subgroup_min_size,
189            self.subgroup_max_size,
190            self.cooperative_matrix_tile_count,
191            self.limits.max_storage_buffer_binding_size / (1024 * 1024),
192            self.limits.max_buffer_size / (1024 * 1024)
193        )
194    }
195}
196
197#[cfg(not(target_arch = "wasm32"))]
198fn push_flag(out: &mut String, label: &str, enabled: bool) {
199    if !out.is_empty() {
200        out.push(' ');
201    }
202    out.push_str(label);
203    out.push('=');
204    out.push_str(if enabled { "1" } else { "0" });
205}
206
207// ── Inference-pipeline (GPU backend) selection — the "which pipeline for this machine" checker ──
208
209/// An explicit GPU-backend override for the inference device.
210///
211/// Priority:
212/// 1. `QUALIA_WGPU_BACKEND` env (`vulkan` | `dx12` | `metal` | `gl` | `primary` | `all`)
213/// 2. Cached [`crate::hardware_passport`] best GPU circuit (from `qualia-cli llm passport`)
214/// 3. `None` → platform default in `init_shared_gpu_async` (Windows → DX12; else wgpu pick)
215///
216/// This is what lets a machine be pinned by measurement rather than a static hierarchy.
217#[cfg(not(target_arch = "wasm32"))]
218pub fn qualia_backend_override() -> Option<wgpu::Backends> {
219    if let Ok(raw) = std::env::var("QUALIA_WGPU_BACKEND") {
220        let v = raw.trim().to_ascii_lowercase();
221        return match v.as_str() {
222            "vulkan" | "vk" => Some(wgpu::Backends::VULKAN),
223            "dx12" | "d3d12" | "directx12" => Some(wgpu::Backends::DX12),
224            "metal" | "mtl" => Some(wgpu::Backends::METAL),
225            "gl" | "opengl" | "gles" => Some(wgpu::Backends::GL),
226            "primary" => Some(wgpu::Backends::PRIMARY),
227            "all" => Some(wgpu::Backends::all()),
228            other => {
229                log::warn!(
230                    "QUALIA_WGPU_BACKEND='{other}' unrecognized — trying passport / default"
231                );
232                None
233            }
234        }
235        .or_else(passport_backend_override);
236    }
237    passport_backend_override()
238}
239
240/// Prefer the measured HardwarePassport GPU backend when no env pin is set.
241#[cfg(not(target_arch = "wasm32"))]
242fn passport_backend_override() -> Option<wgpu::Backends> {
243    let backend = crate::hardware_passport::cached_preferred_wgpu_backend()?;
244    let token = crate::hardware_passport::backend_env_token(&backend)?;
245    log::info!("shared_gpu|backend_from_passport|{backend}|token={token}");
246    match token {
247        "vulkan" => Some(wgpu::Backends::VULKAN),
248        "dx12" => Some(wgpu::Backends::DX12),
249        "metal" => Some(wgpu::Backends::METAL),
250        "gl" => Some(wgpu::Backends::GL),
251        _ => None,
252    }
253}
254
255/// Capability-aware recommendation for which GPU backend this machine *should* run inference on
256/// (advisory; surfaced by the doctor/setup checker). Prefers the **portable, vendor-neutral**
257/// path so the build is not silently locked to Windows (DX12) or NVIDIA (CUDA). Reactive to the
258/// adapter actually in hand; a full enumerate-all-backends-and-pick is the next layer.
259#[cfg(not(target_arch = "wasm32"))]
260pub fn recommend_inference_backend(caps: &GpuAdapterCaps) -> &'static str {
261    match caps.backend {
262        wgpu::Backend::Vulkan => "vulkan — vendor-neutral & portable (recommended)",
263        wgpu::Backend::Metal => "metal — Apple-native (recommended on macOS)",
264        wgpu::Backend::Dx12 => {
265            "dx12 — Windows-native; set QUALIA_WGPU_BACKEND=vulkan for the portable path"
266        }
267        wgpu::Backend::Gl => "gl — compatibility fallback, limited compute (last resort)",
268        _ => "noop/unknown — no GPU compute path",
269    }
270}