1#[cfg(feature = "gpu")]
4use super::artifact::{load_moe_artifacts, MoeArtifacts};
5#[cfg(feature = "gpu")]
6use super::diagnostics::{on_gpu_init_failed, GpuBackendError, GpuInitError};
7use crate::hw_probe::ScanBackend;
8#[cfg(feature = "gpu")]
9use std::sync::LazyLock;
10use std::sync::{Arc, OnceLock};
11
12pub(crate) struct AcquiredGpuPeer {
13 pub(crate) backend: Arc<dyn vyre::VyreBackend>,
14 pub(crate) device_identity: Option<String>,
15 pub(crate) is_software: bool,
16 pub(crate) resident_timed_dispatch_supported: bool,
17}
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub struct GpuBackendAvailability {
21 pub cuda: bool,
22 pub metal: bool,
23 pub wgpu: bool,
24}
25
26impl GpuBackendAvailability {
27 #[must_use]
28 pub const fn any(self) -> bool {
29 self.cuda || self.metal || self.wgpu
30 }
31}
32
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub(crate) struct GpuBackendAcquisitionFailure {
35 pub backend: &'static str,
36 pub diagnostic: String,
37}
38
39pub(crate) struct GpuBackendPeers {
40 cuda: OnceLock<Result<AcquiredGpuPeer, String>>,
41 metal: OnceLock<Result<AcquiredGpuPeer, String>>,
42 wgpu: OnceLock<Result<AcquiredGpuPeer, String>>,
43 pub(crate) cuda_available: bool,
44 pub(crate) metal_available: bool,
45 pub(crate) wgpu_available: bool,
46 pub(crate) cuda_device_identity: Option<String>,
47 pub(crate) cuda_runtime_identity: Option<String>,
48 pub(crate) metal_device_identity: Option<String>,
49 pub(crate) metal_runtime_identity: Option<String>,
50 pub(crate) wgpu_device_identity: Option<String>,
51 pub(crate) wgpu_runtime_identity: Option<String>,
52 pub(crate) wgpu_is_software: bool,
53}
54
55impl Default for GpuBackendPeers {
56 fn default() -> Self {
57 Self {
58 cuda: OnceLock::new(),
59 metal: OnceLock::new(),
60 wgpu: OnceLock::new(),
61 cuda_available: false,
62 metal_available: false,
63 wgpu_available: false,
64 cuda_device_identity: None,
65 metal_device_identity: None,
66 metal_runtime_identity: None,
67 cuda_runtime_identity: None,
68 wgpu_device_identity: None,
69 wgpu_runtime_identity: None,
70 wgpu_is_software: false,
71 }
72 }
73}
74
75pub(super) fn lazy_acquire<T, E>(
76 available: bool,
77 slot: &OnceLock<Result<T, E>>,
78 acquire: impl FnOnce() -> Result<T, E>,
79) -> Option<&Result<T, E>> {
80 if !available {
81 return None;
82 }
83 Some(slot.get_or_init(acquire))
84}
85
86impl GpuBackendPeers {
87 pub(crate) fn get(&self, backend: ScanBackend) -> Option<&Arc<dyn vyre::VyreBackend>> {
88 let result = match backend {
89 ScanBackend::GpuCuda => {
90 lazy_acquire(self.cuda_available, &self.cuda, acquire_cuda_peer)
91 }
92 ScanBackend::GpuMetal => {
93 lazy_acquire(self.metal_available, &self.metal, acquire_metal_peer)
94 }
95 ScanBackend::GpuWgpu => {
96 lazy_acquire(self.wgpu_available, &self.wgpu, acquire_wgpu_peer)
97 }
98 _ => None,
99 }?;
100 match result {
101 Ok(peer) => Some(&peer.backend),
102 Err(error) => {
103 tracing::error!(
104 target: "keyhog::routing",
105 ?backend,
106 diagnostic = %error,
107 "selected GPU backend acquisition failed"
108 );
109 None
110 }
111 }
112 }
113
114 pub(crate) fn initialized(&self, backend: ScanBackend) -> Option<&AcquiredGpuPeer> {
115 let result = match backend {
116 ScanBackend::GpuCuda => self.cuda.get(),
117 ScanBackend::GpuMetal => self.metal.get(),
118 ScanBackend::GpuWgpu => self.wgpu.get(),
119 _ => None,
120 }?;
121 match result {
124 Ok(peer) => Some(peer),
125 Err(_) => None, }
127 }
128 pub(crate) fn resident_timed_dispatch_supported(&self, backend: ScanBackend) -> bool {
129 self.initialized(backend)
130 .is_some_and(|peer| peer.resident_timed_dispatch_supported)
131 }
132
133 pub(crate) fn initialization_error(&self, backend: ScanBackend) -> Option<&str> {
134 match backend {
135 ScanBackend::GpuCuda => self.cuda.get(),
136 ScanBackend::GpuMetal => self.metal.get(),
137 ScanBackend::GpuWgpu => self.wgpu.get(),
138 _ => None,
139 }
140 .and_then(|result| result.as_ref().err().map(String::as_str))
141 }
142
143 pub(crate) fn availability(&self) -> GpuBackendAvailability {
144 GpuBackendAvailability {
145 cuda: self.cuda_available,
146 metal: self.metal_available,
147 wgpu: self.wgpu_available,
148 }
149 }
150}
151
152#[cfg(all(feature = "gpu", target_os = "linux"))]
153pub(super) fn run_cuda_after_preflight<T>(
154 preflight: impl FnOnce() -> Result<(), String>,
155 acquire: impl FnOnce() -> Result<T, String>,
156 operation: &'static str,
157) -> Result<T, String> {
158 preflight()?;
159 std::panic::catch_unwind(std::panic::AssertUnwindSafe(acquire)).map_err(|panic| {
160 format!(
161 "CUDA {operation} panicked: {}. Fix: repair the CUDA driver/runtime{}",
162 crate::error::panic_payload_detail(panic),
163 if operation == "backend acquisition" {
164 " or select another calibrated backend"
165 } else {
166 " before enabling this backend"
167 }
168 )
169 })?
170}
171
172#[cfg(all(feature = "gpu", target_os = "linux"))]
173fn acquire_cuda_peer() -> Result<AcquiredGpuPeer, String> {
174 let backend = run_cuda_after_preflight(
175 ensure_cuda_driver_library_loadable,
176 || {
177 let cuda = vyre_driver_cuda::backend::CudaBackend::acquire()?;
178 let boxed: Box<dyn vyre::VyreBackend> =
179 Box::new(vyre_driver_cuda::CudaBackendRegistration::new(cuda));
180 Ok::<Arc<dyn vyre::VyreBackend>, String>(Arc::from(boxed))
181 },
182 "backend acquisition",
183 )?;
184 tracing::info!(target: "keyhog::routing", "selected CUDA peer backend acquired");
185 Ok(AcquiredGpuPeer {
186 backend,
187 device_identity: None,
188 is_software: false,
189 resident_timed_dispatch_supported: true,
190 })
191}
192
193#[cfg(not(all(feature = "gpu", target_os = "linux")))]
194fn acquire_cuda_peer() -> Result<AcquiredGpuPeer, String> {
195 Err("CUDA peer is not compiled for this platform".to_string())
196}
197
198#[cfg(all(feature = "gpu", target_os = "macos"))]
199fn acquire_metal_peer() -> Result<AcquiredGpuPeer, String> {
200 let backend = std::panic::catch_unwind(std::panic::AssertUnwindSafe(
201 vyre_driver_metal::acquire,
202 ))
203 .map_err(|panic| {
204 format!(
205 "Metal backend acquisition panicked: {}. Fix: repair Metal.framework or select another calibrated backend",
206 crate::error::panic_payload_detail(panic)
207 )
208 })?
209 .map_err(|error| error.to_string())?;
210 tracing::info!(target: "keyhog::routing", "selected native Metal peer backend acquired");
211 Ok(AcquiredGpuPeer {
212 backend: Arc::from(backend),
213 device_identity: Some("Apple Metal default device".to_string()),
214 is_software: false,
215 resident_timed_dispatch_supported: false,
216 })
217}
218
219#[cfg(not(all(feature = "gpu", target_os = "macos")))]
220fn acquire_metal_peer() -> Result<AcquiredGpuPeer, String> {
221 Err("native Metal peer is not compiled for this platform".to_string())
222}
223
224#[cfg(feature = "gpu")]
225fn wgpu_resident_timed_dispatch_supported(features: wgpu::Features) -> bool {
226 features
227 .contains(wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS)
228}
229
230#[cfg(feature = "gpu")]
231fn acquire_wgpu_peer() -> Result<AcquiredGpuPeer, String> {
232 let backend = std::panic::catch_unwind(std::panic::AssertUnwindSafe(
233 vyre_driver_wgpu::WgpuBackend::shared,
234 ))
235 .map_err(|panic| {
236 format!(
237 "WGPU backend acquisition panicked: {}. Fix: repair the graphics driver/runtime or select another calibrated backend",
238 crate::error::panic_payload_detail(panic)
239 )
240 })?
241 .map_err(|error| error.to_string())?;
242 let info = backend.adapter_info();
243 let device_identity =
244 crate::gpu::gpu_adapter_device_identity(info, backend.device_limits().max_buffer_size);
245 let is_software = crate::gpu::is_software_adapter(info);
246 let resident_timed_dispatch_supported =
247 wgpu_resident_timed_dispatch_supported(backend.device_queue().0.features());
248 tracing::info!(
249 target: "keyhog::routing",
250 device_identity,
251 "selected WGPU peer backend acquired"
252 );
253 let backend: Arc<dyn vyre::VyreBackend> = backend;
254 Ok(AcquiredGpuPeer {
255 backend,
256 device_identity: Some(device_identity),
257 is_software,
258 resident_timed_dispatch_supported,
259 })
260}
261
262#[cfg(not(feature = "gpu"))]
263fn acquire_wgpu_peer() -> Result<AcquiredGpuPeer, String> {
264 Err("WGPU peer is not compiled in this build".to_string())
265}
266
267#[cfg(all(feature = "gpu", target_os = "linux"))]
268pub(crate) fn load_dynamic_library(name: &std::ffi::CStr) -> Result<(), String> {
269 unsafe {
273 let handle = libc::dlopen(name.as_ptr(), libc::RTLD_NOW | libc::RTLD_LOCAL);
274 if handle.is_null() {
275 let error = libc::dlerror();
276 let detail = if error.is_null() {
277 "unknown dynamic-loader error".to_owned()
278 } else {
279 std::ffi::CStr::from_ptr(error)
280 .to_string_lossy()
281 .into_owned()
282 };
283 return Err(format!("{}: {detail}", name.to_string_lossy()));
284 }
285 libc::dlclose(handle);
286 }
287 Ok(())
288}
289
290#[cfg(all(feature = "gpu", target_os = "linux"))]
291fn ensure_cuda_driver_library_loadable() -> Result<(), String> {
292 let first_error = match load_dynamic_library(c"libcuda.so.1") {
293 Ok(()) => return Ok(()),
294 Err(error) => error,
295 };
296 let second_error = match load_dynamic_library(c"libcuda.so") {
297 Ok(()) => return Ok(()),
298 Err(error) => error,
299 };
300 Err(format!(
301 "CUDA driver library is unavailable ({second_error}; first attempt: {first_error}). Fix: install or expose the NVIDIA driver libcuda.so before enabling this backend"
302 ))
303}
304
305#[cfg(all(feature = "gpu", target_os = "linux"))]
306pub(crate) fn probe_cuda_peer() -> Result<vyre_driver_cuda::device::CudaDeviceCaps, String> {
307 run_cuda_after_preflight(
308 ensure_cuda_driver_library_loadable,
309 || vyre_driver_cuda::device::CudaDeviceCaps::probe(0).map_err(|error| error.to_string()),
310 "device probe",
311 )
312}
313
314#[cfg(feature = "gpu")]
315pub(crate) struct GpuContext {
316 pub(super) device_queue: Arc<(wgpu::Device, wgpu::Queue)>,
317 pub(super) adapter_info: wgpu::AdapterInfo,
318 pub(super) device_limits: wgpu::Limits,
319 pub(super) artifacts: MoeArtifacts,
320}
321
322#[cfg(feature = "gpu")]
323impl GpuContext {
324 pub(crate) fn vram_mb(&self) -> Option<u64> {
325 const SANE_CAP_MB: u64 = 256 * 1024;
326 Some((self.device_limits.max_buffer_size / (1024 * 1024)).min(SANE_CAP_MB))
327 }
328
329 pub(crate) fn gpu_name(&self) -> &str {
330 &self.adapter_info.name
331 }
332
333 pub(super) fn device(&self) -> &wgpu::Device {
334 &self.device_queue.0
335 }
336
337 pub(super) fn queue(&self) -> &wgpu::Queue {
338 &self.device_queue.1
339 }
340
341 pub(super) fn artifacts(&self) -> &MoeArtifacts {
342 &self.artifacts
343 }
344}
345
346#[cfg(feature = "gpu")]
347static GPU: LazyLock<Result<Option<GpuContext>, GpuBackendError>> =
348 LazyLock::new(|| match init_moe_gpu() {
349 Ok(context) => {
350 tracing::info!("GPU MoE inference initialized (shared device)");
351 Ok(Some(context))
352 }
353 Err(error) => {
354 on_gpu_init_failed(
355 &error,
356 crate::gpu::gpu_disabled_by_policy(),
357 crate::gpu::gpu_required_by_policy(),
358 )?;
359 Ok(None)
360 }
361 });
362
363#[cfg(feature = "gpu")]
364fn init_moe_gpu() -> Result<GpuContext, GpuInitError> {
365 let vyre_backend = vyre_driver_wgpu::WgpuBackend::shared().map_err(|error| {
366 GpuInitError::no_adapter(format!("vyre WgpuBackend unavailable: {error}"))
367 })?;
368 let adapter_info = vyre_backend.adapter_info().clone();
369 if crate::gpu::is_software_adapter(&adapter_info) {
370 return Err(GpuInitError::no_adapter(format!(
371 "GPU adapter is a software fallback ({} on {:?}); refusing to use",
372 adapter_info.name, adapter_info.backend
373 )));
374 }
375 let device_limits = vyre_backend.device_limits().clone();
376 let device_queue = vyre_backend.device_queue();
377 tracing::info!(
378 gpu = %adapter_info.name,
379 backend = ?adapter_info.backend,
380 device_type = ?adapter_info.device_type,
381 driver = %adapter_info.driver,
382 "GPU MoE: reusing vyre shared device"
383 );
384 let artifacts = load_moe_artifacts(&device_queue.0, &adapter_info, &device_limits)
385 .map_err(GpuInitError::adapter_unusable)?;
386 Ok(GpuContext {
387 device_queue,
388 adapter_info,
389 device_limits,
390 artifacts,
391 })
392}
393
394#[cfg(feature = "gpu")]
395pub(crate) fn get_gpu() -> Result<Option<&'static GpuContext>, GpuBackendError> {
396 match &*GPU {
397 Ok(context) => Ok(context.as_ref()),
398 Err(error) => Err(error.clone()),
399 }
400}
401
402#[cfg(all(test, feature = "gpu"))]
403#[path = "../../../tests/unit/gpu_backend_acquisition.rs"]
404mod tests;