1use std::{
25 fmt,
26 sync::{
27 Arc, Mutex, OnceLock, PoisonError, Weak,
28 atomic::{AtomicU64, Ordering},
29 },
30 time::{Duration, Instant},
31};
32
33use cudf::{
34 DeviceMemoryManager, DeviceMemoryManagerInstallationState, DeviceMemoryManagerOptions,
35 io::{KvikioProcessConfig, configure_kvikio_process},
36};
37use query_planning::error::{QueryError, QueryErrorCode, Result, bytes_with_gib};
38use tokio::task::spawn_blocking;
39
40#[cfg(feature = "qualification")]
41use super::DeviceQuarantineInjection;
42pub use super::backend_close::GpuBackendCloseSnapshot;
43pub(crate) use super::observation_completion::ObservationCompletionDispatcher;
44use super::{
45 BackendDiagnostics, DeviceCapabilities, DeviceFacts, DeviceResourceProfile, DeviceTelemetrySnapshot,
46 QueryAdmissionRequest, QueryExecutionConfig, QueryHandle, QueueBounds,
47 backend_close::{CloseCoordinator, ConstructionRollback, reject_reentrant_observation_close},
48 backend_lifecycle::{
49 BackendLifecycle, GpuLifecyclePermit, ObservationPermit, PublicHandleLease, PublicHandleTracker,
50 },
51 backend_metrics::{
52 BACKEND_METRIC_DEVICE_CAPACITY, BackendAttemptDurations, BackendMetricsSnapshot, BackendMetricsStore,
53 },
54 gpu::{AlgeonGpuInventory, GpuInventory, LiveBackendLease, claim_live_backend, normalize_device_ordinals},
55 gpu_execution_pool::RESERVED_LIFECYCLE_WORKERS,
56 observation_completion::ObservationCompletionExecutor,
57 query_service::QueryService,
58};
59use crate::{
60 attempt::{
61 AttemptBackendCounters, AttemptBackendCountersSnapshot, AttemptBegin, AttemptCompletionQueue, AttemptOwner,
62 BeginAttemptRequest,
63 },
64 native_error::cudf_query_error,
65 observability::QueryAttemptReport,
66};
67
68const BUILD_OPERATION: &str = "query_engine.gpu_backend_core.build";
69const CLOSE_OPERATION: &str = "query_engine.gpu_backend_core.close";
70const RESOURCE_OPERATION: &str = "query_engine.gpu_backend_core.resource";
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum GpuMemoryOwnership {
76 WholeDeviceExclusive,
78 StaticPartition {
80 allocation_limit_bytes: u64,
82 },
83}
84
85impl GpuMemoryOwnership {
86 pub(crate) fn allocation_limit_bytes(self) -> Option<u64> {
87 match self {
88 Self::WholeDeviceExclusive => None,
89 Self::StaticPartition { allocation_limit_bytes } => Some(allocation_limit_bytes),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104#[non_exhaustive]
105pub struct GpuDeviceCapacityProbe {
106 pub(crate) device_ordinal: usize,
107 pub(crate) physical_total_bytes: Option<u64>,
111 pub cuda_total_bytes: u64,
114 pub cuda_free_bytes: u64,
116 pub owned_capacity_bytes: u64,
118 pub max_declarable_limit_bytes: u64,
121 pub(crate) backend_reserve_bytes: u64,
122 pub max_managed_capacity_bytes: u64,
124}
125
126pub fn probe_device_capacity(
133 device_ordinal: usize,
134 memory_ownership: GpuMemoryOwnership,
135 backend_reserve_bytes: u64,
136) -> Result<GpuDeviceCapacityProbe> {
137 let facts = AlgeonGpuInventory.facts(device_ordinal)?;
138 let (owned_capacity_bytes, max_managed_capacity_bytes) = resolve_probe_capacities(
139 device_ordinal,
140 &facts,
141 memory_ownership.allocation_limit_bytes(),
142 backend_reserve_bytes,
143 )?;
144 Ok(GpuDeviceCapacityProbe {
145 device_ordinal,
146 physical_total_bytes: facts.physical_total_bytes,
147 cuda_total_bytes: facts.total_bytes,
148 cuda_free_bytes: facts.free_bytes,
149 owned_capacity_bytes,
150 max_declarable_limit_bytes: owned_capacity_bytes,
151 backend_reserve_bytes,
152 max_managed_capacity_bytes,
153 })
154}
155
156fn resolve_probe_capacities(
157 device_ordinal: usize,
158 facts: &DeviceFacts,
159 allocation_limit_bytes: Option<u64>,
160 backend_reserve_bytes: u64,
161) -> Result<(u64, u64)> {
162 const OPERATION: &str = "query_engine.gpu_backend_core.probe_capacity";
163 if let Some(allocation_limit) = allocation_limit_bytes.filter(|limit| *limit > facts.total_bytes) {
164 return Err(QueryError::new(
165 QueryErrorCode::InvalidConfiguration,
166 OPERATION,
167 format!(
168 "device {device_ordinal} static partition allocation_limit_bytes is {}, above the CUDA-visible device memory of {}; the largest accepted allocation_limit_bytes is {}",
169 bytes_with_gib(allocation_limit),
170 bytes_with_gib(facts.total_bytes),
171 bytes_with_gib(facts.total_bytes),
172 ),
173 )
174 .with_fact("device_ordinal", device_ordinal)
175 .with_fact("allocation_limit_bytes", allocation_limit)
176 .with_fact("cuda_total_bytes", facts.total_bytes)
177 .with_fact(
178 "physical_total_bytes",
179 facts.physical_total_bytes.map_or("unavailable".to_owned(), |total| total.to_string()),
180 )
181 .with_fact("reason", "static_partition_exceeds_physical_capacity"));
182 }
183 let owned_capacity_bytes = allocation_limit_bytes.unwrap_or(facts.total_bytes);
184 let max_managed_capacity_bytes =
185 owned_capacity_bytes.checked_sub(backend_reserve_bytes).filter(|capacity| *capacity > 0).ok_or_else(|| {
186 QueryError::new(
187 QueryErrorCode::InvalidConfiguration,
188 OPERATION,
189 format!(
190 "device {device_ordinal} backend_reserve_bytes is {} and leaves no managed capacity out of the owned {}; the largest accepted backend_reserve_bytes is {}",
191 bytes_with_gib(backend_reserve_bytes),
192 bytes_with_gib(owned_capacity_bytes),
193 bytes_with_gib(owned_capacity_bytes.saturating_sub(1)),
194 ),
195 )
196 .with_fact("device_ordinal", device_ordinal)
197 .with_fact("owned_capacity_bytes", owned_capacity_bytes)
198 .with_fact("backend_reserve_bytes", backend_reserve_bytes)
199 .with_fact("reason", "backend_reserve_exhausts_capacity")
200 })?;
201 Ok((owned_capacity_bytes, max_managed_capacity_bytes))
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
215#[non_exhaustive]
216pub enum GpuDeviceMemoryLimit {
217 Automatic {
220 headroom_bytes: u64,
222 fallback_bytes: u64,
224 fraction: Option<GpuMemoryFraction>,
226 },
227 Explicit {
229 limit_bytes: u64,
231 },
232}
233
234impl Default for GpuDeviceMemoryLimit {
235 fn default() -> Self {
236 Self::Automatic { headroom_bytes: 500 * 1024 * 1024, fallback_bytes: 256 * 1024 * 1024, fraction: None }
237 }
238}
239
240#[derive(Clone, Copy, PartialEq, Eq, Hash)]
244pub struct GpuMemoryFraction(u64);
245
246impl GpuMemoryFraction {
247 pub fn new(value: f64) -> Result<Self> {
251 if !value.is_finite() || value <= 0.0 || value > 1.0 {
252 return Err(invalid_config("memory_limit.fraction", "must be greater than zero and at most one")
253 .with_fact("reason", "invalid_fraction"));
254 }
255 Ok(Self(value.to_bits()))
256 }
257
258 pub fn value(self) -> f64 {
260 f64::from_bits(self.0)
261 }
262
263 pub(crate) fn capacity_bytes(self, owned_capacity_bytes: u64) -> u64 {
264 ((owned_capacity_bytes as f64 * self.value()).floor() as u64).min(owned_capacity_bytes)
265 }
266}
267
268impl fmt::Debug for GpuMemoryFraction {
269 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
270 formatter.debug_tuple("GpuMemoryFraction").field(&self.value()).finish()
271 }
272}
273
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276#[non_exhaustive]
277pub struct GpuBackendDeviceInstallationBounds {
278 pub stream_pool_size: usize,
280 pub worker_count: usize,
282 pub lane_count: usize,
284}
285
286impl GpuBackendDeviceInstallationBounds {
287 pub const fn new(stream_pool_size: usize, worker_count: usize, lane_count: usize) -> Self {
289 Self { stream_pool_size, worker_count, lane_count }
290 }
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct GpuBackendDeviceProfile {
302 device_ordinal: usize,
303 device_capabilities: DeviceCapabilities,
304 memory_limit: GpuDeviceMemoryLimit,
305 backend_reserve_bytes: u64,
306 cache_cap_bytes: u64,
307 default_query_limit_bytes: Option<u64>,
308 source_work_per_attempt: usize,
309 max_active_attempts: usize,
310 installation_bounds: Option<GpuBackendDeviceInstallationBounds>,
311 install_telemetry: bool,
312}
313
314impl GpuBackendDeviceProfile {
315 pub(crate) const DEFAULT_QUERY_LIMIT_BYTES: Option<u64> = None;
318 pub(crate) const DEFAULT_SOURCE_WORK_PER_ATTEMPT: usize = 1;
321 pub(crate) const DEFAULT_MAX_ACTIVE_ATTEMPTS: usize = 2;
324
325 pub fn new(device_ordinal: usize) -> Self {
328 Self {
329 device_ordinal,
330 device_capabilities: DeviceCapabilities::default(),
331 memory_limit: GpuDeviceMemoryLimit::default(),
332 backend_reserve_bytes: 0,
333 cache_cap_bytes: 0,
334 default_query_limit_bytes: Self::DEFAULT_QUERY_LIMIT_BYTES,
335 source_work_per_attempt: Self::DEFAULT_SOURCE_WORK_PER_ATTEMPT,
336 max_active_attempts: Self::DEFAULT_MAX_ACTIVE_ATTEMPTS,
337 installation_bounds: None,
338 install_telemetry: true,
339 }
340 }
341
342 pub fn with_memory_limit(mut self, memory_limit: GpuDeviceMemoryLimit) -> Self {
345 self.memory_limit = memory_limit;
346 self
347 }
348
349 pub fn with_device_capabilities(mut self, device_capabilities: DeviceCapabilities) -> Self {
355 self.device_capabilities = device_capabilities;
356 self
357 }
358
359 pub fn with_backend_reserve_bytes(mut self, backend_reserve_bytes: u64) -> Self {
362 self.backend_reserve_bytes = backend_reserve_bytes;
363 self
364 }
365
366 pub fn with_cache_cap_bytes(mut self, cache_cap_bytes: u64) -> Self {
368 self.cache_cap_bytes = cache_cap_bytes;
369 self
370 }
371
372 pub fn cache_cap_bytes(&self) -> u64 {
374 self.cache_cap_bytes
375 }
376
377 pub fn with_default_query_limit_bytes(mut self, default_query_limit_bytes: u64) -> Self {
381 self.default_query_limit_bytes = Some(default_query_limit_bytes);
382 self
383 }
384
385 pub fn with_source_work_per_attempt(mut self, source_work_per_attempt: usize) -> Self {
390 self.source_work_per_attempt = source_work_per_attempt;
391 self
392 }
393
394 pub fn with_max_active_attempts(mut self, max_active_attempts: usize) -> Self {
397 self.max_active_attempts = max_active_attempts;
398 self
399 }
400
401 pub fn with_derived_installation_bounds(mut self, max_active_attempts: usize) -> Self {
410 self.max_active_attempts = max_active_attempts;
411 self.installation_bounds = None;
412 self
413 }
414
415 pub fn with_installation_bounds(mut self, installation_bounds: GpuBackendDeviceInstallationBounds) -> Self {
421 self.installation_bounds = Some(installation_bounds);
422 self
423 }
424
425 pub fn with_telemetry(mut self, install_telemetry: bool) -> Self {
428 self.install_telemetry = install_telemetry;
429 self
430 }
431
432 pub fn device_ordinal(&self) -> usize {
434 self.device_ordinal
435 }
436
437 pub fn device_capabilities(&self) -> &DeviceCapabilities {
439 &self.device_capabilities
440 }
441
442 pub fn memory_limit(&self) -> GpuDeviceMemoryLimit {
444 self.memory_limit
445 }
446
447 pub fn backend_reserve_bytes(&self) -> u64 {
449 self.backend_reserve_bytes
450 }
451
452 pub fn default_query_limit_bytes(&self) -> Option<u64> {
456 self.default_query_limit_bytes
457 }
458
459 pub fn source_work_per_attempt(&self) -> usize {
461 self.source_work_per_attempt
462 }
463
464 pub fn max_active_attempts(&self) -> usize {
466 self.max_active_attempts
467 }
468
469 pub fn installation_bounds(&self) -> Option<GpuBackendDeviceInstallationBounds> {
472 self.installation_bounds
473 }
474
475 pub fn derived_installation_bounds(&self) -> Result<GpuBackendDeviceInstallationBounds> {
481 let auxiliary = self.max_active_attempts.checked_mul(self.source_work_per_attempt).ok_or_else(|| {
482 invalid_device_config(self.device_ordinal, "source_work_per_attempt", "overflowed")
483 .with_fact("reason", "aux_source_work_overflow")
484 })?;
485 let stream_pool_size = self.max_active_attempts.checked_add(auxiliary).ok_or_else(|| {
486 invalid_device_config(self.device_ordinal, "stream_pool_size", "overflowed")
487 .with_fact("reason", "required_slot_overflow")
488 })?;
489 let worker_count = self.max_active_attempts.checked_add(RESERVED_LIFECYCLE_WORKERS).ok_or_else(|| {
490 invalid_device_config(self.device_ordinal, "worker_count", "overflowed")
491 .with_fact("reason", "required_worker_overflow")
492 })?;
493 Ok(GpuBackendDeviceInstallationBounds::new(stream_pool_size, worker_count, self.max_active_attempts))
494 }
495
496 pub fn install_telemetry(&self) -> bool {
498 self.install_telemetry
499 }
500}
501
502#[derive(Debug, Clone, PartialEq, Eq)]
508pub struct GpuBackendDeviceConfig {
509 profile: GpuBackendDeviceProfile,
510 memory_ownership: GpuMemoryOwnership,
511}
512
513impl GpuBackendDeviceConfig {
514 pub fn homogeneous(
523 device_ordinals: impl IntoIterator<Item = usize>,
524 template: &GpuBackendDeviceProfile,
525 memory_ownership: GpuMemoryOwnership,
526 ) -> Vec<Self> {
527 device_ordinals
528 .into_iter()
529 .map(|device_ordinal| {
530 let mut profile = template.clone();
531 profile.device_ordinal = device_ordinal;
532 Self::new(profile, memory_ownership)
533 })
534 .collect()
535 }
536
537 pub fn new(profile: GpuBackendDeviceProfile, memory_ownership: GpuMemoryOwnership) -> Self {
540 Self { profile, memory_ownership }
541 }
542
543 pub fn profile(&self) -> &GpuBackendDeviceProfile {
545 &self.profile
546 }
547
548 pub fn device_ordinal(&self) -> usize {
550 self.profile.device_ordinal
551 }
552
553 pub fn device_capabilities(&self) -> &DeviceCapabilities {
555 &self.profile.device_capabilities
556 }
557
558 pub fn memory_ownership(&self) -> GpuMemoryOwnership {
560 self.memory_ownership
561 }
562
563 pub fn memory_limit(&self) -> GpuDeviceMemoryLimit {
565 self.profile.memory_limit
566 }
567
568 pub fn backend_reserve_bytes(&self) -> u64 {
570 self.profile.backend_reserve_bytes
571 }
572
573 pub fn cache_cap_bytes(&self) -> u64 {
575 self.profile.cache_cap_bytes
576 }
577
578 pub fn default_query_limit_bytes(&self) -> Option<u64> {
582 self.profile.default_query_limit_bytes
583 }
584
585 pub fn source_work_per_attempt(&self) -> usize {
587 self.profile.source_work_per_attempt
588 }
589
590 pub fn max_active_attempts(&self) -> usize {
592 self.profile.max_active_attempts
593 }
594
595 pub fn install_telemetry(&self) -> bool {
597 self.profile.install_telemetry
598 }
599}
600
601#[derive(Debug, Clone)]
608pub struct GpuBackendConfig {
609 devices: Vec<GpuBackendDeviceConfig>,
610 execution_config: QueryExecutionConfig,
611 queue_bounds: QueueBounds,
612 attempt_completion_queue_capacity: Option<usize>,
613}
614
615impl GpuBackendConfig {
616 pub fn new(
617 devices: Vec<GpuBackendDeviceConfig>,
618 execution_config: QueryExecutionConfig,
619 queue_bounds: QueueBounds,
620 ) -> Self {
621 Self { devices, execution_config, queue_bounds, attempt_completion_queue_capacity: None }
622 }
623
624 pub fn with_attempt_completion_queue_capacity(mut self, capacity: usize) -> Self {
625 self.attempt_completion_queue_capacity = Some(capacity);
626 self
627 }
628
629 pub fn validate(self) -> Result<ValidatedGpuBackendConfig> {
632 let Self { mut devices, execution_config, queue_bounds, attempt_completion_queue_capacity } = self;
633 let ordinals = devices.iter().map(GpuBackendDeviceConfig::device_ordinal).collect::<Vec<_>>();
634 normalize_device_ordinals(&ordinals)?;
635 devices.sort_by_key(GpuBackendDeviceConfig::device_ordinal);
636 if let Some(device) = devices.iter().find(|device| device.device_ordinal() >= BACKEND_METRIC_DEVICE_CAPACITY) {
637 return Err(invalid_device_config(
638 device.device_ordinal(),
639 "device_ordinal",
640 "exceeds the fixed backend metrics device capacity",
641 )
642 .with_fact("device_capacity", BACKEND_METRIC_DEVICE_CAPACITY)
643 .with_fact("reason", "backend_metrics_device_ordinal_out_of_range"));
644 }
645 let devices = devices.into_iter().map(validate_device).collect::<Result<Vec<_>>>()?;
646 let derived = devices.iter().try_fold(0usize, |sum, device| {
647 sum.checked_add(device.config.profile.max_active_attempts).ok_or_else(|| {
648 invalid_config("max_active_attempts", "derived global maximum overflowed")
649 .with_fact("reason", "derived_global_max_overflow")
650 })
651 })?;
652 if derived == 0 {
653 return Err(invalid_config("max_active_attempts", "derived global maximum must be positive")
654 .with_fact("reason", "zero"));
655 }
656 execution_config.validate()?;
657 queue_bounds.validate()?;
658 Ok(ValidatedGpuBackendConfig { devices, execution_config, queue_bounds, attempt_completion_queue_capacity })
659 }
660}
661
662#[derive(Debug, Clone)]
665pub struct ValidatedGpuBackendConfig {
666 devices: Vec<ValidatedGpuBackendDevice>,
667 execution_config: QueryExecutionConfig,
668 queue_bounds: QueueBounds,
669 attempt_completion_queue_capacity: Option<usize>,
670}
671
672impl ValidatedGpuBackendConfig {
673 pub fn devices(&self) -> impl ExactSizeIterator<Item = &GpuBackendDeviceConfig> + '_ {
675 self.devices.iter().map(|device| &device.config)
676 }
677
678 pub(crate) fn validated_devices(&self) -> &[ValidatedGpuBackendDevice] {
679 &self.devices
680 }
681
682 pub(crate) fn device_ordinals(&self) -> Vec<usize> {
683 self.devices().map(GpuBackendDeviceConfig::device_ordinal).collect()
684 }
685
686 pub(crate) fn execution_config(&self) -> &QueryExecutionConfig {
687 &self.execution_config
688 }
689
690 pub(crate) fn queue_bounds(&self) -> &QueueBounds {
691 &self.queue_bounds
692 }
693}
694
695#[derive(Debug, Clone)]
698pub(crate) struct ValidatedGpuBackendDevice {
699 pub(crate) config: GpuBackendDeviceConfig,
700 pub(crate) installation_bounds: GpuBackendDeviceInstallationBounds,
701 pub(crate) allocation_limit_bytes: Option<usize>,
702}
703
704fn validate_device(config: GpuBackendDeviceConfig) -> Result<ValidatedGpuBackendDevice> {
705 let installation_bounds = resolve_installation_bounds(&config.profile)?;
706 let allocation_limit_bytes = validate_device_config(&config, installation_bounds)?;
707 Ok(ValidatedGpuBackendDevice { config, installation_bounds, allocation_limit_bytes })
708}
709
710fn resolve_installation_bounds(profile: &GpuBackendDeviceProfile) -> Result<GpuBackendDeviceInstallationBounds> {
714 let derived = profile.derived_installation_bounds()?;
715 let Some(explicit) = profile.installation_bounds else {
716 return Ok(derived);
717 };
718 if explicit.stream_pool_size < derived.stream_pool_size {
719 return Err(invalid_device_config(
720 profile.device_ordinal,
721 "stream_pool_size",
722 "must cover root and auxiliary source work for every active attempt",
723 )
724 .with_fact("reason", "stream_pool_below_required_formula")
725 .with_fact("stream_pool_size", explicit.stream_pool_size)
726 .with_fact("required_slot_capacity", derived.stream_pool_size)
727 .with_fact("max_active_attempts", profile.max_active_attempts)
728 .with_fact("source_work_per_attempt", profile.source_work_per_attempt));
729 }
730 Ok(explicit)
731}
732
733fn validate_device_config(
736 device: &GpuBackendDeviceConfig,
737 bounds: GpuBackendDeviceInstallationBounds,
738) -> Result<Option<usize>> {
739 for (field, value) in [
740 ("max_active_attempts", device.profile.max_active_attempts),
741 ("source_work_per_attempt", device.profile.source_work_per_attempt),
742 ("stream_pool_size", bounds.stream_pool_size),
743 ("worker_count", bounds.worker_count),
744 ("lane_count", bounds.lane_count),
745 ] {
746 if value == 0 {
747 return Err(invalid_device_config(device.profile.device_ordinal, field, "must be greater than zero")
748 .with_fact("reason", "zero"));
749 }
750 }
751 if device.profile.default_query_limit_bytes == Some(0) {
752 return Err(invalid_device_config(
753 device.profile.device_ordinal,
754 "default_query_limit_bytes",
755 "must be greater than zero",
756 )
757 .with_fact("reason", "zero"));
758 }
759 let required_workers = device.profile.max_active_attempts.saturating_add(RESERVED_LIFECYCLE_WORKERS);
762 if bounds.worker_count < required_workers {
763 return Err(invalid_device_config(
764 device.profile.device_ordinal,
765 "worker_count",
766 "must cover every configured active query slot plus the reserved lifecycle worker",
767 )
768 .with_fact("reason", "worker_count_below_active_limit_and_lifecycle_reserve")
769 .with_fact("installation_bound", bounds.worker_count)
770 .with_fact("max_active_attempts", device.profile.max_active_attempts)
771 .with_fact("reserved_lifecycle_workers", RESERVED_LIFECYCLE_WORKERS));
772 }
773 for (field, value) in [("stream_pool_size", bounds.stream_pool_size), ("lane_count", bounds.lane_count)] {
774 if value < device.profile.max_active_attempts {
775 return Err(invalid_device_config(
776 device.profile.device_ordinal,
777 field,
778 "must cover every configured active query slot",
779 )
780 .with_fact("reason", "installation_bound_below_active_limit")
781 .with_fact("installation_bound", value)
782 .with_fact("max_active_attempts", device.profile.max_active_attempts));
783 }
784 }
785 match device.profile.memory_limit {
786 GpuDeviceMemoryLimit::Automatic { fallback_bytes: 0, .. } => {
787 return Err(invalid_device_config(
788 device.profile.device_ordinal,
789 "memory_limit.fallback_bytes",
790 "must be greater than zero",
791 )
792 .with_fact("reason", "zero"));
793 },
794 GpuDeviceMemoryLimit::Automatic { fallback_bytes, .. }
795 if device.profile.backend_reserve_bytes >= fallback_bytes =>
796 {
797 return Err(invalid_device_config(
798 device.profile.device_ordinal,
799 "backend_reserve_bytes",
800 format!(
801 "is {}, at or above the automatic fallback capacity of {}; the largest accepted value is {}",
802 bytes_with_gib(device.profile.backend_reserve_bytes),
803 bytes_with_gib(fallback_bytes),
804 bytes_with_gib(fallback_bytes - 1),
805 ),
806 )
807 .with_fact("reason", "reserve_exhausts_fallback")
808 .with_fact("fallback_bytes", fallback_bytes));
809 },
810 GpuDeviceMemoryLimit::Explicit { limit_bytes: 0 } => {
811 return Err(invalid_device_config(
812 device.profile.device_ordinal,
813 "memory_limit_bytes",
814 "must be greater than zero",
815 )
816 .with_fact("reason", "zero"));
817 },
818 GpuDeviceMemoryLimit::Explicit { limit_bytes } if device.profile.backend_reserve_bytes >= limit_bytes => {
819 return Err(invalid_device_config(
820 device.profile.device_ordinal,
821 "backend_reserve_bytes",
822 format!(
823 "is {}, at or above memory_limit_bytes of {}; the largest accepted value is {}",
824 bytes_with_gib(device.profile.backend_reserve_bytes),
825 bytes_with_gib(limit_bytes),
826 bytes_with_gib(limit_bytes - 1),
827 ),
828 )
829 .with_fact("reason", "reserve_exhausts_explicit_limit")
830 .with_fact("limit_bytes", limit_bytes));
831 },
832 _ => {},
833 }
834 if let GpuMemoryOwnership::StaticPartition { allocation_limit_bytes } = device.memory_ownership {
835 if allocation_limit_bytes == 0 {
836 return Err(invalid_device_config(
837 device.profile.device_ordinal,
838 "memory_ownership.allocation_limit_bytes",
839 "must be greater than zero",
840 )
841 .with_fact("reason", "zero"));
842 }
843 native_allocation_limit_bytes(device)?;
844 if device.profile.backend_reserve_bytes >= allocation_limit_bytes {
845 return Err(invalid_device_config(
846 device.profile.device_ordinal,
847 "backend_reserve_bytes",
848 format!(
849 "is {}, at or above the static partition of {}; the largest accepted value is {}",
850 bytes_with_gib(device.profile.backend_reserve_bytes),
851 bytes_with_gib(allocation_limit_bytes),
852 bytes_with_gib(allocation_limit_bytes - 1),
853 ),
854 )
855 .with_fact("reason", "reserve_exhausts_static_partition")
856 .with_fact("allocation_limit_bytes", allocation_limit_bytes));
857 }
858 if let GpuDeviceMemoryLimit::Explicit { limit_bytes } = device.profile.memory_limit
859 && limit_bytes > allocation_limit_bytes
860 {
861 return Err(invalid_device_config(
862 device.profile.device_ordinal,
863 "memory_limit_bytes",
864 format!(
865 "is {}, above the static partition of {}; the largest accepted value is {}",
866 bytes_with_gib(limit_bytes),
867 bytes_with_gib(allocation_limit_bytes),
868 bytes_with_gib(allocation_limit_bytes),
869 ),
870 )
871 .with_fact("reason", "limit_exceeds_static_partition")
872 .with_fact("allocation_limit_bytes", allocation_limit_bytes));
873 }
874 }
875 let allocation_limit_bytes = native_allocation_limit_bytes(device)?;
876 DeviceMemoryManagerOptions { allocation_limit_bytes, ..Default::default() }.validate().map_err(|error| {
877 cudf_query_error(BUILD_OPERATION, "invalid per-device memory manager installation options", error)
878 .with_fact("field", "device_memory_manager_options")
879 .with_fact("device_ordinal", device.profile.device_ordinal)
880 .with_fact("reason", "invalid_device_memory_manager_options")
881 })?;
882 Ok(allocation_limit_bytes)
883}
884
885fn native_allocation_limit_bytes(device: &GpuBackendDeviceConfig) -> Result<Option<usize>> {
886 device
887 .memory_ownership
888 .allocation_limit_bytes()
889 .map(|limit| {
890 usize::try_from(limit).map_err(|_error| {
891 invalid_device_config(
892 device.profile.device_ordinal,
893 "memory_ownership.allocation_limit_bytes",
894 "does not fit the native allocation-size type",
895 )
896 .with_fact("reason", "out_of_range")
897 .with_fact("allocation_limit_bytes", limit)
898 })
899 })
900 .transpose()
901}
902
903fn invalid_device_config(device_ordinal: usize, field: &'static str, message: impl fmt::Display) -> QueryError {
904 QueryError::new(
905 QueryErrorCode::InvalidConfiguration,
906 BUILD_OPERATION,
907 format!("device {device_ordinal} {field} {message}"),
908 )
909 .with_fact("field", field)
910 .with_fact("device_ordinal", device_ordinal)
911}
912
913fn invalid_config(field: &'static str, message: impl fmt::Display) -> QueryError {
914 QueryError::new(QueryErrorCode::InvalidConfiguration, BUILD_OPERATION, format!("{field} {message}"))
915 .with_fact("field", field)
916}
917
918#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
929#[non_exhaustive]
930pub(crate) enum GpuBackendConstructionCheckpoint {
931 BeforeDeviceDiscovery { device_ordinal: usize },
932 AfterDeviceDiscovery { device_ordinal: usize },
933 BeforeMemoryManager { device_ordinal: usize },
934 AfterMemoryManager { device_ordinal: usize },
935 BeforeStreamPool { device_ordinal: usize },
936 AfterStreamPool { device_ordinal: usize },
937 BeforeDeviceContext { device_ordinal: usize },
938 AfterDeviceContext { device_ordinal: usize },
939 BeforeWorkerPool { device_ordinal: usize },
940 AfterWorkerPool { device_ordinal: usize },
941 BeforeStartupRecords,
942 AfterStartupRecords,
943 BeforeCleanupWorker,
944 AfterCleanupWorker,
945 Published,
946}
947
948pub(crate) trait GpuBackendConstructionProbe: Send + Sync + fmt::Debug {
951 fn checkpoint(&self, checkpoint: GpuBackendConstructionCheckpoint) -> Result<()>;
952}
953
954#[derive(Debug)]
955pub(crate) struct NoopConstructionProbe;
956
957impl GpuBackendConstructionProbe for NoopConstructionProbe {
958 fn checkpoint(&self, _checkpoint: GpuBackendConstructionCheckpoint) -> Result<()> {
959 Ok(())
960 }
961}
962
963impl GpuBackendCore {
964 pub fn new(config: ValidatedGpuBackendConfig) -> Result<Self> {
978 Self::construct(
979 config,
980 &NoopConstructionProbe,
981 Arc::new(ConstructionResourceJournal::default()),
982 ObservationCompletionExecutor::spawn,
983 )
984 }
985
986 fn construct(
998 config: ValidatedGpuBackendConfig,
999 probe: &dyn GpuBackendConstructionProbe,
1000 journal: Arc<ConstructionResourceJournal>,
1001 spawn_completion_executor: impl FnOnce() -> Result<ObservationCompletionExecutor>,
1002 ) -> Result<Self> {
1003 let ordinals = config.device_ordinals();
1004 let generation_id = next_backend_identity();
1005 let backend_metrics = BackendMetricsStore::new(generation_id, &ordinals)?;
1006 let owner_lease = claim_live_backend(&ordinals)?;
1007
1008 let generation = Arc::new(ResourceGeneration::new(generation_id, owner_lease, Arc::clone(&journal)));
1013 let resource_core = Arc::new(ResourceCore {
1014 identity: next_backend_identity(),
1015 generation: Mutex::new(Some(Arc::clone(&generation))),
1016 });
1017 let observation_core = Arc::new(ObservationCore {
1018 identity: next_backend_identity(),
1019 lifecycle: BackendLifecycle::new(),
1020 attempt_counters: Arc::new(AttemptBackendCounters::default()),
1021 attempt_completion_queue: config
1022 .attempt_completion_queue_capacity
1023 .and_then(AttemptCompletionQueue::new)
1024 .map(Arc::new),
1025 backend_metrics: backend_metrics.clone(),
1026 completion_executor: spawn_completion_executor()?,
1027 });
1028
1029 probe.checkpoint(GpuBackendConstructionCheckpoint::BeforeCleanupWorker)?;
1030 let coordinator = CloseCoordinator::spawn(Arc::clone(&resource_core), Arc::clone(&observation_core))?;
1031 let mut rollback = ConstructionRollback::new(Arc::clone(&coordinator));
1032
1033 let build_result = (|| {
1034 probe.checkpoint(GpuBackendConstructionCheckpoint::AfterCleanupWorker)?;
1035 let token = generation.owner_lease.token();
1036 let service = QueryService::install_for_backend(&config, &token, probe, &journal, &backend_metrics)?;
1037 generation.service.set(service).map_err(|_service| {
1038 QueryError::new(
1039 QueryErrorCode::Internal,
1040 BUILD_OPERATION,
1041 "backend resource generation was initialized more than once",
1042 )
1043 .with_fact("reason", "duplicate_resource_generation_install")
1044 })?;
1045 observation_core.completion_executor.activate_process_exit();
1049 coordinator.activate_process_exit();
1050 let kvikio_process_config = configure_kvikio_process_defaults(&ordinals)?;
1053
1054 let service =
1055 generation.service().expect("resource service is available immediately after successful installation");
1056
1057 let devices = config.validated_devices();
1062 let observed = service.backend_device_startup_facts();
1063 let worker_pools = service.gpu_execution_pool_reports();
1064 if observed.len() != devices.len() || worker_pools.len() != devices.len() {
1065 return Err(QueryError::new(
1066 QueryErrorCode::Internal,
1067 BUILD_OPERATION,
1068 "constructed GPU resources do not cover every validated device profile",
1069 )
1070 .with_fact("reason", "incomplete_device_startup_records")
1071 .with_fact("profile_count", devices.len())
1072 .with_fact("device_record_count", observed.len())
1073 .with_fact("worker_pool_count", worker_pools.len()));
1074 }
1075 let mut startup_records = Vec::with_capacity(devices.len());
1076 for device in devices {
1077 let profile = &device.config;
1078 let (facts, capacity_bytes, installation, health_identity, resource_profile) = observed
1079 .iter()
1080 .find(|(facts, _, _, _, _)| facts.device == profile.device_ordinal())
1081 .cloned()
1082 .ok_or_else(|| {
1083 QueryError::new(
1084 QueryErrorCode::Internal,
1085 BUILD_OPERATION,
1086 "validated device has no immutable startup record",
1087 )
1088 .with_fact("reason", "device_startup_record_missing")
1089 .with_fact("device_ordinal", profile.device_ordinal())
1090 })?;
1091 let worker_pool = worker_pools
1092 .iter()
1093 .find(|pool| pool.device_ordinal == profile.device_ordinal())
1094 .ok_or_else(|| {
1095 QueryError::new(
1096 QueryErrorCode::Internal,
1097 BUILD_OPERATION,
1098 "validated device has no GPU worker pool",
1099 )
1100 .with_fact("reason", "device_worker_pool_missing")
1101 .with_fact("device_ordinal", profile.device_ordinal())
1102 })?;
1103 if worker_pool.worker_count != device.installation_bounds.worker_count {
1104 return Err(QueryError::new(
1105 QueryErrorCode::Internal,
1106 BUILD_OPERATION,
1107 "GPU worker pool does not match its validated device profile",
1108 )
1109 .with_fact("reason", "device_worker_pool_mismatch")
1110 .with_fact("device_ordinal", profile.device_ordinal())
1111 .with_fact("profile_worker_count", device.installation_bounds.worker_count)
1112 .with_fact("installed_worker_count", worker_pool.worker_count));
1113 }
1114 let allocation_limit_bytes = device.allocation_limit_bytes;
1115 let installed_allocation_limit =
1116 installation.effective_options.map(|options| options.allocation_limit_bytes);
1117 if installed_allocation_limit != Some(allocation_limit_bytes) {
1118 return Err(QueryError::new(
1119 QueryErrorCode::Internal,
1120 BUILD_OPERATION,
1121 "device memory manager record does not match its validated installation profile",
1122 )
1123 .with_fact("reason", "device_memory_manager_record_mismatch")
1124 .with_fact("device_ordinal", profile.device_ordinal())
1125 .with_fact("expected_allocation_limit_bytes", format!("{allocation_limit_bytes:?}"))
1126 .with_fact("installed_options", format!("{:?}", installation.effective_options)));
1127 }
1128 startup_records.push(GpuBackendDeviceStartupRecord {
1129 device_ordinal: facts.device,
1130 model_name: facts.model_name.clone(),
1131 uuid: facts.uuid.clone(),
1132 physical_total_bytes: facts.physical_total_bytes,
1133 facts,
1134 capacity_bytes,
1135 resource_profile,
1136 memory_ownership: profile.memory_ownership,
1137 memory_limit: profile.memory_limit(),
1138 backend_reserve_bytes: profile.backend_reserve_bytes(),
1139 max_active_attempts: profile.max_active_attempts(),
1140 stream_pool_size: device.installation_bounds.stream_pool_size,
1141 worker_count: worker_pool.worker_count,
1142 lane_count: device.installation_bounds.lane_count,
1143 install_telemetry: profile.install_telemetry(),
1144 memory_manager_installation: installation,
1145 device_health_generation: health_identity.device_health_generation,
1146 backend_health_generation: health_identity.backend_health_generation,
1147 });
1148 }
1149
1150 probe.checkpoint(GpuBackendConstructionCheckpoint::BeforeStartupRecords)?;
1151 let startup_records: Arc<[GpuBackendDeviceStartupRecord]> = startup_records.into();
1152 journal.record_startup_records(Arc::clone(&startup_records))?;
1153 generation.startup_records.set(Arc::clone(&startup_records)).map_err(|_records| {
1154 QueryError::new(
1155 QueryErrorCode::Internal,
1156 BUILD_OPERATION,
1157 "backend startup records were initialized more than once",
1158 )
1159 .with_fact("reason", "duplicate_startup_record_install")
1160 })?;
1161 probe.checkpoint(GpuBackendConstructionCheckpoint::AfterStartupRecords)?;
1162 let close_on_last = Arc::clone(&coordinator);
1166 let public_handles = PublicHandleTracker::new(move || {
1167 close_on_last.signal();
1168 });
1169 let public_handle = public_handles.acquire().expect("new public-handle tracker accepts its first lease");
1170 probe.checkpoint(GpuBackendConstructionCheckpoint::Published)?;
1171 Ok(GpuBackendCore {
1172 resource_core,
1173 observation_core,
1174 coordinator,
1175 public_handle,
1176 startup_records,
1177 kvikio_process_config,
1178 generation_id,
1179 })
1180 })();
1181 match build_result {
1182 Ok(core) => {
1183 rollback.disarm();
1184 Ok(core)
1185 },
1186 Err(error) => Err(rollback.attach_cleanup_facts(error)),
1187 }
1188 }
1189}
1190
1191fn configure_kvikio_process_defaults(ordinals: &[usize]) -> Result<Option<KvikioProcessConfig>> {
1195 let device_ordinal = *ordinals.first().expect("a validated backend config names at least one device");
1196 match configure_kvikio_process(device_ordinal, &KvikioProcessConfig::DEFAULT) {
1197 Ok(config) => Ok(Some(config)),
1198 Err(error) if error.code() == cudf::ErrorCode::UnsupportedOperation => Ok(None),
1199 Err(error) => Err(cudf_query_error(BUILD_OPERATION, "failed to configure KvikIO process defaults", error)
1200 .with_fact("device_ordinal", device_ordinal)),
1201 }
1202}
1203
1204#[derive(Debug, Clone, PartialEq, Eq)]
1211pub struct GpuBackendDeviceStartupRecord {
1212 pub device_ordinal: usize,
1214 pub model_name: Option<String>,
1216 pub uuid: Option<String>,
1218 pub physical_total_bytes: Option<u64>,
1221 pub facts: DeviceFacts,
1223 pub capacity_bytes: u64,
1226 pub resource_profile: DeviceResourceProfile,
1228 pub memory_ownership: GpuMemoryOwnership,
1230 pub memory_limit: GpuDeviceMemoryLimit,
1232 pub backend_reserve_bytes: u64,
1234 pub max_active_attempts: usize,
1236 pub stream_pool_size: usize,
1238 pub worker_count: usize,
1240 pub lane_count: usize,
1242 pub install_telemetry: bool,
1244 pub memory_manager_installation: DeviceMemoryManagerInstallationState,
1246 pub device_health_generation: u64,
1248 pub backend_health_generation: u64,
1250}
1251
1252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1257pub struct GpuBackendCoreIdentity {
1258 pub resource_core: u64,
1259 pub observation_core: u64,
1260 pub resource_generation: u64,
1261}
1262
1263#[derive(Debug)]
1271pub struct GpuBackendCore {
1272 resource_core: Arc<ResourceCore>,
1273 observation_core: Arc<ObservationCore>,
1274 coordinator: Arc<CloseCoordinator>,
1275 public_handle: PublicHandleLease,
1279 startup_records: Arc<[GpuBackendDeviceStartupRecord]>,
1280 kvikio_process_config: Option<KvikioProcessConfig>,
1281 generation_id: u64,
1282}
1283
1284#[cfg(feature = "qualification")]
1291#[derive(Clone, Debug)]
1292pub struct GpuBackendFaultInjector {
1293 session: GpuBackendSession,
1294}
1295
1296#[cfg(feature = "qualification")]
1297impl GpuBackendFaultInjector {
1298 pub fn quarantine_device(&self, device_ordinal: usize) -> Result<DeviceQuarantineInjection> {
1304 self.session.with_service(|service| service.inject_device_quarantine(device_ordinal))
1305 }
1306}
1307
1308impl Clone for GpuBackendCore {
1309 fn clone(&self) -> Self {
1310 Self {
1311 resource_core: Arc::clone(&self.resource_core),
1312 observation_core: Arc::clone(&self.observation_core),
1313 coordinator: Arc::clone(&self.coordinator),
1314 public_handle: self.public_handle.clone(),
1315 startup_records: Arc::clone(&self.startup_records),
1316 kvikio_process_config: self.kvikio_process_config,
1317 generation_id: self.generation_id,
1318 }
1319 }
1320}
1321
1322impl GpuBackendCore {
1323 pub fn session_capability(&self) -> GpuBackendSession {
1331 GpuBackendSession {
1332 resource_core: Arc::downgrade(&self.resource_core),
1333 observation_core: Arc::clone(&self.observation_core),
1334 identities: self.identity(),
1335 }
1336 }
1337
1338 #[cfg(feature = "qualification")]
1341 pub fn fault_injector(&self) -> GpuBackendFaultInjector {
1342 GpuBackendFaultInjector { session: self.session_capability() }
1343 }
1344
1345 pub fn device_startup_records(&self) -> &[GpuBackendDeviceStartupRecord] {
1346 &self.startup_records
1347 }
1348
1349 pub fn kvikio_process_config(&self) -> Option<KvikioProcessConfig> {
1352 self.kvikio_process_config
1353 }
1354
1355 pub fn identity(&self) -> GpuBackendCoreIdentity {
1356 GpuBackendCoreIdentity {
1357 resource_core: self.resource_core.identity,
1358 observation_core: self.observation_core.identity,
1359 resource_generation: self.generation_id,
1360 }
1361 }
1362
1363 pub fn begin_attempt(&self, request: BeginAttemptRequest) -> Result<AttemptBegin> {
1365 self.session_capability().begin_attempt(request)
1366 }
1367
1368 pub fn backend_metrics_snapshot(&self) -> BackendMetricsSnapshot {
1373 self.observation_core.backend_metrics.snapshot()
1374 }
1375
1376 pub fn close(&self) -> bool {
1385 self.coordinator.signal()
1386 }
1387
1388 pub fn close_snapshot(&self) -> GpuBackendCloseSnapshot {
1391 self.coordinator.state.snapshot(&self.observation_core.lifecycle)
1392 }
1393
1394 pub fn close_and_wait(&self, timeout: Duration) -> Result<GpuBackendCloseSnapshot> {
1407 reject_reentrant_observation_close()?;
1408 let deadline = Instant::now().checked_add(timeout).unwrap_or_else(Instant::now);
1409 self.close_and_wait_until(deadline)
1410 }
1411
1412 pub fn close_and_wait_until(&self, deadline: Instant) -> Result<GpuBackendCloseSnapshot> {
1415 reject_reentrant_observation_close()?;
1416 self.coordinator.close_and_wait_until(deadline)
1417 }
1418
1419 pub async fn close_and_wait_async(&self, timeout: Duration) -> Result<GpuBackendCloseSnapshot> {
1423 reject_reentrant_observation_close()?;
1424 let coordinator = Arc::clone(&self.coordinator);
1425 spawn_blocking(move || {
1426 let deadline = Instant::now().checked_add(timeout).unwrap_or_else(Instant::now);
1427 coordinator.close_and_wait_until(deadline)
1428 })
1429 .await
1430 .map_err(|error| {
1431 QueryError::new(QueryErrorCode::Internal, CLOSE_OPERATION, "backend close wait task failed")
1432 .with_fact("reason", "close_wait_task_failed")
1433 .with_fact("join_error", error.to_string())
1434 })?
1435 }
1436}
1437
1438#[derive(Clone, Debug)]
1443pub struct GpuBackendSession {
1444 resource_core: Weak<ResourceCore>,
1445 observation_core: Arc<ObservationCore>,
1446 identities: GpuBackendCoreIdentity,
1447}
1448
1449impl GpuBackendSession {
1450 pub fn identity(&self) -> GpuBackendCoreIdentity {
1451 self.identities
1452 }
1453
1454 pub fn is_closing(&self) -> bool {
1460 self.observation_core.lifecycle.is_closing()
1461 }
1462
1463 pub fn begin_attempt(&self, request: BeginAttemptRequest) -> Result<AttemptBegin> {
1472 let permit = self.observation_core.lifecycle.try_begin_observation()?;
1473 self.observation_core.attempt_counters.attempts_begun.fetch_add(1, Ordering::AcqRel);
1474 let begin = AttemptOwner::begin(
1475 request,
1476 permit,
1477 Arc::clone(&self.observation_core.attempt_counters),
1478 self.observation_core.attempt_completion_queue.clone(),
1479 );
1480 register_backend_attempt_metrics_sink(&begin.owner, self.observation_core.backend_metrics.clone());
1481 Ok(begin)
1482 }
1483
1484 pub fn backend_metrics_snapshot(&self) -> BackendMetricsSnapshot {
1487 self.observation_core.backend_metrics.snapshot()
1488 }
1489
1490 pub fn take_abandoned_attempt_reports(&self, max: usize) -> Vec<Arc<QueryAttemptReport>> {
1494 self.observation_core.attempt_completion_queue.as_ref().map_or_else(Vec::new, |queue| queue.drain(max))
1495 }
1496
1497 pub fn attempt_counters_snapshot(&self) -> AttemptBackendCountersSnapshot {
1498 self.observation_core.attempt_counters.snapshot()
1499 }
1500
1501 fn acquire_resource(&self) -> Result<(ObservationPermit, GpuResourcePermit, ObservationCompletionDispatcher)> {
1514 let observation = self.observation_core.lifecycle.try_begin_observation()?;
1515 let resource_core = self.resource_core.upgrade().ok_or_else(backend_closing_resource_error)?;
1516 let generation = resource_core.generation.lock().map_err(|_error| resource_core_poisoned_error())?;
1517 let lifecycle = self.observation_core.lifecycle.try_begin_gpu_resource()?;
1518 let generation = generation.as_ref().cloned().ok_or_else(backend_closing_resource_error)?;
1519 Ok((
1520 observation,
1521 GpuResourcePermit { generation, _lifecycle: lifecycle },
1522 self.observation_core.completion_executor.dispatcher(),
1523 ))
1524 }
1525
1526 #[cfg(any(test, feature = "test-hooks"))]
1530 #[doc(hidden)]
1531 pub fn admit_query_with_request(&self, request: QueryAdmissionRequest) -> Result<QueryHandle> {
1532 let (observation, resource, completion_dispatcher) = self.acquire_resource()?;
1533 let service = resource.generation.service().cloned().ok_or_else(resource_generation_unpublished_error)?;
1534 service.admit_query_with_backend_permits(request, observation, resource, completion_dispatcher)
1535 }
1536
1537 #[cfg(any(test, feature = "test-hooks"))]
1539 #[doc(hidden)]
1540 pub fn admit_query(&self) -> Result<QueryHandle> {
1541 self.admit_query_with_request(QueryAdmissionRequest::test_attempt())
1542 }
1543
1544 #[cfg(any(test, feature = "test-hooks"))]
1547 #[doc(hidden)]
1548 pub fn try_admit_query_with_request(&self, request: QueryAdmissionRequest) -> Result<Option<QueryHandle>> {
1549 let (observation, resource, completion_dispatcher) = self.acquire_resource()?;
1550 let service = resource.generation.service().cloned().ok_or_else(resource_generation_unpublished_error)?;
1551 service
1552 .enqueue_admission_with_backend_permits(request, observation, resource, completion_dispatcher)?
1553 .try_admit()
1554 }
1555
1556 pub async fn admit_async_with_request(&self, request: QueryAdmissionRequest) -> Result<QueryHandle> {
1573 let (observation, resource, completion_dispatcher) = self.acquire_resource()?;
1574 let service = resource.generation.service().cloned().ok_or_else(resource_generation_unpublished_error)?;
1575 service.admit_async_with_backend_permits(request, observation, resource, completion_dispatcher).await
1576 }
1577
1578 pub fn same_backend(&self, other: &Self) -> bool {
1580 self.identities == other.identities
1581 }
1582
1583 pub fn diagnostics(&self) -> Result<BackendDiagnostics> {
1586 self.with_service(QueryService::diagnostics)
1587 }
1588
1589 pub fn device_telemetry_snapshots(&self) -> Result<Vec<DeviceTelemetrySnapshot>> {
1590 self.with_service(|service| Ok(service.device_telemetry_snapshots()))
1591 }
1592
1593 fn with_service<T>(&self, operation: impl FnOnce(&QueryService) -> Result<T>) -> Result<T> {
1596 let (_observation, resource, _completion_dispatcher) = self.acquire_resource()?;
1597 let service = resource.generation.service().ok_or_else(resource_generation_unpublished_error)?;
1598 operation(service)
1599 }
1600}
1601
1602fn register_backend_attempt_metrics_sink(owner: &AttemptOwner, backend_metrics: BackendMetricsStore) {
1606 owner.register_sealed_report_sink(move |report| {
1607 let projection = report.sealed_projection();
1608 backend_metrics.record_sealed_attempt(
1609 &projection,
1610 BackendAttemptDurations {
1611 total_ns: report.total_ns(),
1612 planning_ns: report.planning_ns(),
1613 queue_wait_ns: report.queue_wait_ns(),
1614 physical_execution_ns: report.physical_execution_ns(),
1615 },
1616 );
1617 });
1618}
1619
1620pub(crate) struct GpuResourcePermit {
1623 generation: Arc<ResourceGeneration>,
1624 _lifecycle: GpuLifecyclePermit,
1630}
1631
1632impl fmt::Debug for GpuResourcePermit {
1633 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1634 formatter
1635 .debug_struct("GpuResourcePermit")
1636 .field("resource_generation", &self.generation.generation_id)
1637 .finish_non_exhaustive()
1638 }
1639}
1640
1641#[derive(Debug)]
1650pub(crate) struct ResourceCore {
1651 identity: u64,
1652 pub(crate) generation: Mutex<Option<Arc<ResourceGeneration>>>,
1653}
1654
1655#[derive(Debug)]
1663pub(crate) struct ObservationCore {
1664 identity: u64,
1665 pub(crate) lifecycle: BackendLifecycle,
1666 attempt_counters: Arc<AttemptBackendCounters>,
1667 attempt_completion_queue: Option<Arc<AttemptCompletionQueue>>,
1668 backend_metrics: BackendMetricsStore,
1669 completion_executor: ObservationCompletionExecutor,
1670}
1671
1672#[derive(Debug)]
1683pub(crate) struct ResourceGeneration {
1684 service: OnceLock<QueryService>,
1687 startup_records: OnceLock<Arc<[GpuBackendDeviceStartupRecord]>>,
1688 generation_id: u64,
1689 construction_journal: Arc<ConstructionResourceJournal>,
1692 owner_lease: LiveBackendLease,
1694}
1695
1696impl ResourceGeneration {
1697 fn new(
1698 generation_id: u64,
1699 owner_lease: LiveBackendLease,
1700 construction_journal: Arc<ConstructionResourceJournal>,
1701 ) -> Self {
1702 Self {
1703 service: OnceLock::new(),
1704 startup_records: OnceLock::new(),
1705 generation_id,
1706 construction_journal,
1707 owner_lease,
1708 }
1709 }
1710
1711 pub(crate) fn service(&self) -> Option<&QueryService> {
1712 self.service.get()
1713 }
1714
1715 pub(crate) fn construction_journal(&self) -> &ConstructionResourceJournal {
1716 &self.construction_journal
1717 }
1718}
1719
1720#[derive(Debug)]
1721enum ConstructionResource {
1722 MemoryManager(DeviceMemoryManager),
1723 StreamPool(super::ServiceStreamPool),
1724 DeviceContext(super::DeviceContext),
1725 ExecutionPool(super::GpuExecutionPool),
1726 StartupRecords(Arc<[GpuBackendDeviceStartupRecord]>),
1727}
1728
1729#[derive(Debug, Default)]
1740pub(crate) struct ConstructionResourceJournal {
1741 entries: Mutex<Vec<ConstructionResource>>,
1742}
1743
1744impl ConstructionResourceJournal {
1745 pub(crate) fn record_memory_manager(&self, manager: DeviceMemoryManager) -> Result<()> {
1746 self.record(ConstructionResource::MemoryManager(manager))
1747 }
1748
1749 pub(crate) fn record_stream_pool(&self, pool: super::ServiceStreamPool) -> Result<()> {
1750 self.record(ConstructionResource::StreamPool(pool))
1751 }
1752
1753 pub(crate) fn record_device_context(&self, context: super::DeviceContext) -> Result<()> {
1754 self.record(ConstructionResource::DeviceContext(context))
1755 }
1756
1757 pub(crate) fn record_execution_pool(&self, pool: super::GpuExecutionPool) -> Result<()> {
1758 self.record(ConstructionResource::ExecutionPool(pool))
1759 }
1760
1761 fn record_startup_records(&self, records: Arc<[GpuBackendDeviceStartupRecord]>) -> Result<()> {
1762 self.record(ConstructionResource::StartupRecords(records))
1763 }
1764
1765 fn record(&self, resource: ConstructionResource) -> Result<()> {
1766 self.entries.lock().map_err(|_| construction_journal_poisoned())?.push(resource);
1767 Ok(())
1768 }
1769
1770 pub(crate) fn close_and_drain_until(&self, deadline: Instant) -> Result<()> {
1781 let entries = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
1784 let mut first_deadline = None;
1785 let mut first_permanent = None;
1786 for entry in entries.iter().rev() {
1787 let result = match entry {
1788 ConstructionResource::ExecutionPool(pool) => pool.close_and_wait_until(deadline),
1789 ConstructionResource::StreamPool(pool) => {
1790 pool.close_and_wait_until(deadline, "query_engine.gpu_backend_core.rollback")
1791 },
1792 ConstructionResource::MemoryManager(manager) => {
1797 let _ = manager.device();
1798 Ok(())
1799 },
1800 ConstructionResource::DeviceContext(context) => {
1801 let _ = context.device();
1802 Ok(())
1803 },
1804 ConstructionResource::StartupRecords(records) => {
1805 let _ = records.len();
1806 Ok(())
1807 },
1808 };
1809 if let Err(error) = result {
1810 if error.is_cleanup_deadline_elapsed() {
1811 if first_deadline.is_none() {
1812 first_deadline = Some(error);
1813 }
1814 } else if first_permanent.is_none() {
1815 first_permanent = Some(error);
1816 }
1817 }
1818 }
1819 if let Some(error) = first_permanent.or(first_deadline) {
1820 return Err(error);
1821 }
1822 Ok(())
1823 }
1824}
1825
1826fn construction_journal_poisoned() -> QueryError {
1827 QueryError::new(QueryErrorCode::Internal, BUILD_OPERATION, "construction resource journal mutex was poisoned")
1828 .with_fact("reason", "construction_journal_poisoned")
1829}
1830
1831fn backend_closing_resource_error() -> QueryError {
1832 QueryError::new(
1833 QueryErrorCode::Cancelled,
1834 RESOURCE_OPERATION,
1835 "backend is closing and has no live resource generation",
1836 )
1837 .with_fact("reason", "backend_closing")
1838}
1839
1840fn resource_core_poisoned_error() -> QueryError {
1841 QueryError::new(QueryErrorCode::Internal, RESOURCE_OPERATION, "backend resource-core mutex was poisoned")
1842 .with_fact("reason", "resource_core_poisoned")
1843}
1844
1845fn resource_generation_unpublished_error() -> QueryError {
1846 QueryError::new(QueryErrorCode::Internal, RESOURCE_OPERATION, "backend resource generation is not published")
1847 .with_fact("reason", "resource_generation_unpublished")
1848}
1849
1850fn next_backend_identity() -> u64 {
1853 static NEXT: AtomicU64 = AtomicU64::new(1);
1854 NEXT.fetch_add(1, Ordering::Relaxed)
1855}
1856
1857#[cfg(test)]
1858mod tests {
1859 use std::{
1860 sync::{Condvar, atomic::AtomicU8, mpsc},
1861 thread,
1862 };
1863
1864 use tokio::runtime::Builder as TokioRuntimeBuilder;
1865
1866 use super::*;
1867 use crate::{
1868 observability::{RuntimeObservationConfig, RuntimeObservationOwner, RuntimeTerminalOutcome},
1869 runtime::{
1870 BACKEND_METRIC_DEVICE_CAPACITY, GpuExecutionJobKind, GpuExecutionPool, QueryActivityLease,
1871 SourceSchedulerPolicy, observation_completion::ObservationCompletionWorkerLane,
1872 },
1873 };
1874
1875 #[derive(Debug)]
1876 struct ActivityPermitOrderingProbe {
1877 lifecycle: BackendLifecycle,
1878 backend: GpuBackendCore,
1879 observed_active_gpu_permits: Arc<std::sync::atomic::AtomicUsize>,
1880 completion: mpsc::SyncSender<std::result::Result<(), String>>,
1881 }
1882
1883 impl Drop for ActivityPermitOrderingProbe {
1884 fn drop(&mut self) {
1885 self.observed_active_gpu_permits.store(self.lifecycle.snapshot().active_gpu_permits, Ordering::Release);
1886 let result = (|| {
1887 let close = self.backend.close_and_wait(Duration::from_secs(10)).map_err(|error| error.to_string())?;
1888 if !close.gpu_resource_quiescent || !close.observation_quiescent {
1889 return Err(format!("backend close was not quiescent: {close:?}"));
1890 }
1891 let replacement =
1892 GpuBackendCore::new(one_device_backend_config(0)).map_err(|error| error.to_string())?;
1893 let replacement_close =
1894 replacement.close_and_wait(Duration::from_secs(10)).map_err(|error| error.to_string())?;
1895 if !replacement_close.gpu_resource_quiescent || !replacement_close.observation_quiescent {
1896 return Err(format!("replacement backend close was not quiescent: {replacement_close:?}"));
1897 }
1898 Ok(())
1899 })();
1900 let _ = self.completion.send(result);
1901 }
1902 }
1903
1904 #[derive(Debug)]
1905 struct ReentrantCloseActivityProbe {
1906 backend: GpuBackendCore,
1907 entered: mpsc::SyncSender<()>,
1908 completion: mpsc::SyncSender<std::result::Result<GpuBackendCloseSnapshot, String>>,
1909 }
1910
1911 impl Drop for ReentrantCloseActivityProbe {
1912 fn drop(&mut self) {
1913 let _ = self.entered.send(());
1914 let result = self.backend.close_and_wait(Duration::from_secs(10)).map_err(|error| error.to_string());
1915 let _ = self.completion.send(result);
1916 }
1917 }
1918
1919 fn live_backend_test_lock() -> std::sync::MutexGuard<'static, ()> {
1920 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1921 LOCK.get_or_init(|| Mutex::new(())).lock().unwrap_or_else(std::sync::PoisonError::into_inner)
1922 }
1923
1924 fn inert_backend_for_observation_delivery_test() -> GpuBackendCore {
1925 let resource_core = Arc::new(ResourceCore { identity: next_backend_identity(), generation: Mutex::new(None) });
1926 let generation_id = next_backend_identity();
1927 let observation_core = Arc::new(ObservationCore {
1928 identity: next_backend_identity(),
1929 lifecycle: BackendLifecycle::new(),
1930 attempt_counters: Arc::new(AttemptBackendCounters::default()),
1931 attempt_completion_queue: None,
1932 backend_metrics: BackendMetricsStore::new(generation_id, &[])
1933 .expect("test backend metrics store accepts the inert generation"),
1934 completion_executor: ObservationCompletionExecutor::spawn().expect("test creates observation executor"),
1935 });
1936 let coordinator = CloseCoordinator::spawn(Arc::clone(&resource_core), Arc::clone(&observation_core))
1937 .expect("test creates close coordinator");
1938 let close_on_last = Arc::clone(&coordinator);
1939 let public_handles = PublicHandleTracker::new(move || {
1940 close_on_last.signal();
1941 });
1942 let public_handle = public_handles.acquire().expect("test creates public handle");
1943 GpuBackendCore {
1944 resource_core,
1945 observation_core,
1946 coordinator,
1947 public_handle,
1948 startup_records: Arc::from([]),
1949 kvikio_process_config: None,
1950 generation_id,
1951 }
1952 }
1953
1954 #[test]
1955 fn close_recovers_a_poisoned_generation_slot() {
1956 let backend = inert_backend_for_observation_delivery_test();
1957 let resource_core = Arc::clone(&backend.resource_core);
1958 thread::spawn(move || {
1959 let _generation = resource_core.generation.lock().unwrap();
1960 panic!("poison the resource generation slot");
1961 })
1962 .join()
1963 .expect_err("the poisoning thread panics while holding the generation slot");
1964
1965 let close = backend.close_and_wait(Duration::from_secs(2)).expect("close recovers the generation slot");
1966
1967 assert!(close.gpu_resource_quiescent);
1968 assert!(close.observation_quiescent);
1969 }
1970
1971 #[test]
1972 fn close_and_wait_reclaims_the_backend_cleanup_worker() {
1973 let backend = inert_backend_for_observation_delivery_test();
1974 let coordinator = Arc::clone(&backend.coordinator);
1975
1976 let close = backend.close_and_wait(Duration::from_secs(2)).expect("inert backend closes");
1977
1978 assert!(close.gpu_resource_quiescent);
1979 assert!(close.observation_quiescent);
1980 assert!(coordinator.worker_join_is_reclaimed_for_test(), "close must reclaim its cleanup worker handle");
1981 }
1982
1983 #[test]
1984 fn engine_begin_records_one_sealed_projection_despite_a_panicking_peer_sink() {
1985 use crate::runtime::backend_metrics::{BackendAttemptDisposition, BackendAttemptOutcome};
1986
1987 let backend = inert_backend_for_observation_delivery_test();
1988 let begin = backend.begin_attempt(BeginAttemptRequest::default()).expect("inert backend begins an attempt");
1989 begin.owner.register_sealed_report_sink(|_| panic!("injected peer sink panic"));
1990
1991 begin.owner.commit_terminal(RuntimeTerminalOutcome::Succeeded).unwrap();
1992 let first = backend.backend_metrics_snapshot();
1993 assert_eq!(first.attempts.completed(BackendAttemptDisposition::Unplanned, BackendAttemptOutcome::Succeeded), 1,);
1994
1995 drop(begin);
1998 let second = backend.backend_metrics_snapshot();
1999 assert_eq!(second.generation, first.generation);
2000 assert_eq!(
2001 second.attempts.completed(BackendAttemptDisposition::Unplanned, BackendAttemptOutcome::Succeeded),
2002 1,
2003 );
2004
2005 let close = backend.close_and_wait(Duration::from_secs(2)).expect("inert backend closes");
2006 assert!(close.observation_quiescent);
2007 }
2008
2009 #[test]
2010 fn process_exit_with_live_admitted_handle_does_not_hang() {
2011 const CHILD_ENV: &str = "ALGEON_TEST_PROCESS_EXIT_WITH_LIVE_ADMISSION";
2012 const TEST_NAME: &str = "runtime::backend_core::tests::process_exit_with_live_admitted_handle_does_not_hang";
2013
2014 if std::env::var_os(CHILD_ENV).is_some() {
2015 let backend = GpuBackendCore::new(one_device_backend_config(0)).expect("child process builds a backend");
2016 let session = backend.session_capability();
2017 let _live_handle = session.admit_query().expect("child process retains one admitted handle");
2018 std::process::exit(0);
2022 }
2023
2024 let executable = std::env::current_exe().expect("current test executable is available");
2025 let mut child = std::process::Command::new(executable)
2026 .arg(TEST_NAME)
2027 .arg("--exact")
2028 .arg("--nocapture")
2029 .env(CHILD_ENV, "1")
2030 .spawn()
2031 .expect("spawn process-exit regression child");
2032 let deadline = Instant::now() + Duration::from_secs(15);
2033 loop {
2034 if let Some(status) = child.try_wait().expect("poll process-exit regression child") {
2035 assert!(status.success(), "process-exit regression child failed with {status}");
2036 break;
2037 }
2038 if Instant::now() >= deadline {
2039 let _ = child.kill();
2040 let _ = child.wait();
2041 panic!("process exit hung while an admitted handle remained live");
2042 }
2043 thread::sleep(Duration::from_millis(10));
2044 }
2045 }
2046
2047 #[test]
2048 fn observation_delivery_rejects_all_blocking_close_apis_without_signaling_close() {
2049 let backend = inert_backend_for_observation_delivery_test();
2050 let (sender, receiver) = mpsc::sync_channel(1);
2051 let observer_backend = backend.clone();
2052 let owner = RuntimeObservationOwner::new(
2053 "reentrant-observation-close",
2054 None,
2055 RuntimeObservationConfig::bounded(8, 4 * 1024),
2056 );
2057 owner.register_seal_observer(move |_| {
2058 let sync = observer_backend.close_and_wait(Duration::from_secs(1));
2059 let until = observer_backend.close_and_wait_until(Instant::now() + Duration::from_secs(1));
2060 let asynchronous = TokioRuntimeBuilder::new_current_thread()
2061 .enable_all()
2062 .build()
2063 .expect("test builds current-thread runtime")
2064 .block_on(observer_backend.close_and_wait_async(Duration::from_secs(1)));
2065 sender.send((sync, until, asynchronous)).expect("test receives close rejections");
2066 });
2067
2068 owner.finish_query(Some(RuntimeTerminalOutcome::Succeeded)).expect("test seals observation owner");
2069 let (sync, until, asynchronous) = receiver.recv_timeout(Duration::from_secs(2)).expect("observer delivered");
2070 for error in [sync.unwrap_err(), until.unwrap_err(), asynchronous.unwrap_err()] {
2071 assert_eq!(error.code(), QueryErrorCode::Internal);
2072 assert_eq!(error.fact_value("reason"), Some("reentrant_observation_close"));
2073 }
2074 assert!(!backend.close_snapshot().closing, "reentrant blocking close must not signal close");
2075 let close = backend.close_and_wait(Duration::from_secs(2)).expect("non-reentrant close succeeds");
2076 assert!(close.gpu_resource_quiescent);
2077 assert!(close.observation_quiescent);
2078 }
2079
2080 fn journal_resource_labels(journal: &ConstructionResourceJournal) -> Result<Vec<&'static str>> {
2081 let entries = journal.entries.lock().map_err(|_| construction_journal_poisoned())?;
2082 Ok(entries
2083 .iter()
2084 .map(|entry| match entry {
2085 ConstructionResource::MemoryManager(_) => "memory_manager",
2086 ConstructionResource::StreamPool(_) => "stream_pool",
2087 ConstructionResource::DeviceContext(_) => "device_context",
2088 ConstructionResource::ExecutionPool(_) => "execution_pool",
2089 ConstructionResource::StartupRecords(_) => "startup_records",
2090 })
2091 .collect())
2092 }
2093
2094 #[derive(Debug)]
2095 struct FailingProbe(GpuBackendConstructionCheckpoint);
2096 impl GpuBackendConstructionProbe for FailingProbe {
2097 fn checkpoint(&self, checkpoint: GpuBackendConstructionCheckpoint) -> Result<()> {
2098 if checkpoint == self.0 {
2099 return Err(QueryError::new(
2100 QueryErrorCode::Internal,
2101 BUILD_OPERATION,
2102 "injected construction failure",
2103 )
2104 .with_fact("reason", "injected_construction_failure"));
2105 }
2106 Ok(())
2107 }
2108 }
2109
2110 #[derive(Debug, Default)]
2111 struct PublishedCheckpointProbe(AtomicU8);
2112
2113 impl GpuBackendConstructionProbe for PublishedCheckpointProbe {
2114 fn checkpoint(&self, checkpoint: GpuBackendConstructionCheckpoint) -> Result<()> {
2115 if matches!(checkpoint, GpuBackendConstructionCheckpoint::Published) {
2116 self.0.store(1, Ordering::Release);
2117 }
2118 Ok(())
2119 }
2120 }
2121
2122 #[derive(Debug)]
2123 struct JournalBoundaryProbe {
2124 checkpoint: GpuBackendConstructionCheckpoint,
2125 journal: Weak<ConstructionResourceJournal>,
2126 expected_resources: &'static [&'static str],
2127 inspected: AtomicU8,
2128 }
2129
2130 impl GpuBackendConstructionProbe for JournalBoundaryProbe {
2131 fn checkpoint(&self, checkpoint: GpuBackendConstructionCheckpoint) -> Result<()> {
2132 if checkpoint != self.checkpoint {
2133 return Ok(());
2134 }
2135 let journal = self.journal.upgrade().ok_or_else(|| {
2136 QueryError::new(
2137 QueryErrorCode::Internal,
2138 BUILD_OPERATION,
2139 "construction journal was dropped before probe",
2140 )
2141 .with_fact("reason", "construction_journal_dropped_before_probe")
2142 })?;
2143 let observed = journal_resource_labels(&journal)?;
2144 if observed != self.expected_resources {
2145 return Err(QueryError::new(
2146 QueryErrorCode::Internal,
2147 BUILD_OPERATION,
2148 "construction resource was not journaled before its injected boundary",
2149 )
2150 .with_fact("reason", "construction_journal_boundary_mismatch")
2151 .with_fact("checkpoint", format!("{checkpoint:?}"))
2152 .with_fact("expected_resources", self.expected_resources.join(","))
2153 .with_fact("observed_resources", observed.join(",")));
2154 }
2155 self.inspected.store(1, Ordering::Release);
2156 Err(QueryError::new(QueryErrorCode::Internal, BUILD_OPERATION, "injected construction failure")
2157 .with_fact("reason", "injected_construction_failure"))
2158 }
2159 }
2160
2161 #[derive(Debug, Default)]
2162 struct BlockingGate {
2163 released: Mutex<bool>,
2164 changed: Condvar,
2165 }
2166
2167 impl BlockingGate {
2168 fn wait(&self) {
2169 let mut released = self.released.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
2170 while !*released {
2171 released = self.changed.wait(released).unwrap_or_else(std::sync::PoisonError::into_inner);
2172 }
2173 }
2174
2175 fn release(&self) {
2176 *self.released.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = true;
2177 self.changed.notify_all();
2178 }
2179 }
2180
2181 fn backend_config(devices: Vec<GpuBackendDeviceConfig>) -> GpuBackendConfig {
2182 GpuBackendConfig::new(devices, QueryExecutionConfig::default(), QueueBounds::default())
2183 }
2184
2185 fn one_device_backend(device_ordinal: usize) -> GpuBackendConfig {
2186 backend_config(vec![GpuBackendDeviceConfig::new(
2187 GpuBackendDeviceProfile::new(device_ordinal),
2188 GpuMemoryOwnership::WholeDeviceExclusive,
2189 )])
2190 }
2191
2192 fn one_device_backend_config(device_ordinal: usize) -> ValidatedGpuBackendConfig {
2193 one_device_backend(device_ordinal).validate().expect("one-device backend config validates")
2194 }
2195
2196 fn wait_for_live_owner_release(device_ordinal: usize) {
2197 let deadline = Instant::now() + Duration::from_secs(5);
2198 loop {
2199 match claim_live_backend(&[device_ordinal]) {
2200 Ok(lease) => {
2201 drop(lease);
2202 return;
2203 },
2204 Err(error)
2205 if error.fact_value("reason") == Some("backend_already_live") && Instant::now() < deadline =>
2206 {
2207 thread::sleep(Duration::from_millis(5));
2208 },
2209 Err(error) => panic!("owner claim was not released by construction cleanup: {error}"),
2210 }
2211 }
2212 }
2213
2214 #[test]
2215 fn pure_config_rejects_invalid_device_profiles_before_construction() {
2216 let config = backend_config(vec![GpuBackendDeviceConfig::new(
2217 GpuBackendDeviceProfile::new(0).with_max_active_attempts(0),
2218 GpuMemoryOwnership::WholeDeviceExclusive,
2219 )]);
2220 let error = config.validate().unwrap_err();
2221 assert_eq!(error.code(), QueryErrorCode::InvalidConfiguration);
2222 assert_eq!(error.message(), "device 0 max_active_attempts must be greater than zero");
2223 assert_eq!(error.fact_value("field"), Some("max_active_attempts"));
2224 assert_eq!(error.fact_value("reason"), Some("zero"));
2225 }
2226
2227 #[test]
2228 fn pure_config_rejects_explicit_zero_default_query_limit_before_construction() {
2229 let config = backend_config(vec![GpuBackendDeviceConfig::new(
2230 GpuBackendDeviceProfile::new(0).with_default_query_limit_bytes(0),
2231 GpuMemoryOwnership::WholeDeviceExclusive,
2232 )]);
2233
2234 let error = config.validate().unwrap_err();
2235
2236 assert_eq!(error.fact_value("field"), Some("default_query_limit_bytes"));
2237 assert_eq!(error.fact_value("reason"), Some("zero"));
2238 }
2239
2240 #[test]
2241 fn observation_completion_executor_spawn_failure_never_publishes_or_retains_live_owner() {
2242 let _owner_test = live_backend_test_lock();
2243 let device_ordinal = BACKEND_METRIC_DEVICE_CAPACITY - 3;
2246 for failing_lane in
2247 [ObservationCompletionWorkerLane::Observation, ObservationCompletionWorkerLane::ActivityTail]
2248 {
2249 let published = Arc::new(PublishedCheckpointProbe::default());
2250
2251 let error = GpuBackendCore::construct(
2252 one_device_backend_config(device_ordinal),
2253 published.as_ref(),
2254 Arc::default(),
2255 || ObservationCompletionExecutor::spawn_with_lane_failure_for_test(failing_lane),
2256 )
2257 .unwrap_err();
2258
2259 assert_eq!(error.fact_value("reason"), Some("observation_completion_worker_spawn_failed"));
2260 assert_eq!(error.fact_value("worker_lane"), Some(failing_lane.as_str()));
2261 assert_eq!(published.0.load(Ordering::Acquire), 0, "executor spawn failure must precede publication");
2262 wait_for_live_owner_release(device_ordinal);
2263 }
2264 }
2265
2266 #[test]
2267 fn pure_config_rejects_a_reserve_that_exhausts_the_fallback_capacity() {
2268 let config = backend_config(vec![GpuBackendDeviceConfig::new(
2269 GpuBackendDeviceProfile::new(0).with_backend_reserve_bytes(256 * 1024 * 1024),
2270 GpuMemoryOwnership::WholeDeviceExclusive,
2271 )]);
2272
2273 let error = config.validate().unwrap_err();
2274
2275 assert_eq!(error.code(), QueryErrorCode::InvalidConfiguration);
2276 assert_eq!(error.fact_value("reason"), Some("reserve_exhausts_fallback"));
2277 assert_eq!(
2278 error.message(),
2279 "device 0 backend_reserve_bytes is 268435456 bytes (0.25 GiB), at or above the automatic fallback capacity of 268435456 bytes (0.25 GiB); the largest accepted value is 268435455 bytes (0.24 GiB)"
2280 );
2281 }
2282
2283 #[test]
2284 fn pure_config_rejects_an_explicit_limit_above_the_static_partition_with_the_largest_accepted_value() {
2285 const GIB: u64 = 1 << 30;
2286 let config = backend_config(vec![GpuBackendDeviceConfig::new(
2287 GpuBackendDeviceProfile::new(0)
2288 .with_memory_limit(GpuDeviceMemoryLimit::Explicit { limit_bytes: 100 * GIB }),
2289 GpuMemoryOwnership::StaticPartition { allocation_limit_bytes: 95 * GIB },
2290 )]);
2291
2292 let error = config.validate().unwrap_err();
2293
2294 assert_eq!(error.code(), QueryErrorCode::InvalidConfiguration);
2295 assert_eq!(error.fact_value("reason"), Some("limit_exceeds_static_partition"));
2296 assert_eq!(
2297 error.message(),
2298 "device 0 memory_limit_bytes is 107374182400 bytes (100.00 GiB), above the static partition of 102005473280 bytes (95.00 GiB); the largest accepted value is 102005473280 bytes (95.00 GiB)"
2299 );
2300 }
2301
2302 #[test]
2303 fn automatic_installation_bounds_cover_every_active_attempt() {
2304 let config = backend_config(vec![GpuBackendDeviceConfig::new(
2305 GpuBackendDeviceProfile::new(0).with_max_active_attempts(2),
2306 GpuMemoryOwnership::WholeDeviceExclusive,
2307 )])
2308 .validate()
2309 .expect("derived multi-attempt installation profile validates");
2310
2311 let bounds = config.validated_devices()[0].installation_bounds;
2312 assert_eq!(bounds.lane_count, 2);
2313 assert_eq!(bounds.worker_count, 3);
2314 assert!(bounds.stream_pool_size >= 2);
2315 }
2316
2317 #[test]
2318 fn automatic_bounds_keep_device_source_work_independent_of_execution_prefetch() {
2319 let execution = QueryExecutionConfig {
2320 source_scheduler_policy: SourceSchedulerPolicy::new(7),
2321 ..QueryExecutionConfig::default()
2322 };
2323 let config = GpuBackendConfig::new(
2324 vec![GpuBackendDeviceConfig::new(
2325 GpuBackendDeviceProfile::new(0).with_max_active_attempts(2),
2326 GpuMemoryOwnership::WholeDeviceExclusive,
2327 )],
2328 execution,
2329 QueueBounds::default(),
2330 )
2331 .validate()
2332 .expect("device-local default remains valid");
2333
2334 let device = &config.validated_devices()[0];
2335 assert_eq!(device.config.source_work_per_attempt(), 1);
2336 assert_eq!(device.installation_bounds.stream_pool_size, 4);
2337 }
2338
2339 fn assert_query_activity_allows_close_and_replacement_after_release(acquire_context: bool) {
2340 let _owner_test = live_backend_test_lock();
2341 let backend = GpuBackendCore::new(one_device_backend_config(0)).unwrap();
2342 let lifecycle = backend.observation_core.lifecycle.clone();
2343 let observed_active_gpu_permits = Arc::new(std::sync::atomic::AtomicUsize::new(usize::MAX));
2344 let (completion, completed) = mpsc::sync_channel(1);
2345 let activity = QueryActivityLease::new(ActivityPermitOrderingProbe {
2346 lifecycle: lifecycle.clone(),
2347 backend: backend.clone(),
2348 observed_active_gpu_permits: Arc::clone(&observed_active_gpu_permits),
2349 completion,
2350 });
2351 let attempt = AttemptOwner::native_for_test();
2352 let request =
2353 QueryAdmissionRequest::new(attempt.admission_binding()).with_runtime_observation_activity(activity);
2354 let handle = backend.session_capability().admit_query_with_request(request).unwrap();
2355 assert_eq!(lifecycle.snapshot().active_gpu_permits, 1);
2356 if acquire_context {
2357 let context = handle.acquire_context().expect("query acquires a service-stream context");
2358 drop(context);
2359 }
2360
2361 drop(attempt);
2363 drop(handle);
2364
2365 completed
2366 .recv_timeout(Duration::from_secs(30))
2367 .expect("activity probe completed")
2368 .expect("activity observed close quiescence and replacement ownership");
2369 assert_eq!(observed_active_gpu_permits.load(Ordering::Acquire), 0);
2370 let close = backend.close_and_wait(Duration::from_secs(10)).unwrap();
2371 assert!(close.gpu_resource_quiescent);
2372 assert!(close.observation_quiescent);
2373 }
2374
2375 fn query_activity_returns_only_after_service_resources_and_backend_gpu_permit_release_case() {
2376 assert_query_activity_allows_close_and_replacement_after_release(false);
2377 }
2378
2379 #[test]
2380 fn query_activity_returns_only_after_service_resources_and_backend_gpu_permit_release() {
2381 query_activity_returns_only_after_service_resources_and_backend_gpu_permit_release_case();
2382 context_cleanup_completes_before_query_activity_can_close_and_replace_backend();
2383 }
2384
2385 fn context_cleanup_completes_before_query_activity_can_close_and_replace_backend() {
2386 assert_query_activity_allows_close_and_replacement_after_release(true);
2387 }
2388
2389 #[test]
2390 fn backend_observation_quiescence_waits_for_detached_seal_observers() {
2391 let _owner_test = live_backend_test_lock();
2392 let backend = GpuBackendCore::new(one_device_backend_config(0)).unwrap();
2393 let attempt = AttemptOwner::native_for_test();
2394 let binding = attempt.admission_binding();
2395 let gate = Arc::new((Mutex::new(false), Condvar::new()));
2396 let observer_gate = Arc::clone(&gate);
2397 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
2398 binding.register_seal_observer(move |_snapshot| {
2399 let _ = entered_sender.send(());
2400 let (lock, changed) = &*observer_gate;
2401 let mut released = lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
2402 while !*released {
2403 released = changed.wait(released).unwrap_or_else(std::sync::PoisonError::into_inner);
2404 }
2405 });
2406 let handle =
2407 backend.session_capability().admit_query_with_request(QueryAdmissionRequest::new(binding)).unwrap();
2408 drop(handle.acquire_context().expect("query acquires a service-stream context"));
2409 drop(attempt);
2410 drop(handle);
2411
2412 entered_receiver
2413 .recv_timeout(Duration::from_secs(10))
2414 .expect("detached observation completion reaches its seal observer");
2415 let error = backend.close_and_wait(Duration::from_millis(250)).unwrap_err();
2416 assert_eq!(error.fact_value("reason"), Some("backend_close_timeout"));
2417 assert_eq!(error.fact_value("gpu_resource_quiescent"), Some("true"));
2418 assert_eq!(error.fact_value("observation_quiescent"), Some("false"));
2419 assert_eq!(error.fact_value("active_gpu_permits"), Some("0"));
2420 assert_eq!(error.fact_value("active_observation_permits"), Some("1"));
2421
2422 let (lock, changed) = &*gate;
2423 *lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = true;
2424 changed.notify_all();
2425 let close = backend.close_and_wait(Duration::from_secs(10)).unwrap();
2426 assert!(close.gpu_resource_quiescent);
2427 assert!(close.observation_quiescent);
2428 }
2429
2430 #[test]
2431 fn reentrant_activity_close_does_not_block_later_query_stream_cleanup() {
2432 let _owner_test = live_backend_test_lock();
2433 let backend = GpuBackendCore::new(
2434 backend_config(vec![GpuBackendDeviceConfig::new(
2435 GpuBackendDeviceProfile::new(0)
2436 .with_default_query_limit_bytes(256 * 1024 * 1024)
2437 .with_max_active_attempts(2)
2438 .with_installation_bounds(GpuBackendDeviceInstallationBounds::new(6, 3, 2)),
2439 GpuMemoryOwnership::WholeDeviceExclusive,
2440 )])
2441 .validate()
2442 .expect("explicit-bounds backend config validates"),
2443 )
2444 .unwrap();
2445 let (entered_sender, entered_receiver) = mpsc::sync_channel(1);
2446 let (completion_sender, completion_receiver) = mpsc::sync_channel(1);
2447 let first_attempt = AttemptOwner::native_for_test();
2448 let first = backend
2449 .session_capability()
2450 .admit_query_with_request(
2451 QueryAdmissionRequest::new(first_attempt.admission_binding()).with_runtime_observation_activity(
2452 QueryActivityLease::new(ReentrantCloseActivityProbe {
2453 backend: backend.clone(),
2454 entered: entered_sender,
2455 completion: completion_sender,
2456 }),
2457 ),
2458 )
2459 .unwrap();
2460 let second =
2461 backend.session_capability().admit_query_with_request(QueryAdmissionRequest::test_attempt()).unwrap();
2462 drop(first.acquire_context().expect("first query acquires a service-stream context"));
2463 drop(second.acquire_context().expect("second query acquires a service-stream context"));
2464
2465 drop(first_attempt);
2466 drop(first);
2467 entered_receiver
2468 .recv_timeout(Duration::from_secs(10))
2469 .expect("first activity begins its reentrant backend close");
2470
2471 second.end_test_attempt();
2472 drop(second);
2473
2474 let close = completion_receiver
2475 .recv_timeout(Duration::from_secs(20))
2476 .expect("later stream cleanup unblocks the reentrant close")
2477 .expect("reentrant close reaches full quiescence");
2478 assert!(close.gpu_resource_quiescent);
2479 assert!(close.observation_quiescent);
2480 }
2481
2482 #[test]
2483 fn post_worker_failure_transfers_owner_release_to_cleanup_coordinator() {
2484 let _owner_test = live_backend_test_lock();
2485 let ordinal = BACKEND_METRIC_DEVICE_CAPACITY - 1;
2488 let error = GpuBackendCore::construct(
2489 one_device_backend_config(ordinal),
2490 &FailingProbe(GpuBackendConstructionCheckpoint::AfterCleanupWorker),
2491 Arc::default(),
2492 ObservationCompletionExecutor::spawn,
2493 )
2494 .unwrap_err();
2495 assert_eq!(error.fact_value("reason"), Some("injected_construction_failure"));
2496 assert!(matches!(error.fact_value("rollback_cleanup"), Some("quiescent" | "transferred")));
2497
2498 wait_for_live_owner_release(ordinal);
2499 }
2500
2501 #[test]
2502 fn every_constructed_resource_is_journaled_before_its_after_boundary() {
2503 let _owner_test = live_backend_test_lock();
2504
2505 const MANAGER: &[&str] = &["memory_manager"];
2506 const STREAM: &[&str] = &["memory_manager", "stream_pool"];
2507 const CONTEXT: &[&str] = &["memory_manager", "stream_pool", "device_context"];
2508 const WORKER: &[&str] = &["memory_manager", "stream_pool", "device_context", "execution_pool"];
2509 const STARTUP: &[&str] =
2510 &["memory_manager", "stream_pool", "device_context", "execution_pool", "startup_records"];
2511 let cases = [
2512 (GpuBackendConstructionCheckpoint::AfterMemoryManager { device_ordinal: 0 }, MANAGER),
2513 (GpuBackendConstructionCheckpoint::AfterStreamPool { device_ordinal: 0 }, STREAM),
2514 (GpuBackendConstructionCheckpoint::AfterDeviceContext { device_ordinal: 0 }, CONTEXT),
2515 (GpuBackendConstructionCheckpoint::AfterWorkerPool { device_ordinal: 0 }, WORKER),
2516 (GpuBackendConstructionCheckpoint::AfterStartupRecords, STARTUP),
2517 (GpuBackendConstructionCheckpoint::Published, STARTUP),
2518 ];
2519
2520 for (checkpoint, expected_resources) in cases {
2521 let journal = Arc::new(ConstructionResourceJournal::default());
2522 let probe = Arc::new(JournalBoundaryProbe {
2523 checkpoint,
2524 journal: Arc::downgrade(&journal),
2525 expected_resources,
2526 inspected: AtomicU8::new(0),
2527 });
2528 let error = GpuBackendCore::construct(
2529 one_device_backend_config(0),
2530 probe.as_ref(),
2531 journal,
2532 ObservationCompletionExecutor::spawn,
2533 )
2534 .unwrap_err();
2535
2536 assert_eq!(error.fact_value("reason"), Some("injected_construction_failure"), "{checkpoint:?}");
2537 assert_eq!(probe.inspected.load(Ordering::Acquire), 1, "{checkpoint:?}");
2538 assert!(matches!(error.fact_value("rollback_cleanup"), Some("quiescent" | "transferred")));
2539 wait_for_live_owner_release(0);
2540 }
2541 }
2542
2543 #[test]
2544 fn owner_is_retained_until_blocked_execution_runtime_drains_and_joins() {
2545 let _owner_test = live_backend_test_lock();
2546 let ordinal = usize::MAX - 2;
2547 let owner_lease = claim_live_backend(&[ordinal]).unwrap();
2548 let journal = Arc::new(ConstructionResourceJournal::default());
2549 let pool = GpuExecutionPool::new(ordinal, 2).unwrap();
2550 journal.record_execution_pool(pool.clone()).unwrap();
2551 let gate = Arc::new(BlockingGate::default());
2552 let worker_gate = Arc::clone(&gate);
2553 let (started_sender, started_receiver) = mpsc::sync_channel(0);
2554 pool.spawn(GpuExecutionJobKind::Cleanup, async move {
2555 started_sender.send(()).unwrap();
2556 worker_gate.wait();
2557 })
2558 .unwrap();
2559 started_receiver.recv().unwrap();
2560
2561 let generation = Arc::new(ResourceGeneration::new(next_backend_identity(), owner_lease, Arc::clone(&journal)));
2562 let resource_core =
2563 Arc::new(ResourceCore { identity: next_backend_identity(), generation: Mutex::new(Some(generation)) });
2564 let observation_core = Arc::new(ObservationCore {
2565 identity: next_backend_identity(),
2566 lifecycle: BackendLifecycle::new(),
2567 attempt_counters: Arc::new(AttemptBackendCounters::default()),
2568 attempt_completion_queue: None,
2569 backend_metrics: BackendMetricsStore::new(next_backend_identity(), &[])
2570 .expect("test backend metrics store accepts an empty device set"),
2571 completion_executor: ObservationCompletionExecutor::spawn().unwrap(),
2572 });
2573 let coordinator = CloseCoordinator::spawn(Arc::clone(&resource_core), Arc::clone(&observation_core)).unwrap();
2574
2575 assert!(coordinator.signal());
2576 let timeout = coordinator
2577 .state
2578 .wait_until(Instant::now() + Duration::from_millis(250), &observation_core.lifecycle)
2579 .unwrap_err();
2580 assert_eq!(timeout.fact_value("reason"), Some("backend_close_timeout"));
2581 let replacement = claim_live_backend(&[ordinal]).unwrap_err();
2582 assert_eq!(replacement.fact_value("reason"), Some("backend_already_live"));
2583
2584 gate.release();
2585 let snapshot =
2586 coordinator.state.wait_until(Instant::now() + Duration::from_secs(5), &observation_core.lifecycle).unwrap();
2587 assert!(snapshot.gpu_resource_quiescent);
2588 assert!(snapshot.observation_quiescent);
2589 pool.join_supervisor().expect("coordinator joined the runtime supervisor before releasing ownership");
2590 let replacement = claim_live_backend(&[ordinal]).expect("joined runtime permits a replacement owner");
2591 drop(replacement);
2592 }
2593
2594 #[test]
2595 fn dropping_last_public_handle_retains_owner_through_timeout_then_recovers_after_late_cleanup() {
2596 let _owner_test = live_backend_test_lock();
2597 let ordinal = usize::MAX - 3;
2598 let owner_lease = claim_live_backend(&[ordinal]).expect("test claims the live owner");
2599 let journal = Arc::new(ConstructionResourceJournal::default());
2600 let pool = GpuExecutionPool::new(ordinal, 2).expect("test execution pool");
2601 journal.record_execution_pool(pool.clone()).expect("journal records the execution pool");
2602 let gate = Arc::new(BlockingGate::default());
2603 let worker_gate = Arc::clone(&gate);
2604 let (started_sender, started_receiver) = mpsc::sync_channel(0);
2605 pool.spawn(GpuExecutionJobKind::Cleanup, async move {
2606 started_sender.send(()).expect("cleanup job start receiver remains live");
2607 worker_gate.wait();
2608 })
2609 .expect("cleanup job submits");
2610 started_receiver.recv().expect("cleanup job starts");
2611
2612 let generation = Arc::new(ResourceGeneration::new(next_backend_identity(), owner_lease, Arc::clone(&journal)));
2613 let resource_core =
2614 Arc::new(ResourceCore { identity: next_backend_identity(), generation: Mutex::new(Some(generation)) });
2615 let observation_core = Arc::new(ObservationCore {
2616 identity: next_backend_identity(),
2617 lifecycle: BackendLifecycle::new(),
2618 attempt_counters: Arc::new(AttemptBackendCounters::default()),
2619 attempt_completion_queue: None,
2620 backend_metrics: BackendMetricsStore::new(next_backend_identity(), &[])
2621 .expect("test backend metrics store accepts an empty device set"),
2622 completion_executor: ObservationCompletionExecutor::spawn().expect("test observation executor"),
2623 });
2624 let coordinator = CloseCoordinator::spawn(Arc::clone(&resource_core), Arc::clone(&observation_core))
2625 .expect("cleanup coordinator");
2626 let close_on_last = Arc::clone(&coordinator);
2627 let public_handles = PublicHandleTracker::new(move || {
2628 close_on_last.signal();
2629 });
2630 let backend = GpuBackendCore {
2631 resource_core,
2632 observation_core: Arc::clone(&observation_core),
2633 coordinator: Arc::clone(&coordinator),
2634 public_handle: public_handles.acquire().expect("first public backend handle"),
2635 startup_records: Arc::from([]),
2636 kvikio_process_config: None,
2637 generation_id: next_backend_identity(),
2638 };
2639
2640 drop(backend);
2641 let timeout = coordinator
2642 .state
2643 .wait_until(Instant::now() + Duration::from_millis(250), &observation_core.lifecycle)
2644 .expect_err("last-handle close cannot release a live execution runtime");
2645 assert_eq!(timeout.fact_value("reason"), Some("backend_close_timeout"));
2646 assert_eq!(timeout.fact_value("gpu_resource_quiescent"), Some("false"));
2647 assert!(coordinator.state.snapshot(&observation_core.lifecycle).closing, "last handle signals the close gate");
2648 let replacement =
2649 claim_live_backend(&[ordinal]).expect_err("late cleanup retains the device owner after last-handle drop");
2650 assert_eq!(replacement.fact_value("reason"), Some("backend_already_live"));
2651
2652 gate.release();
2653 let snapshot = coordinator
2654 .state
2655 .wait_until(Instant::now() + Duration::from_secs(5), &observation_core.lifecycle)
2656 .expect("late cleanup eventually quiesces both scopes");
2657 assert!(snapshot.gpu_resource_quiescent);
2658 assert!(snapshot.observation_quiescent);
2659 assert!(snapshot.cleanup_late_success_count > 0, "the timed-out cleanup phase is recorded as a late success");
2660 pool.join_supervisor().expect("cleanup joins the execution supervisor before owner release");
2661 let replacement =
2662 claim_live_backend(&[ordinal]).expect("owner lease becomes reusable only after late cleanup quiesces");
2663 drop(replacement);
2664 }
2665
2666 #[test]
2667 fn begin_attempt_after_close_is_rejected_without_creating_an_attempt() {
2668 let backend = inert_backend_for_observation_delivery_test();
2669 let session = backend.session_capability();
2670 assert!(backend.close());
2671 let before = session.attempt_counters_snapshot();
2672 let error = session.begin_attempt(BeginAttemptRequest::default()).unwrap_err();
2673 assert_eq!(error.code(), QueryErrorCode::Cancelled);
2674 assert_eq!(error.fact_value("reason"), Some("backend_closing"));
2675 let after = session.attempt_counters_snapshot();
2676 assert_eq!(before.attempts_begun, after.attempts_begun, "a losing begin creates no counted attempt");
2677 backend.close_and_wait(Duration::from_secs(10)).unwrap();
2678 }
2679
2680 #[test]
2681 fn begin_attempt_winning_the_close_gate_is_counted_until_report_ready() {
2682 let backend = inert_backend_for_observation_delivery_test();
2683 let session = backend.session_capability();
2684 let begin = session.begin_attempt(BeginAttemptRequest::default()).unwrap();
2685 assert_eq!(session.attempt_counters_snapshot().attempts_begun, 1);
2686
2687 assert!(backend.close());
2688 let snapshot = backend.close_snapshot();
2689 assert!(!snapshot.observation_quiescent, "an open attempt holds observation scope open");
2690
2691 begin.owner.commit_terminal(RuntimeTerminalOutcome::Succeeded).unwrap();
2694 let report = begin
2695 .completion
2696 .completion()
2697 .wait_timeout(Duration::from_secs(5))
2698 .expect("the attempt publishes its report during close");
2699 assert!(matches!(report.outcome, RuntimeTerminalOutcome::Succeeded));
2700 drop(begin);
2701 assert_eq!(session.attempt_counters_snapshot().reports_ready, 1);
2702 let close = backend.close_and_wait(Duration::from_secs(10)).unwrap();
2703 assert!(close.observation_quiescent);
2704 }
2705
2706 #[test]
2707 fn close_after_report_ready_does_not_wait_for_a_held_completion_receiver() {
2708 let backend = inert_backend_for_observation_delivery_test();
2709 let begin = backend.begin_attempt(BeginAttemptRequest::default()).expect("attempt begins before close");
2710
2711 begin.owner.commit_terminal(RuntimeTerminalOutcome::Succeeded).unwrap();
2712 let completion = begin.completion.completion();
2713 let report = completion.wait_timeout(Duration::from_secs(2)).expect("report is ready before close");
2714 assert!(matches!(report.outcome, RuntimeTerminalOutcome::Succeeded));
2715
2716 assert!(backend.close());
2717 let close = backend
2718 .close_and_wait(Duration::from_secs(2))
2719 .expect("reported attempt no longer holds close observation scope");
2720 assert!(close.gpu_resource_quiescent);
2721 assert!(close.observation_quiescent);
2722 assert!(completion.is_ready(), "the caller may retain its completed result after backend close");
2723 }
2724
2725 #[test]
2726 fn concurrent_begin_and_close_linearize_without_an_unaccounted_attempt() {
2727 const RACE_ITERATIONS: usize = 16;
2728
2729 for _ in 0..RACE_ITERATIONS {
2730 let backend = inert_backend_for_observation_delivery_test();
2731 let session = backend.session_capability();
2732 let start = Arc::new(std::sync::Barrier::new(3));
2733
2734 let begin_start = Arc::clone(&start);
2735 let begin_session = session.clone();
2736 let begin_worker = thread::spawn(move || {
2737 begin_start.wait();
2738 begin_session.begin_attempt(BeginAttemptRequest::default())
2739 });
2740
2741 let close_start = Arc::clone(&start);
2742 let close_backend = backend.clone();
2743 let close_worker = thread::spawn(move || {
2744 close_start.wait();
2745 close_backend.close()
2746 });
2747
2748 start.wait();
2749 let begin = begin_worker.join().expect("begin worker does not panic");
2750 assert!(close_worker.join().expect("close worker does not panic"), "the sole close signal wins");
2751
2752 let attempts_begun = match begin {
2753 Ok(begin) => {
2754 begin.owner.commit_terminal(RuntimeTerminalOutcome::Succeeded).unwrap();
2755 let report = begin
2756 .completion
2757 .completion()
2758 .wait_timeout(Duration::from_secs(2))
2759 .expect("winning begin publishes");
2760 assert!(matches!(report.outcome, RuntimeTerminalOutcome::Succeeded));
2761 1
2762 },
2763 Err(error) => {
2764 assert_eq!(error.code(), QueryErrorCode::Cancelled);
2765 assert_eq!(error.fact_value("reason"), Some("backend_closing"));
2766 0
2767 },
2768 };
2769
2770 let counters = session.attempt_counters_snapshot();
2771 assert_eq!(counters.attempts_begun, attempts_begun, "only a begin-gate winner is counted");
2772 assert_eq!(counters.reports_ready, attempts_begun, "every counted begin publishes before close completes");
2773 let error =
2774 session.begin_attempt(BeginAttemptRequest::default()).expect_err("close excludes every later begin");
2775 assert_eq!(error.fact_value("reason"), Some("backend_closing"));
2776
2777 let close = backend.close_and_wait(Duration::from_secs(2)).expect("linearized attempt state drains");
2778 assert!(close.gpu_resource_quiescent);
2779 assert!(close.observation_quiescent);
2780 }
2781 }
2782
2783 #[test]
2784 fn abandoned_attempt_reports_land_in_the_bounded_completion_queue() {
2785 let _owner_test = live_backend_test_lock();
2786 let config = one_device_backend(0).with_attempt_completion_queue_capacity(2).validate().unwrap();
2787 let backend = GpuBackendCore::new(config).unwrap();
2788 let session = backend.session_capability();
2789
2790 let begin = session.begin_attempt(BeginAttemptRequest::default()).unwrap();
2791 drop(begin.completion);
2792 begin.owner.commit_terminal(RuntimeTerminalOutcome::Dropped).unwrap();
2793 drop(begin.owner);
2794
2795 let drained = session.take_abandoned_attempt_reports(8);
2796 assert_eq!(drained.len(), 1);
2797 assert!(matches!(drained[0].outcome, RuntimeTerminalOutcome::Dropped));
2798 let counters = session.attempt_counters_snapshot();
2799 assert_eq!(counters.completion_queue_pushed, 1);
2800 assert_eq!(counters.completion_queue_overflowed, 0);
2801 backend.close_and_wait(Duration::from_secs(10)).unwrap();
2802 }
2803
2804 fn probe_facts(total_bytes: u64) -> DeviceFacts {
2805 DeviceFacts {
2806 device: 3,
2807 model_name: None,
2808 uuid: None,
2809 physical_total_bytes: Some(1_100),
2810 total_bytes,
2811 free_bytes: 500,
2812 sm_count: None,
2813 }
2814 }
2815
2816 #[test]
2817 fn probe_capacities_resolve_owned_and_managed_bounds() {
2818 let facts = probe_facts(1_000);
2819 for (limit, reserve, expected) in [
2820 (None, 0, (1_000, 1_000)),
2821 (None, 100, (1_000, 900)),
2822 (Some(600), 100, (600, 500)),
2823 (Some(1_000), 0, (1_000, 1_000)),
2824 ] {
2825 assert_eq!(resolve_probe_capacities(3, &facts, limit, reserve).expect("probe math resolves"), expected);
2826 }
2827 }
2828
2829 #[test]
2830 fn probe_capacities_reject_partition_above_cuda_total() {
2831 let facts = probe_facts(1_000);
2832 let error = resolve_probe_capacities(3, &facts, Some(1_001), 0).unwrap_err();
2833 assert_eq!(error.fact_value("reason"), Some("static_partition_exceeds_physical_capacity"));
2834 assert_eq!(error.fact_value("cuda_total_bytes"), Some("1000"));
2835 assert_eq!(error.fact_value("physical_total_bytes"), Some("1100"));
2836 }
2837
2838 #[test]
2839 fn probe_capacities_reject_exhausting_reserve() {
2840 let facts = probe_facts(1_000);
2841 for reserve in [1_000, 1_001] {
2842 let error = resolve_probe_capacities(3, &facts, None, reserve).unwrap_err();
2843 assert_eq!(error.fact_value("reason"), Some("backend_reserve_exhausts_capacity"));
2844 }
2845 }
2846
2847 #[test]
2848 fn derived_installation_bounds_clear_explicit_bounds() {
2849 let profile = GpuBackendDeviceProfile::new(0)
2850 .with_max_active_attempts(2)
2851 .with_installation_bounds(GpuBackendDeviceInstallationBounds::new(99, 99, 99))
2852 .with_derived_installation_bounds(4);
2853 assert_eq!(profile.max_active_attempts(), 4);
2854 assert_eq!(profile.installation_bounds(), None);
2855 }
2856
2857 #[test]
2858 fn homogeneous_device_configs_rebind_template_ordinals() {
2859 let template = GpuBackendDeviceProfile::new(7)
2860 .with_memory_limit(GpuDeviceMemoryLimit::Explicit { limit_bytes: 512 })
2861 .with_max_active_attempts(3);
2862 let devices = GpuBackendDeviceConfig::homogeneous([0, 2], &template, GpuMemoryOwnership::WholeDeviceExclusive);
2863 assert_eq!(devices.len(), 2);
2864 assert_eq!(devices[0].device_ordinal(), 0);
2865 assert_eq!(devices[1].device_ordinal(), 2);
2866 for device in &devices {
2867 assert_eq!(device.memory_limit(), GpuDeviceMemoryLimit::Explicit { limit_bytes: 512 });
2868 assert_eq!(device.max_active_attempts(), 3);
2869 assert_eq!(device.memory_ownership(), GpuMemoryOwnership::WholeDeviceExclusive);
2870 }
2871 }
2872
2873 #[test]
2874 fn probe_device_capacity_reports_live_cuda_totals() {
2875 let probe = probe_device_capacity(0, GpuMemoryOwnership::WholeDeviceExclusive, 0)
2876 .expect("GPU lane provides device 0 for capacity probing");
2877 assert_eq!(probe.device_ordinal, 0);
2878 assert!(probe.cuda_total_bytes > 0);
2879 assert!(probe.cuda_free_bytes <= probe.cuda_total_bytes);
2880 assert_eq!(probe.owned_capacity_bytes, probe.cuda_total_bytes);
2881 assert_eq!(probe.max_declarable_limit_bytes, probe.cuda_total_bytes);
2882 assert_eq!(probe.max_managed_capacity_bytes, probe.cuda_total_bytes);
2883 }
2884}