1use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
12
13#[cfg(not(target_arch = "wasm32"))]
14use std::sync::OnceLock;
15
16#[cfg(any(not(target_arch = "wasm32"), feature = "wasm-llm"))]
20mod caps;
21#[cfg(not(target_arch = "wasm32"))]
22pub(crate) use caps::experimental_features_allowed;
23#[cfg(any(not(target_arch = "wasm32"), feature = "wasm-llm"))]
24pub(crate) use caps::requested_native_llm_features;
25#[cfg(not(target_arch = "wasm32"))]
26pub use caps::{
27 qualia_backend_override, recommend_inference_backend, GpuAdapterCaps, GpuFeatureCaps,
28 GpuLimitCaps,
29};
30
31#[cfg(not(target_arch = "wasm32"))]
35pub mod device_registry;
36
37#[repr(u8)]
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum OperationalMode {
41 #[default]
42 Full = 0,
43 Eco = 1,
44 Reserve = 2,
46}
47
48impl OperationalMode {
49 #[inline]
50 pub fn from_pressure(pressure: f32) -> Self {
51 if pressure >= 0.92 {
52 Self::Reserve
53 } else if pressure >= 0.72 {
54 Self::Eco
55 } else {
56 Self::Full
57 }
58 }
59
60 #[inline]
61 pub fn max_particles(self) -> u32 {
62 match self {
63 Self::Full => 50_000,
64 Self::Eco => 8_000,
65 Self::Reserve => 0,
66 }
67 }
68
69 #[inline]
70 pub fn bloom_enabled(self) -> bool {
71 matches!(self, Self::Full)
72 }
73
74 #[inline]
78 pub fn supports_3d(self) -> bool {
79 matches!(self, Self::Full)
80 }
81}
82
83#[repr(u8)]
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum ComputeUniverse {
87 LlmInference = 0,
89 Tensor10D = 1,
91 Viewport = 2,
93 AcousticPlane = 3,
95}
96
97impl ComputeUniverse {
98 pub const ALL: [Self; 4] = [
99 Self::LlmInference,
100 Self::Tensor10D,
101 Self::Viewport,
102 Self::AcousticPlane,
103 ];
104
105 #[inline]
107 pub fn partition_index(self) -> usize {
108 match self {
109 Self::AcousticPlane => Self::Tensor10D as usize,
110 u => u as usize,
111 }
112 }
113
114 #[inline]
115 pub fn label(self) -> &'static str {
116 match self {
117 Self::LlmInference => "U0 LLM",
118 Self::Tensor10D => "U1 Tensor10D",
119 Self::Viewport => "U2 Viewport",
120 Self::AcousticPlane => "U3 AcousticPlane",
121 }
122 }
123
124 #[inline]
125 pub fn default_queue_lane(self) -> QueueLane {
126 match self {
127 Self::LlmInference => QueueLane::LlmCompute,
128 Self::Tensor10D => QueueLane::TensorCompute,
129 Self::Viewport | Self::AcousticPlane => QueueLane::ViewportRender,
130 }
131 }
132
133 #[inline]
134 pub fn ledger_slots(self) -> &'static [VramLedgerSlot] {
135 match self {
136 Self::LlmInference => &[VramLedgerSlot::LlmKvCache, VramLedgerSlot::LlmWeightStaging],
137 Self::Tensor10D | Self::AcousticPlane => &[VramLedgerSlot::Tensor10D],
138 Self::Viewport => &[VramLedgerSlot::Viewport],
139 }
140 }
141}
142
143#[repr(u8)]
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
146pub enum VramLedgerSlot {
147 LlmKvCache = 0,
148 LlmWeightStaging = 1,
149 Tensor10D = 2,
150 Viewport = 3,
151}
152
153#[repr(u8)]
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum QueueLane {
157 LlmCompute = 0,
158 TensorCompute = 1,
159 ViewportRender = 2,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub struct VramByteRange {
165 pub offset: u64,
166 pub size: u64,
167}
168
169impl VramByteRange {
170 #[inline]
171 pub const fn empty() -> Self {
172 Self { offset: 0, size: 0 }
173 }
174
175 #[inline]
176 pub fn end(&self) -> u64 {
177 self.offset.saturating_add(self.size)
178 }
179
180 #[inline]
182 pub fn contains(&self, alloc_offset: u64, alloc_size: u64) -> bool {
183 alloc_size == 0
184 || alloc_offset >= self.offset && alloc_offset.saturating_add(alloc_size) <= self.end()
185 }
186}
187
188#[derive(Debug, Clone, Copy)]
190pub struct UniversePartition {
191 pub universe: ComputeUniverse,
192 pub mode: OperationalMode,
193 pub vram_budget_bytes: u64,
195 pub ledger_range: VramByteRange,
197 pub queue_lane: QueueLane,
198}
199
200#[derive(Debug, Clone)]
202pub struct UniverseOrchestrator {
203 pub active_mode: OperationalMode,
204 pub partitions: [UniversePartition; 3],
205}
206
207impl UniverseOrchestrator {
208 #[inline]
210 pub fn budget_triplet(total_bytes: u64, mode: OperationalMode) -> (u64, u64, u64) {
211 match mode {
212 OperationalMode::Reserve => {
213 let u2 = total_bytes / 10;
214 let rem = total_bytes.saturating_sub(u2);
215 let u0 = rem / 2;
216 let u1 = rem.saturating_sub(u0);
217 (u0, u1, u2)
218 }
219 OperationalMode::Eco => {
220 let u2 = total_bytes / 4;
221 let rem = total_bytes.saturating_sub(u2);
222 let u0 = rem.saturating_mul(6) / 10;
223 let u1 = rem.saturating_sub(u0);
224 (u0, u1, u2)
225 }
226 OperationalMode::Full => {
227 let u0 = total_bytes.saturating_mul(55) / 100;
228 let u1 = total_bytes.saturating_mul(25) / 100;
229 let u2 = total_bytes.saturating_mul(15) / 100;
230 (u0, u1, u2)
231 }
232 }
233 }
234
235 fn partition_at_offset(
236 universe: ComputeUniverse,
237 mode: OperationalMode,
238 offset: u64,
239 size: u64,
240 ) -> UniversePartition {
241 UniversePartition {
242 universe,
243 mode,
244 vram_budget_bytes: size,
245 ledger_range: VramByteRange { offset, size },
246 queue_lane: universe.default_queue_lane(),
247 }
248 }
249
250 pub fn from_total_budget(total_bytes: u64, mode: OperationalMode) -> Self {
252 let (u0, u1, u2) = Self::budget_triplet(total_bytes, mode);
253 Self {
254 active_mode: mode,
255 partitions: [
256 Self::partition_at_offset(ComputeUniverse::LlmInference, mode, 0, u0),
257 Self::partition_at_offset(ComputeUniverse::Tensor10D, mode, u0, u1),
258 Self::partition_at_offset(
259 ComputeUniverse::Viewport,
260 mode,
261 u0.saturating_add(u1),
262 u2,
263 ),
264 ],
265 }
266 }
267
268 #[inline]
270 pub fn from_total_budget_full(total_bytes: u64) -> Self {
271 Self::from_total_budget(total_bytes, OperationalMode::Full)
272 }
273
274 #[inline]
275 pub fn partition(&self, universe: ComputeUniverse) -> &UniversePartition {
276 &self.partitions[universe.partition_index()]
277 }
278
279 #[inline]
281 pub fn effective_mode(
282 &self,
283 universe: ComputeUniverse,
284 global: OperationalMode,
285 ) -> OperationalMode {
286 match global {
287 OperationalMode::Full => OperationalMode::Full,
288 OperationalMode::Eco => match universe {
289 ComputeUniverse::Viewport | ComputeUniverse::AcousticPlane => OperationalMode::Eco,
290 _ => OperationalMode::Eco,
291 },
292 OperationalMode::Reserve => match universe {
293 ComputeUniverse::LlmInference => OperationalMode::Full,
294 ComputeUniverse::Tensor10D => OperationalMode::Eco,
295 ComputeUniverse::Viewport | ComputeUniverse::AcousticPlane => {
296 OperationalMode::Reserve
297 }
298 },
299 }
300 }
301
302 #[inline]
303 pub fn max_particles(&self, universe: ComputeUniverse, global: OperationalMode) -> u32 {
304 self.effective_mode(universe, global).max_particles()
305 }
306
307 #[inline]
308 pub fn bloom_enabled(&self, universe: ComputeUniverse, global: OperationalMode) -> bool {
309 universe == ComputeUniverse::Viewport
310 && self.effective_mode(universe, global).bloom_enabled()
311 }
312}
313
314#[inline]
316pub fn universe_orchestrator() -> UniverseOrchestrator {
317 let ledger = global_vram_ledger();
318 UniverseOrchestrator::from_total_budget(ledger.budget().max(1), ledger.mode())
319}
320
321#[inline]
323pub fn global_universe_orchestrator() -> UniverseOrchestrator {
324 universe_orchestrator()
325}
326
327#[inline]
329pub fn viewport_operational_mode() -> OperationalMode {
330 let ledger = global_vram_ledger();
331 universe_orchestrator().effective_mode(ComputeUniverse::Viewport, ledger.mode())
332}
333
334#[inline]
336pub fn ambient_draw_instances_for_mode(resident: u32, global: OperationalMode) -> u32 {
337 let cap = universe_orchestrator()
338 .effective_mode(ComputeUniverse::Viewport, global)
339 .max_particles();
340 resident.min(cap)
341}
342
343#[inline]
345pub fn ambient_draw_instances(resident: u32) -> u32 {
346 ambient_draw_instances_for_mode(resident, global_vram_ledger().mode())
347}
348
349#[derive(Debug, Default)]
351pub struct VramLedger {
352 budget_bytes: AtomicU64,
353 tensor_bytes: AtomicU64,
354 kv_cache_bytes: AtomicU64,
355 render_bytes: AtomicU64,
356 llm_weight_staging_bytes: AtomicU64,
357 mode: AtomicU32,
358}
359
360impl VramLedger {
361 pub const KV_CACHE_CAP_BYTES: u64 = 448 * 1024 * 1024;
362
363 #[inline]
364 fn load_slot(&self, slot: VramLedgerSlot) -> u64 {
365 match slot {
366 VramLedgerSlot::LlmKvCache => self.kv_cache_bytes.load(Ordering::Relaxed),
367 VramLedgerSlot::LlmWeightStaging => {
368 self.llm_weight_staging_bytes.load(Ordering::Relaxed)
369 }
370 VramLedgerSlot::Tensor10D => self.tensor_bytes.load(Ordering::Relaxed),
371 VramLedgerSlot::Viewport => self.render_bytes.load(Ordering::Relaxed),
372 }
373 }
374
375 #[inline]
376 fn store_slot(&self, slot: VramLedgerSlot, bytes: u64) {
377 let bytes = match slot {
378 VramLedgerSlot::LlmKvCache => bytes.min(Self::KV_CACHE_CAP_BYTES),
379 _ => bytes,
380 };
381 match slot {
382 VramLedgerSlot::LlmKvCache => self.kv_cache_bytes.store(bytes, Ordering::Relaxed),
383 VramLedgerSlot::LlmWeightStaging => self
384 .llm_weight_staging_bytes
385 .store(bytes, Ordering::Relaxed),
386 VramLedgerSlot::Tensor10D => self.tensor_bytes.store(bytes, Ordering::Relaxed),
387 VramLedgerSlot::Viewport => self.render_bytes.store(bytes, Ordering::Relaxed),
388 }
389 self.refresh_mode();
390 }
391
392 #[inline]
393 pub fn budget(&self) -> u64 {
394 self.budget_bytes.load(Ordering::Relaxed)
395 }
396
397 #[inline]
398 pub fn used_in_slot(&self, slot: VramLedgerSlot) -> u64 {
399 self.load_slot(slot)
400 }
401
402 #[inline]
403 pub fn record_slot(&self, slot: VramLedgerSlot, bytes: u64) {
404 self.store_slot(slot, bytes);
405 }
406
407 #[inline]
408 pub fn universe_used_bytes(&self, universe: ComputeUniverse) -> u64 {
409 universe
410 .ledger_slots()
411 .iter()
412 .map(|s| self.load_slot(*s))
413 .sum()
414 }
415
416 #[inline]
417 pub fn universe_pressure(
418 &self,
419 orchestrator: &UniverseOrchestrator,
420 universe: ComputeUniverse,
421 ) -> f32 {
422 let cap = orchestrator.partition(universe).vram_budget_bytes;
423 if cap == 0 {
424 return 0.0;
425 }
426 (self.universe_used_bytes(universe) as f32 / cap as f32).clamp(0.0, 1.25)
427 }
428
429 #[inline]
430 pub fn can_allocate_in_universe(
431 &self,
432 orchestrator: &UniverseOrchestrator,
433 universe: ComputeUniverse,
434 extra_bytes: u64,
435 ) -> bool {
436 let part = orchestrator.partition(universe);
437 let used = self.universe_used_bytes(universe);
438 if used.saturating_add(extra_bytes) > part.vram_budget_bytes {
439 return false;
440 }
441 part.ledger_range
442 .contains(part.ledger_range.offset.saturating_add(used), extra_bytes)
443 && self.can_allocate(extra_bytes)
444 }
445
446 #[inline]
448 pub fn slot_byte_offset(orchestrator: &UniverseOrchestrator, slot: VramLedgerSlot) -> u64 {
449 let universe = match slot {
450 VramLedgerSlot::LlmKvCache | VramLedgerSlot::LlmWeightStaging => {
451 ComputeUniverse::LlmInference
452 }
453 VramLedgerSlot::Tensor10D => ComputeUniverse::Tensor10D,
454 VramLedgerSlot::Viewport => ComputeUniverse::Viewport,
455 };
456 let used_before = match slot {
457 VramLedgerSlot::LlmWeightStaging => {
458 orchestrator
459 .partition(ComputeUniverse::LlmInference)
460 .ledger_range
461 .offset
462 + global_vram_ledger().used_in_slot(VramLedgerSlot::LlmKvCache)
463 }
464 _ => orchestrator.partition(universe).ledger_range.offset,
465 };
466 used_before
467 }
468
469 #[inline]
470 pub fn record_universe(&self, universe: ComputeUniverse, slot: VramLedgerSlot, bytes: u64) {
471 debug_assert!(universe.ledger_slots().contains(&slot));
472 self.store_slot(slot, bytes);
473 }
474
475 #[inline]
476 pub fn new(budget_bytes: u64) -> Self {
477 Self {
478 budget_bytes: AtomicU64::new(budget_bytes),
479 ..Default::default()
480 }
481 }
482
483 #[inline]
484 pub fn set_budget(&self, bytes: u64) {
485 self.budget_bytes.store(bytes, Ordering::Relaxed);
486 }
487
488 #[inline]
489 pub fn record_tensor(&self, bytes: u64) {
490 self.record_universe(ComputeUniverse::Tensor10D, VramLedgerSlot::Tensor10D, bytes);
491 }
492
493 #[inline]
494 pub fn record_kv_cache(&self, bytes: u64) {
495 self.record_universe(
496 ComputeUniverse::LlmInference,
497 VramLedgerSlot::LlmKvCache,
498 bytes,
499 );
500 }
501
502 #[inline]
503 pub fn record_render(&self, bytes: u64) {
504 self.record_universe(ComputeUniverse::Viewport, VramLedgerSlot::Viewport, bytes);
505 }
506
507 #[inline]
508 pub fn record_llm_staging(&self, bytes: u64) {
509 self.record_universe(
510 ComputeUniverse::LlmInference,
511 VramLedgerSlot::LlmWeightStaging,
512 bytes,
513 );
514 }
515
516 #[inline]
517 pub fn used_bytes(&self) -> u64 {
518 self.tensor_bytes.load(Ordering::Relaxed)
519 + self.kv_cache_bytes.load(Ordering::Relaxed)
520 + self.render_bytes.load(Ordering::Relaxed)
521 + self.llm_weight_staging_bytes.load(Ordering::Relaxed)
522 }
523
524 #[inline]
525 pub fn pressure(&self) -> f32 {
526 let budget = self.budget_bytes.load(Ordering::Relaxed);
527 if budget == 0 {
528 return 0.0;
529 }
530 (self.used_bytes() as f32 / budget as f32).clamp(0.0, 1.25)
531 }
532
533 #[inline]
534 pub fn mode(&self) -> OperationalMode {
535 match self.mode.load(Ordering::Relaxed) {
536 1 => OperationalMode::Eco,
537 2 => OperationalMode::Reserve,
538 _ => OperationalMode::Full,
539 }
540 }
541
542 #[inline]
543 pub fn refresh_mode(&self) {
544 let m = OperationalMode::from_pressure(self.pressure());
545 self.mode.store(m as u32, Ordering::Relaxed);
546 }
547
548 #[inline]
549 pub fn can_allocate(&self, extra_bytes: u64) -> bool {
550 let budget = self.budget_bytes.load(Ordering::Relaxed);
551 self.used_bytes() + extra_bytes <= budget
552 }
553}
554
555static LEDGER: std::sync::OnceLock<VramLedger> = std::sync::OnceLock::new();
557
558#[inline]
559pub fn global_vram_ledger() -> &'static VramLedger {
560 LEDGER.get_or_init(|| VramLedger::new(6 * 1024 * 1024 * 1024))
561}
562
563static LLM_HEAT_MILLI: AtomicU32 = AtomicU32::new(0);
566static LOGIC_FLASH_MILLI: AtomicU32 = AtomicU32::new(0);
567static BAKE_PULSE_MILLI: AtomicU32 = AtomicU32::new(0);
568static NETWORK_RIPPLE_MILLI: AtomicU32 = AtomicU32::new(0);
569static PRODUCER_CYCLE_MILLI: AtomicU32 = AtomicU32::new(0);
570static CONTEXT_RING_DROP_MILLI: AtomicU32 = AtomicU32::new(0);
571static DRAFT_ACCEPT_MILLI: AtomicU32 = AtomicU32::new(0);
572static DRAFT_LEN_CUR: AtomicU32 = AtomicU32::new(0);
573
574#[inline]
575fn pulse(atom: &AtomicU32, strength_milli: u32) {
576 let cur = atom.load(Ordering::Relaxed);
577 atom.store(cur.max(strength_milli), Ordering::Relaxed);
578}
579
580#[inline]
581fn sample_decay(atom: &AtomicU32, decay: u32) -> f32 {
582 let v = atom.load(Ordering::Relaxed);
583 atom.store(v.saturating_sub(decay), Ordering::Relaxed);
584 (v as f32 / 1000.0).clamp(0.0, 1.0)
585}
586
587#[inline]
589pub fn record_llm_decode_step() {
590 pulse(&LLM_HEAT_MILLI, 1000);
591}
592
593#[inline]
595pub fn record_logic_flash() {
596 pulse(&LOGIC_FLASH_MILLI, 900);
597}
598
599#[inline]
601pub fn record_bake_pulse() {
602 pulse(&BAKE_PULSE_MILLI, 850);
603 global_vram_ledger().refresh_mode();
604}
605
606#[inline]
608pub fn record_network_ripple() {
609 pulse(&NETWORK_RIPPLE_MILLI, 700);
610}
611
612#[inline]
614pub fn record_producer_cycle(pushed_tokens: u32) {
615 let strength = (500 + pushed_tokens.min(16) * 25).min(1000);
616 pulse(&PRODUCER_CYCLE_MILLI, strength);
617}
618
619#[inline]
621pub fn record_context_ring_drop() {
622 pulse(&CONTEXT_RING_DROP_MILLI, 800);
623}
624
625#[inline]
627pub fn record_draft_acceptance(accepted: u32, draft_len: u32) {
628 if draft_len > 0 {
629 let rate = ((accepted as u64) * 1000 / draft_len as u64).min(1000) as u32;
630 DRAFT_LEN_CUR.store(draft_len, Ordering::Relaxed);
631 pulse(&DRAFT_ACCEPT_MILLI, rate);
632 }
633}
634
635#[inline]
637pub fn sample_ambient_telemetry() -> [f32; 11] {
638 let ledger = global_vram_ledger();
639 [
640 ledger.pressure(),
641 sample_decay(&NETWORK_RIPPLE_MILLI, 25),
642 sample_decay(&BAKE_PULSE_MILLI, 20),
643 sample_decay(&LOGIC_FLASH_MILLI, 35),
644 sample_decay(&LLM_HEAT_MILLI, 30),
645 sample_decay(&PRODUCER_CYCLE_MILLI, 18),
646 sample_decay(&CONTEXT_RING_DROP_MILLI, 22),
647 0.08,
648 0.25,
649 ledger.pressure() * 0.5,
650 ledger.mode() as u32 as f32,
651 ]
652}
653
654#[cfg(not(target_arch = "wasm32"))]
657pub struct SharedGpuContext {
658 pub device: wgpu::Device,
659 pub queue: wgpu::Queue,
660 pub instance: wgpu::Instance,
662 pub adapter: wgpu::Adapter,
664 pub adapter_caps: GpuAdapterCaps,
666 pub enabled_features: GpuFeatureCaps,
668 pub timestamps_supported: bool,
671 pub timestamp_period_ns: f32,
673}
674
675#[cfg(not(target_arch = "wasm32"))]
676impl SharedGpuContext {
677 #[inline]
680 pub fn queue_for_lane(&self, lane: QueueLane) -> &wgpu::Queue {
681 let _ = lane;
682 &self.queue
683 }
684
685 #[inline]
686 pub fn queue_for_universe(&self, universe: ComputeUniverse) -> &wgpu::Queue {
687 self.queue_for_lane(universe.default_queue_lane())
688 }
689}
690
691#[cfg(not(target_arch = "wasm32"))]
692static SHARED_GPU: OnceLock<Option<SharedGpuContext>> = OnceLock::new();
693
694#[cfg(not(target_arch = "wasm32"))]
701fn resolve_dx12_compiler() -> wgpu::Dx12Compiler {
702 if let Ok(p) = std::env::var("QUALIA_DXC_PATH") {
703 if !p.trim().is_empty() {
704 log::info!("shared_gpu|dx12_compiler|DynamicDxc(env)={p}");
705 return wgpu::Dx12Compiler::DynamicDxc { dxc_path: p };
706 }
707 }
708 if let Ok(exe) = std::env::current_exe() {
709 if let Some(dir) = exe.parent() {
710 let dll = dir.join("dxcompiler.dll");
711 if dll.exists() {
712 log::info!(
713 "shared_gpu|dx12_compiler|DynamicDxc(vendored)={}",
714 dll.display()
715 );
716 return wgpu::Dx12Compiler::DynamicDxc {
717 dxc_path: dll.to_string_lossy().into_owned(),
718 };
719 }
720 }
721 }
722 wgpu::Dx12Compiler::Auto
723}
724
725#[cfg(not(target_arch = "wasm32"))]
726async fn init_shared_gpu_async() -> Result<SharedGpuContext, String> {
727 let dx12_compiler = resolve_dx12_compiler();
739 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
740 desc.backend_options.dx12.shader_compiler = dx12_compiler;
741 if let Some(backends) = caps::qualia_backend_override() {
742 log::info!("shared_gpu|backend_override|QUALIA_WGPU_BACKEND={backends:?}");
743 desc.backends = backends;
744 } else if cfg!(target_os = "windows") {
745 desc.backends = wgpu::Backends::DX12;
750 log::info!(
751 "shared_gpu|backend_default|windows->dx12 (override with QUALIA_WGPU_BACKEND=vulkan)"
752 );
753 }
754 let instance = wgpu::Instance::new(desc);
755 let adapter = instance
756 .request_adapter(&wgpu::RequestAdapterOptions {
757 power_preference: wgpu::PowerPreference::HighPerformance,
758 ..Default::default()
759 })
760 .await
761 .map_err(|e| format!("Failed to find wgpu adapter: {e}"))?;
762
763 #[cfg(target_os = "windows")]
767 if let Ok(memory) = crate::directml_bridge::probe_best_adapter_memory() {
768 let total_local = memory
769 .local_budget_bytes
770 .max(memory.dedicated_vram_bytes)
771 .max(memory.available_local_bytes());
772 if total_local > 0 {
773 global_vram_ledger().set_budget(total_local);
774 }
775 }
776
777 init_shared_gpu_for_adapter(instance, adapter).await
778}
779
780#[cfg(not(target_arch = "wasm32"))]
786pub(crate) async fn init_shared_gpu_for_adapter(
787 instance: wgpu::Instance,
788 adapter: wgpu::Adapter,
789) -> Result<SharedGpuContext, String> {
790 let adapter_caps = GpuAdapterCaps::from_adapter(&adapter);
791 log::info!(
792 "shared_gpu|adapter|{}|{}",
793 adapter_caps.summary_line(),
794 adapter_caps.llm_feature_line()
795 );
796 log::info!(
797 "shared_gpu|inference_backend|{}|recommend: {}",
798 adapter_caps.backend_label(),
799 caps::recommend_inference_backend(&adapter_caps)
800 );
801 if adapter_caps.is_integrated_gpu()
802 && std::env::var("QUALIA_LLM_ALLOW_IGPU").ok().as_deref() != Some("1")
803 {
804 log::warn!(
805 "shared_gpu|adapter|integrated_gpu_selected|set QUALIA_LLM_ALLOW_IGPU=1 to acknowledge this for native LLM runs"
806 );
807 }
808
809 let required_features = requested_native_llm_features(adapter.features());
814 let enabled_features = GpuFeatureCaps::from_features(required_features);
815 log::info!(
816 "shared_gpu|enabled_features|{}",
817 enabled_features.compact_flags()
818 );
819 if adapter_caps.features.cooperative_matrix && !enabled_features.cooperative_matrix {
820 log::info!(
821 "shared_gpu|cooperative_matrix_supported_but_disabled|set QUALIA_WGPU_EXPERIMENTAL_FEATURES=1 to request it"
822 );
823 }
824 let ts_supported = enabled_features.timestamp_query;
825 let adapter_limits = adapter.limits();
833 let required_limits = wgpu::Limits {
834 max_buffer_size: adapter_limits.max_buffer_size,
835 max_storage_buffer_binding_size: adapter_limits.max_storage_buffer_binding_size,
836 ..wgpu::Limits::default()
837 };
838 let experimental_features = if required_features.intersects(
839 wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX | wgpu::Features::EXPERIMENTAL_RAY_QUERY,
840 ) {
841 unsafe { wgpu::ExperimentalFeatures::enabled() }
845 } else {
846 wgpu::ExperimentalFeatures::disabled()
847 };
848 let (device, queue) = adapter
849 .request_device(&wgpu::DeviceDescriptor {
850 required_features,
851 required_limits,
852 experimental_features,
853 ..Default::default()
854 })
855 .await
856 .map_err(|e| e.to_string())?;
857
858 let timestamp_period_ns = if ts_supported {
859 queue.get_timestamp_period()
860 } else {
861 0.0
862 };
863
864 Ok(SharedGpuContext {
865 device,
866 queue,
867 instance,
868 adapter,
869 adapter_caps,
870 enabled_features,
871 timestamps_supported: ts_supported,
872 timestamp_period_ns,
873 })
874}
875
876#[cfg(not(target_arch = "wasm32"))]
883pub fn try_shared_gpu() -> Option<&'static SharedGpuContext> {
884 SHARED_GPU
885 .get_or_init(|| {
886 let handle = match tokio::runtime::Handle::try_current() {
887 Ok(h) => h,
888 Err(_) => match tokio::runtime::Runtime::new() {
889 Ok(rt) => Box::leak(Box::new(rt)).handle().clone(),
890 Err(_) => return None,
891 },
892 };
893 tokio::task::block_in_place(|| handle.block_on(init_shared_gpu_async()).ok())
894 })
895 .as_ref()
896}
897
898#[cfg(not(target_arch = "wasm32"))]
902pub fn shared_gpu() -> &'static SharedGpuContext {
903 try_shared_gpu().expect("shared wgpu init failed")
904}
905
906#[cfg(all(test, not(target_arch = "wasm32")))]
907mod shared_gpu_robustness_tests {
908 use super::*;
909
910 #[test]
911 fn try_shared_gpu_never_panics_and_is_idempotent() {
912 let first = try_shared_gpu().is_some();
916 let second = try_shared_gpu().is_some();
917 assert_eq!(first, second, "try_shared_gpu must be cached + consistent");
918 }
919}
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924
925 #[test]
929 #[serial_test::serial(gpu)]
930 fn report_inference_backend() {
931 if !crate::wgsl_forge::test_gpu_available() {
932 return;
933 }
934 let g = shared_gpu();
935 eprintln!(
936 "[inference-backend] selected = {} | recommend: {}",
937 g.adapter_caps.backend_label(),
938 recommend_inference_backend(&g.adapter_caps),
939 );
940 eprintln!("[inference-backend] {}", g.adapter_caps.summary_line());
941 assert!(!g.adapter_caps.backend_label().is_empty());
943 }
944
945 #[test]
946 fn pressure_triggers_eco_and_reserve() {
947 let ledger = VramLedger::new(1000);
948 ledger.record_tensor(500);
949 assert_eq!(ledger.mode(), OperationalMode::Full);
950 ledger.record_kv_cache(300);
951 assert_eq!(ledger.mode(), OperationalMode::Eco);
952 ledger.record_render(200);
953 assert_eq!(ledger.mode(), OperationalMode::Reserve);
954 }
955
956 #[test]
957 fn universe_partitions_sum_below_total() {
958 let orch = UniverseOrchestrator::from_total_budget_full(10_000);
959 let sum: u64 = orch.partitions.iter().map(|p| p.vram_budget_bytes).sum();
960 assert!(sum <= 10_000);
961 assert_eq!(
962 orch.partition(ComputeUniverse::LlmInference)
963 .vram_budget_bytes,
964 5500
965 );
966 }
967
968 #[test]
969 fn reserve_mode_caps_u2_at_ten_percent() {
970 let orch = UniverseOrchestrator::from_total_budget(10_000, OperationalMode::Reserve);
971 assert_eq!(
972 orch.partition(ComputeUniverse::Viewport).vram_budget_bytes,
973 1_000
974 );
975 assert_eq!(
976 orch.partition(ComputeUniverse::LlmInference)
977 .vram_budget_bytes,
978 4_500
979 );
980 assert_eq!(orch.active_mode, OperationalMode::Reserve);
981 }
982
983 #[test]
984 fn ledger_ranges_are_consecutive_non_overlapping() {
985 let orch = UniverseOrchestrator::from_total_budget(10_000, OperationalMode::Eco);
986 let u0 = orch.partition(ComputeUniverse::LlmInference).ledger_range;
987 let u1 = orch.partition(ComputeUniverse::Tensor10D).ledger_range;
988 let u2 = orch.partition(ComputeUniverse::Viewport).ledger_range;
989 assert_eq!(u0.offset, 0);
990 assert_eq!(u1.offset, u0.end());
991 assert_eq!(u2.offset, u1.end());
992 assert!(u2.end() <= 10_000);
993 }
994
995 #[test]
996 fn u2_cannot_evict_u0_kv_under_reserve() {
997 let orch = UniverseOrchestrator::from_total_budget_full(10_000);
998 assert_eq!(
999 orch.effective_mode(ComputeUniverse::LlmInference, OperationalMode::Reserve),
1000 OperationalMode::Full
1001 );
1002 assert_eq!(
1003 orch.effective_mode(ComputeUniverse::Viewport, OperationalMode::Reserve),
1004 OperationalMode::Reserve
1005 );
1006 }
1007
1008 #[test]
1009 fn universe_budget_isolated() {
1010 let ledger = VramLedger::new(10_000);
1011 let orch = UniverseOrchestrator::from_total_budget_full(10_000);
1012 ledger.record_render(2000);
1013 assert!(ledger.can_allocate_in_universe(&orch, ComputeUniverse::LlmInference, 5000));
1014 assert!(!ledger.can_allocate_in_universe(&orch, ComputeUniverse::Viewport, 2000));
1015 }
1016
1017 #[test]
1018 fn ambient_draw_instant_step_by_mode() {
1019 let orch =
1020 UniverseOrchestrator::from_total_budget(6 * 1024 * 1024 * 1024, OperationalMode::Full);
1021 assert_eq!(
1022 orch.max_particles(ComputeUniverse::Viewport, OperationalMode::Full),
1023 50_000
1024 );
1025 assert_eq!(
1026 orch.max_particles(ComputeUniverse::Viewport, OperationalMode::Eco),
1027 8_000
1028 );
1029 assert_eq!(
1030 orch.max_particles(ComputeUniverse::Viewport, OperationalMode::Reserve),
1031 0
1032 );
1033
1034 let resident = 50_000_u32;
1035 assert_eq!(
1036 ambient_draw_instances_for_mode(resident, OperationalMode::Full),
1037 50_000
1038 );
1039 assert_eq!(
1040 ambient_draw_instances_for_mode(resident, OperationalMode::Eco),
1041 8_000
1042 );
1043 assert_eq!(
1044 ambient_draw_instances_for_mode(resident, OperationalMode::Reserve),
1045 0
1046 );
1047 assert_eq!(
1048 ambient_draw_instances_for_mode(3_000, OperationalMode::Eco),
1049 3_000
1050 );
1051 }
1052}