Skip to main content

query_runtime/runtime/
backend_core.rs

1//! Ownership-enforcing GPU backend construction and lifecycle facade.
2//!
3//! Configuration specifications (`GpuBackendDeviceProfile`,
4//! `GpuBackendDeviceConfig`, `GpuBackendConfig`) are validated into a
5//! `ValidatedGpuBackendConfig` before process-wide resources are acquired.
6//! `GpuBackendCore::construct` claims the exclusive live-backend lease for the
7//! configured devices, installs the query service and associated GPU resources within
8//! a resource generation, and publishes the core once each device startup record
9//! matches its validated profile. Downstream ownership is partitioned across
10//! distinct handle types: `GpuBackendCore` acts as a reference-counted owning handle
11//! that sustains the resource generation until the final instance drops, whereas
12//! `GpuBackendSession` maintains a `Weak` reference to prevent extending device
13//! ownership beyond backend close.
14//!
15//! The lifetime invariant ensures that admission, service reads, and attempt
16//! initiations can hold the resource generation only when granted by the
17//! `BackendLifecycle` gate prior to close. Backend closure releases the live
18//! owner lease after the query service achieves quiescence and all active holders
19//! terminate. The lifecycle gate resides in `backend_lifecycle`, while generation
20//! cleanup executes in `backend_close`. The governing admission contract is defined in
21//! `website/docs/design/admission.mdx#from-capability-validation-to-admission`, and thread
22//! and runtime ownership rules are described in `website/docs/design/index.mdx#runtimes-and-threads`.
23
24use 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/// Deployment ownership promised for one CUDA device.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum GpuMemoryOwnership {
76    /// Algeon is the exclusive allocator owner for the complete visible device.
77    WholeDeviceExclusive,
78    /// Algeon is constrained by an immutable process-manager allocation limit.
79    StaticPartition {
80        /// Immutable process-manager allocator limit in bytes.
81        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/// Pre-build capacity probe for one CUDA device.
95///
96/// Reports the declarable capacity of one device without acquiring the process
97/// owner lease or installing any allocator: the CUDA-visible totals, the NVML
98/// physical total when reported, the owned capacity for the requested
99/// ownership, and the managed capacity left after the backend reserve.
100/// Build-time resolution stays the enforcement authority; this probe
101/// predicts ownership-vs-facts inputs without requiring callers to resolve
102/// limits through repeated `build()` failures.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104#[non_exhaustive]
105pub struct GpuDeviceCapacityProbe {
106    pub(crate) device_ordinal: usize,
107    /// NVML-reported physical framebuffer capacity, when the optional NVML
108    /// substrate reported one. CUDA-visible capacity may be smaller by the
109    /// driver-reserved framebuffer.
110    pub(crate) physical_total_bytes: Option<u64>,
111    /// CUDA-visible allocation capacity. Ownership and explicit limits are
112    /// compared against this value. The NVML total plays no part in that comparison.
113    pub cuda_total_bytes: u64,
114    /// CUDA-visible free allocation bytes sampled by the probe; not an admission balance.
115    pub cuda_free_bytes: u64,
116    /// CUDA-visible total or the promised static-partition limit, in bytes.
117    pub owned_capacity_bytes: u64,
118    /// Largest explicit managed-capacity limit this ownership accepts,
119    /// before the backend reserve is subtracted.
120    pub max_declarable_limit_bytes: u64,
121    pub(crate) backend_reserve_bytes: u64,
122    /// Owned capacity minus backend reserve, in bytes.
123    pub max_managed_capacity_bytes: u64,
124}
125
126/// Queries declarable capacity for one device without building a backend.
127///
128/// This probe acquires no live-backend lease and installs no memory
129/// manager. Setting the explicit limit to at most `max_declarable_limit_bytes`
130/// ensures the build will not fail on capacity limits. Refusals carry the same
131/// `reason` facts reported by backend initialization.
132pub 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/// Soft admission capacity calculation for one device.
205///
206/// `Automatic` resolves at build time, after the device is discovered, to the
207/// owned capacity minus `headroom_bytes`, or to `fallback_bytes` when the
208/// subtraction leaves nothing; `fraction` further caps the result at that
209/// share of the owned capacity. `Explicit` is accepted as given and is
210/// rejected when it exceeds the owned capacity: at validation for a static
211/// partition, at build for a whole device. Both describe the ledger's
212/// configured limit; admission commits grants against it, and neither
213/// constrains the allocator installed on the device.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
215#[non_exhaustive]
216pub enum GpuDeviceMemoryLimit {
217    /// Discovers admission capacity from the promised owned device capacity.
218    /// The default reserves 500 MiB, falls back to 256 MiB and applies no fraction.
219    Automatic {
220        /// Bytes subtracted from owned capacity before resolving the limit.
221        headroom_bytes: u64,
222        /// Positive byte limit used when subtraction leaves no capacity.
223        fallback_bytes: u64,
224        /// Optional further cap on the share of owned capacity.
225        fraction: Option<GpuMemoryFraction>,
226    },
227    /// Declares an immutable byte limit checked against owned device capacity.
228    Explicit {
229        /// Admission capacity in bytes before backend reserve is subtracted.
230        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/// Validated automatic-capacity fraction retained unresolved until post-claim
241/// device discovery. The private bit representation makes `Eq`/`Hash` sound.
242/// NaN, infinities, zero, and out-of-range values cannot be constructed.
243#[derive(Clone, Copy, PartialEq, Eq, Hash)]
244pub struct GpuMemoryFraction(u64);
245
246impl GpuMemoryFraction {
247    /// Validates a capacity fraction in `(0, 1]`.
248    /// Returns `InvalidConfiguration` for zero, negative values, values above
249    /// one, NaN or infinity.
250    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    /// Returns the validated fraction without applying it to a device capacity.
259    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/// Explicit installation limits for one device profile.
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276#[non_exhaustive]
277pub struct GpuBackendDeviceInstallationBounds {
278    /// Installed streams, covering root and auxiliary slots for active attempts.
279    pub stream_pool_size: usize,
280    /// Worker threads, covering active attempts plus the reserved lifecycle worker.
281    pub worker_count: usize,
282    /// Execution lanes, at least the maximum active-attempt count.
283    pub lane_count: usize,
284}
285
286impl GpuBackendDeviceInstallationBounds {
287    /// Creates explicit stream, worker, and execution-lane bounds.
288    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/// Pure per-device construction profile validated before owner acquisition.
294///
295/// Encapsulates device-local resource policies and configuration defaults,
296/// including capacity, reserve, query limit, active-attempt count, source work,
297/// installation bounds, capabilities, and telemetry. Retains no memory ownership;
298/// the deployment owner binds a profile with explicit [`GpuMemoryOwnership`]
299/// via [`GpuBackendDeviceConfig::new`].
300#[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    /// Absence derives the default grant from the resolved query share
316    /// divided by the maximum active-attempt count.
317    pub(crate) const DEFAULT_QUERY_LIMIT_BYTES: Option<u64> = None;
318    /// One auxiliary source-work slot is reserved for each active attempt by
319    /// default.
320    pub(crate) const DEFAULT_SOURCE_WORK_PER_ATTEMPT: usize = 1;
321    /// Device admission begins with two active attempts unless the installer
322    /// explicitly freezes a different per-device limit.
323    pub(crate) const DEFAULT_MAX_ACTIVE_ATTEMPTS: usize = 2;
324
325    /// Starts a profile with automatic capacity discovery and two active
326    /// attempts. Callers may override any device-local bound below.
327    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    /// Sets the admission capacity policy. Validation and device discovery
343    /// resolve the policy before managed capacity is installed.
344    pub fn with_memory_limit(mut self, memory_limit: GpuDeviceMemoryLimit) -> Self {
345        self.memory_limit = memory_limit;
346        self
347    }
348
349    /// Freezes the native execution capabilities available on this device.
350    ///
351    /// The default accepts every capability with a modeled native contract.
352    /// Supplying an exact profile is useful for heterogeneous deployments and
353    /// is consumed by admission before any waiter is queued.
354    pub fn with_device_capabilities(mut self, device_capabilities: DeviceCapabilities) -> Self {
355        self.device_capabilities = device_capabilities;
356        self
357    }
358
359    /// Sets bytes reserved outside managed query and cache capacity.
360    /// Backend construction rejects a reserve that leaves no managed capacity.
361    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    /// Sets the aggregate accounted resident-cache ceiling for this device.
367    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    /// Returns the accounted resident-cache ceiling in bytes. The default is zero.
373    pub fn cache_cap_bytes(&self) -> u64 {
374        self.cache_cap_bytes
375    }
376
377    /// Sets the immutable default memory limit for each query on this device.
378    /// When unset, it resolves to the query share divided by the maximum
379    /// active-attempt count, rounded down to whole bytes.
380    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    /// Sets source work reserved per active attempt.
386    ///
387    /// This is part of the immutable device envelope; execution/session
388    /// source scheduling controls must not change controller capacity.
389    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    /// Sets the device admission concurrency bound. Zero is rejected by
395    /// backend validation; existing explicit installation bounds remain in force.
396    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    /// Sets the active-attempt limit and clears explicit installation bounds
402    /// so construction derives them from this profile.
403    ///
404    /// Prefer this over spelling `with_max_active_attempts` plus
405    /// `with_installation_bounds` separately: the pair carries the same
406    /// number twice with no type-level guarantee they agree. Explicit bounds
407    /// remain available through `with_installation_bounds` for deployments
408    /// that deliberately overprovision stream or worker capacity.
409    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    /// Sets explicit stream, worker, and execution-lane bounds.
416    ///
417    /// Most callers should prefer [`Self::with_derived_installation_bounds`];
418    /// use this only to deliberately overprovision beyond the derived
419    /// `A_d * (1 + S_d)` stream formula.
420    pub fn with_installation_bounds(mut self, installation_bounds: GpuBackendDeviceInstallationBounds) -> Self {
421        self.installation_bounds = Some(installation_bounds);
422        self
423    }
424
425    /// Enables or disables installation of diagnostic device telemetry.
426    /// Telemetry does not participate in query admission accounting.
427    pub fn with_telemetry(mut self, install_telemetry: bool) -> Self {
428        self.install_telemetry = install_telemetry;
429        self
430    }
431
432    /// Returns the CUDA device ordinal selected by this declaration.
433    pub fn device_ordinal(&self) -> usize {
434        self.device_ordinal
435    }
436
437    /// Immutable native execution capabilities selected for this device.
438    pub fn device_capabilities(&self) -> &DeviceCapabilities {
439        &self.device_capabilities
440    }
441
442    /// Returns the unresolved admission capacity policy, resolved at device discovery.
443    pub fn memory_limit(&self) -> GpuDeviceMemoryLimit {
444        self.memory_limit
445    }
446
447    /// Returns bytes reserved outside managed query and cache capacity. The default is zero.
448    pub fn backend_reserve_bytes(&self) -> u64 {
449        self.backend_reserve_bytes
450    }
451
452    /// Returns the explicit default query grant in bytes. `None` requests
453    /// `floor((managed_capacity_bytes - cache_cap_bytes) / max_active_attempts)`
454    /// after device capacity resolution.
455    pub fn default_query_limit_bytes(&self) -> Option<u64> {
456        self.default_query_limit_bytes
457    }
458
459    /// Returns auxiliary source-work slots reserved for each active attempt. The default is one.
460    pub fn source_work_per_attempt(&self) -> usize {
461        self.source_work_per_attempt
462    }
463
464    /// Returns the maximum simultaneously admitted attempts on this device. The default is two.
465    pub fn max_active_attempts(&self) -> usize {
466        self.max_active_attempts
467    }
468
469    /// Returns the explicit installation bounds. `None` requests the values
470    /// returned by [`Self::derived_installation_bounds`].
471    pub fn installation_bounds(&self) -> Option<GpuBackendDeviceInstallationBounds> {
472        self.installation_bounds
473    }
474
475    /// The installation bounds a profile without explicit ones receives:
476    /// `A_d * (1 + S_d)` stream slots, `A_d` execution lanes, and one worker
477    /// per active attempt plus the worker reserved for context setup and
478    /// release cleanup. Explicit bounds must cover the stream slots and
479    /// workers of this derivation.
480    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    /// Returns whether device telemetry is installed. New profiles enable it.
497    pub fn install_telemetry(&self) -> bool {
498        self.install_telemetry
499    }
500}
501
502/// Pure per-device construction value binding a profile to deployment ownership.
503///
504/// The profile owns every device-local default; this value only completes the
505/// binding with the process owner's explicit [`GpuMemoryOwnership`]. Backend
506/// construction resolves installation bounds and validates the bound value.
507#[derive(Debug, Clone, PartialEq, Eq)]
508pub struct GpuBackendDeviceConfig {
509    profile: GpuBackendDeviceProfile,
510    memory_ownership: GpuMemoryOwnership,
511}
512
513impl GpuBackendDeviceConfig {
514    /// Builds one device config per ordinal from a shared template profile.
515    ///
516    /// Every returned config clones the template policy (capacity, reserve,
517    /// query limit, attempts, bounds, capabilities, telemetry) and rebinds
518    /// only the device ordinal; the template's own ordinal is ignored. The
519    /// deployment ownership is shared by every device. This is the shorthand
520    /// for the common homogeneous multi-device deployment; heterogeneous
521    /// devices still declare one [`Self::new`] each.
522    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    /// Binds a device profile to the deployment ownership promise.
538    /// Validation occurs when the backend builder validates or builds this config.
539    pub fn new(profile: GpuBackendDeviceProfile, memory_ownership: GpuMemoryOwnership) -> Self {
540        Self { profile, memory_ownership }
541    }
542
543    /// Returns the device-local construction policy bound to this ownership promise.
544    pub fn profile(&self) -> &GpuBackendDeviceProfile {
545        &self.profile
546    }
547
548    /// Returns the CUDA device ordinal selected by this declaration.
549    pub fn device_ordinal(&self) -> usize {
550        self.profile.device_ordinal
551    }
552
553    /// Immutable native execution capabilities selected for this device.
554    pub fn device_capabilities(&self) -> &DeviceCapabilities {
555        &self.profile.device_capabilities
556    }
557
558    /// Returns the deployment ownership promised by the process owner.
559    pub fn memory_ownership(&self) -> GpuMemoryOwnership {
560        self.memory_ownership
561    }
562
563    /// Returns the unresolved admission capacity policy, resolved at device discovery.
564    pub fn memory_limit(&self) -> GpuDeviceMemoryLimit {
565        self.profile.memory_limit
566    }
567
568    /// Returns bytes reserved outside managed query and cache capacity. The default is zero.
569    pub fn backend_reserve_bytes(&self) -> u64 {
570        self.profile.backend_reserve_bytes
571    }
572
573    /// Returns the accounted resident-cache ceiling in bytes. The default is zero.
574    pub fn cache_cap_bytes(&self) -> u64 {
575        self.profile.cache_cap_bytes
576    }
577
578    /// Returns the explicit default query grant in bytes. `None` requests
579    /// `floor((managed_capacity_bytes - cache_cap_bytes) / max_active_attempts)`
580    /// after device capacity resolution.
581    pub fn default_query_limit_bytes(&self) -> Option<u64> {
582        self.profile.default_query_limit_bytes
583    }
584
585    /// Returns auxiliary source-work slots reserved for each active attempt. The default is one.
586    pub fn source_work_per_attempt(&self) -> usize {
587        self.profile.source_work_per_attempt
588    }
589
590    /// Returns the maximum simultaneously admitted attempts on this device. The default is two.
591    pub fn max_active_attempts(&self) -> usize {
592        self.profile.max_active_attempts
593    }
594
595    /// Returns whether device telemetry is installed. New profiles enable it.
596    pub fn install_telemetry(&self) -> bool {
597        self.profile.install_telemetry
598    }
599}
600
601/// Backend construction input for the device set, the execution and queue
602/// policy, and the optional attempt completion queue.
603///
604/// [`Self::validate`] produces the [`ValidatedGpuBackendConfig`] required by
605/// [`GpuBackendCore::new`], ensuring declaration rules execute prior to claiming
606/// the process owner.
607#[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    /// Resolves each device's installation bounds and checks every device,
630    /// execution, and queue rule. Devices are ordered by ordinal.
631    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/// Configuration for [`GpuBackendCore::new`] with verified rules, resolved
663/// installation bounds, and devices ordered by ordinal.
664#[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    /// The validated device configs, ordered by ordinal.
674    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/// One validated device: its config, the installation bounds it is built
696/// with, and its static partition in the native allocation-size type.
697#[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
710/// A profile without explicit installation bounds takes
711/// [`GpuBackendDeviceProfile::derived_installation_bounds`]; explicit bounds
712/// must cover its stream slots.
713fn 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
733/// Checks one device's rules and returns its static partition in the native
734/// allocation-size type.
735fn 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    // Every active attempt's query execution runs outside the reserved
760    // lifecycle workers.
761    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/// Construction boundary exposed to deterministic probes and failure injectors.
919///
920/// Execution order differs from declaration order. `BeforeCleanupWorker` and
921/// `AfterCleanupWorker` bracket the close coordinator spawn and come first,
922/// because the coordinator is the rollback owner for everything after it. The
923/// per-device checkpoints then run inside the service install in ordinal
924/// order, followed by the startup-record pair and `Published`. A probe that
925/// fails at any checkpoint after `AfterCleanupWorker` observes the resources
926/// recorded so far in the construction journal, and the coordinator drains
927/// them.
928#[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
948/// Injectable construction observer. Returning an error injects failure at the
949/// named transaction boundary.
950pub(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    /// Claims the process owner of the configured devices, installs their
965    /// GPU resources, and publishes the backend.
966    ///
967    /// Only one backend may be live per process; a second call while one is
968    /// live, or while a closed one is still draining, fails with the
969    /// `backend_already_live` reason. Construction blocks the calling thread
970    /// for the CUDA context, memory manager, and stream pool installation of
971    /// every device. On failure the close coordinator takes over every
972    /// installed resource, and the error carries `rollback_cleanup` facts that
973    /// say whether that cleanup reached quiescence within its bounded slice or
974    /// was transferred to the coordinator's thread. In the latter case the
975    /// owner lease is released asynchronously, so a retry may briefly observe
976    /// `backend_already_live`.
977    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    /// The construction transaction behind [`Self::new`].
987    ///
988    /// Ordering matters at three points. The owner lease is claimed before any
989    /// GPU resource exists and moves into the resource generation immediately,
990    /// so a panic or early return cannot drop it from this stack while
991    /// installed resources are still live. The close coordinator is spawned
992    /// before the first device install because from that point the
993    /// coordinator is the rollback owner and this function is not.
994    /// Process-exit registration happens after the service install because
995    /// that install is what initializes CUDA, and the registration must
996    /// follow libcudart's own `atexit` handler to run before it.
997    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        // The generation shell and cleanup worker are the rollback journal for
1009        // every post-claim construction step. In particular, the live owner
1010        // lease is never held only by this builder's stack while GPU resources
1011        // are being installed.
1012        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            // CUDA has been initialized while installing the service-owned
1046            // device resources. Register the coordinator only now so its
1047            // `atexit` handler runs before libcudart's finalizer.
1048            observation_core.completion_executor.activate_process_exit();
1049            coordinator.activate_process_exit();
1050            // KvikIO's process-global pool is configured now, with CUDA
1051            // initialized and no source read in existence yet.
1052            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            // The startup records are cross-checked against the validated profiles. They
1058            // are not copied from them. A device whose installed worker count or allocation
1059            // limit differs from its profile means the service installed something other
1060            // than what validation approved, and the backend fails closed without publishing.
1061            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            // Public handles are counted by the tracker because `Arc` strong
1163            // counts also include sessions and the coordinator, whose
1164            // references must not keep the backend open.
1165            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
1191/// Applies [`KvikioProcessConfig::DEFAULT`] on the first owned device and
1192/// returns the effective settings. A cuDF build without KvikIO support leaves
1193/// the pool to libcudf and reports `None`.
1194fn 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/// Immutable per-device record frozen before backend publication.
1205///
1206/// Each record combines the discovered device facts with the configuration
1207/// the device was installed under. It is read without any permit, so it stays
1208/// available after close for diagnostics about a generation that no longer
1209/// owns the device.
1210#[derive(Debug, Clone, PartialEq, Eq)]
1211pub struct GpuBackendDeviceStartupRecord {
1212    /// CUDA ordinal associated with this immutable installation record.
1213    pub device_ordinal: usize,
1214    /// NVML product name captured with the immutable device inventory facts.
1215    pub model_name: Option<String>,
1216    /// NVML UUID captured with the immutable device inventory facts.
1217    pub uuid: Option<String>,
1218    /// NVML-reported physical framebuffer capacity captured with the immutable
1219    /// device inventory facts.
1220    pub physical_total_bytes: Option<u64>,
1221    /// Device facts sampled during backend construction.
1222    pub facts: DeviceFacts,
1223    /// Managed capacity the ledger commits grants against: the resolved
1224    /// [`GpuDeviceMemoryLimit`] minus `backend_reserve_bytes`.
1225    pub capacity_bytes: u64,
1226    /// Full ordinal-local resource-controller profile frozen after discovery.
1227    pub resource_profile: DeviceResourceProfile,
1228    /// Deployment ownership promise used to resolve installed capacity.
1229    pub memory_ownership: GpuMemoryOwnership,
1230    /// Admission capacity policy used for this device installation.
1231    pub memory_limit: GpuDeviceMemoryLimit,
1232    /// Bytes excluded from managed query and cache capacity.
1233    pub backend_reserve_bytes: u64,
1234    /// Maximum simultaneous device-backed admitted attempts.
1235    pub max_active_attempts: usize,
1236    /// Installed CUDA stream-pool capacity.
1237    pub stream_pool_size: usize,
1238    /// Installed execution-worker thread count, including lifecycle workers.
1239    pub worker_count: usize,
1240    /// Installed device execution-lane count.
1241    pub lane_count: usize,
1242    /// Whether diagnostic telemetry was requested for this installation.
1243    pub install_telemetry: bool,
1244    /// Native memory-manager installation status recorded during construction.
1245    pub memory_manager_installation: DeviceMemoryManagerInstallationState,
1246    /// Persistent engine health epoch bound to this device installation.
1247    pub device_health_generation: u64,
1248    /// Backend-owner health epoch for this published core generation.
1249    pub backend_health_generation: u64,
1250}
1251
1252/// Shared identity facts used to prove facade/session clones reuse one domain.
1253///
1254/// All three ids come from one process-wide counter, so two backends built in
1255/// the same process never share an id even after the first has closed.
1256#[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/// One process-owned GPU backend core.
1264///
1265/// Every value of this type is a public handle. Clones mint a new
1266/// `PublicHandleLease`, and dropping the last lease signals close the same
1267/// way [`Self::close`] does. Dropping all handles therefore never leaks the device
1268/// owner, but neither does it wait for cleanup; callers that need to know when
1269/// the devices are reusable call one of the `close_and_wait*` methods first.
1270#[derive(Debug)]
1271pub struct GpuBackendCore {
1272    resource_core: Arc<ResourceCore>,
1273    observation_core: Arc<ObservationCore>,
1274    coordinator: Arc<CloseCoordinator>,
1275    /// The counted handle. Its tracker fires the close signal on the last
1276    /// drop; `Arc` counts on the cores are not consulted because sessions and
1277    /// the coordinator hold them too.
1278    public_handle: PublicHandleLease,
1279    startup_records: Arc<[GpuBackendDeviceStartupRecord]>,
1280    kvikio_process_config: Option<KvikioProcessConfig>,
1281    generation_id: u64,
1282}
1283
1284/// Explicit capability for controlled fault validation on one live backend.
1285///
1286/// Constructing or holding this value does not alter backend behavior. Only
1287/// [`Self::quarantine_device`] changes state. It transitions the existing
1288/// per-device admission ledger and introduces no separate validation or
1289/// production scheduler mode.
1290#[cfg(feature = "qualification")]
1291#[derive(Clone, Debug)]
1292pub struct GpuBackendFaultInjector {
1293    session: GpuBackendSession,
1294}
1295
1296#[cfg(feature = "qualification")]
1297impl GpuBackendFaultInjector {
1298    /// Closes admission on exactly one installed device ledger.
1299    ///
1300    /// This emulates the controller-visible quarantine result of a proven
1301    /// device-local native fault without fabricating an unsafe CUDA fault. The
1302    /// remaining healthy compatible ledgers remain eligible for placement.
1303    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    /// Mints the capability that session and optimizer state hold.
1324    ///
1325    /// A session is not a public handle: it holds the resource core weakly, so
1326    /// any number of live sessions neither delay the last-handle close signal
1327    /// nor keep the resource generation reachable after close has taken it.
1328    /// The observation core is held strongly because attempt reports and
1329    /// metrics must stay readable after the GPU resources are gone.
1330    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    /// Returns an explicit capability for controlled device-quarantine
1339    /// validation. This is not a backend construction or production mode.
1340    #[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    /// KvikIO's process settings as applied when this backend was built, or
1350    /// `None` for a cuDF build without KvikIO support.
1351    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    /// See [`GpuBackendSession::begin_attempt`].
1364    pub fn begin_attempt(&self, request: BeginAttemptRequest) -> Result<AttemptBegin> {
1365        self.session_capability().begin_attempt(request)
1366    }
1367
1368    /// Returns one coherent pull snapshot for this backend resource generation.
1369    ///
1370    /// The snapshot has a monotonic store generation so a consumer can detect
1371    /// whether it combined observations from different points in time.
1372    pub fn backend_metrics_snapshot(&self) -> BackendMetricsSnapshot {
1373        self.observation_core.backend_metrics.snapshot()
1374    }
1375
1376    /// Signals close without waiting. Returns `true` only for the caller that
1377    /// won the lifecycle gate; later calls, and a last-handle drop after an
1378    /// explicit close, return `false` and change nothing.
1379    ///
1380    /// From the winning call on, every new admission, service read, and
1381    /// attempt begin fails with the `backend_closing` reason, while work that
1382    /// already holds a permit runs to completion. Cleanup proceeds on the
1383    /// coordinator's thread.
1384    pub fn close(&self) -> bool {
1385        self.coordinator.signal()
1386    }
1387
1388    /// Point-in-time close progress. Readable at any time, including before
1389    /// close and from inside observation delivery.
1390    pub fn close_snapshot(&self) -> GpuBackendCloseSnapshot {
1391        self.coordinator.state.snapshot(&self.observation_core.lifecycle)
1392    }
1393
1394    /// Signals close and blocks the calling thread until both quiescence
1395    /// scopes are reached or `timeout` elapses.
1396    ///
1397    /// A timeout returns `ServiceOverloaded` with reason `backend_close_timeout`
1398    /// and the quiescence facts at that moment; cleanup continues on the
1399    /// coordinator's thread and a later call can observe its completion. A
1400    /// cleanup failure returns `Internal` with reason `cleanup_failed_closed`;
1401    /// the owner lease is then retained, and the devices cannot be claimed
1402    /// again in this process. Calling from a runtime observation observer is
1403    /// rejected with `reentrant_observation_close` before close is signalled,
1404    /// because that observer is itself observation work the wait would need to
1405    /// finish.
1406    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    /// [`Self::close_and_wait`] against an absolute deadline. Lock contention
1413    /// inside the wait consumes this same deadline.
1414    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    /// [`Self::close_and_wait`] for callers on a Tokio runtime. The wait parks
1420    /// a thread, so it runs on the host runtime's blocking pool; the reentrancy
1421    /// check runs first, on the caller's task, so a rejected call never spawns.
1422    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/// Opaque capability installed into session/optimizer state.
1439///
1440/// It cannot construct or expose raw runtime resources and its weak resource
1441/// reference cannot keep device ownership alive after public facade close.
1442#[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    /// Returns whether the backend close gate has been signalled.
1455    ///
1456    /// An attempt that won the begin gate reads this flag and terminalizes
1457    /// `unplanned` with `backend_closing` evidence. Planning does not start in
1458    /// that case. The begin gate itself remains the only linearization point.
1459    pub fn is_closing(&self) -> bool {
1460        self.observation_core.lifecycle.is_closing()
1461    }
1462
1463    /// Begins one attempt lifecycle under this backend's observation scope.
1464    ///
1465    /// The observation permit is the linearization point: an attempt that
1466    /// obtains it is counted in `attempts_begun` and holds the observation
1467    /// scope open until its report is ready, so close cannot complete without
1468    /// that report. A begin that loses to close fails with `backend_closing`
1469    /// and leaves no counted attempt. No GPU permit is taken here; the attempt
1470    /// claims a device only when its first native fragment is admitted.
1471    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    /// Returns the same coherent generation snapshot exposed by the owning
1485    /// backend core without requiring access to live GPU resources.
1486    pub fn backend_metrics_snapshot(&self) -> BackendMetricsSnapshot {
1487        self.observation_core.backend_metrics.snapshot()
1488    }
1489
1490    /// Drains up to `max` reports of attempts whose completion receiver was
1491    /// dropped before the report was ready. Empty when the backend was built
1492    /// without an attempt completion queue.
1493    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    /// Takes the permits an admission or service read needs to enter the
1502    /// current resource generation.
1503    ///
1504    /// The order is load-bearing. The GPU permit is taken while the generation
1505    /// slot's mutex is held, and the close worker takes the slot under that
1506    /// same mutex only after setting the close bit. A caller that wins the GPU
1507    /// permit therefore always finds the generation still in the slot, and
1508    /// the close worker, which waits for the GPU permit count to reach zero
1509    /// before releasing the generation, can never observe a count of zero
1510    /// while a caller still holds a generation clone. The observation permit
1511    /// is taken first so a backend that is closing is rejected before the
1512    /// mutex is touched.
1513    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    /// Admits a query on the calling thread, which parks until the request is
1527    /// admitted or rejected. Production frontends use
1528    /// [`Self::admit_async_with_request`].
1529    #[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    /// Admits a fresh native attempt; see [`QueryAdmissionRequest::test_attempt`].
1538    #[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    /// Admits a query only if it can be placed now; otherwise the request
1545    /// leaves the queue and this returns `None`.
1546    #[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    /// Admits one native attempt, waiting in the bounded admission queue
1557    /// until a grant is placed, the request is cancelled, or its queue
1558    /// deadline passes.
1559    ///
1560    /// The observation and GPU permits are taken before the request enters
1561    /// the queue and travel with it inside its `QueryCompletion`, so an
1562    /// attempt that is queued, admitted, or cleaning up holds the resource
1563    /// generation for its whole lifetime, and close waits for it. On a
1564    /// rejected or cancelled request the GPU permit is released before the
1565    /// observation is sealed and delivered, the observation permit covers that
1566    /// delivery and is released afterwards, and the opaque activity lease runs
1567    /// last because its destructor may reenter close. Failures before the
1568    /// queue carry `backend_closing`
1569    /// (`Cancelled`); failures inside the queue are the admission controller's
1570    /// (`ServiceOverloaded` for a full queue or an elapsed wait, the typed
1571    /// capability or health rejections otherwise).
1572    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    /// Whether both sessions were minted from the same published core.
1579    pub fn same_backend(&self, other: &Self) -> bool {
1580        self.identities == other.identities
1581    }
1582
1583    /// Reads every service report once. Like every service read, it fails
1584    /// with `backend_closing` once close begins.
1585    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    /// Runs a read against the live service while holding both permits, so
1594    /// the service and its device resources cannot be released mid-read.
1595    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
1602/// Projects each sealed attempt report into the backend metrics store. The
1603/// sink runs once per attempt at seal time; the store therefore stays a
1604/// projection of the observation snapshot and holds no events of its own.
1605fn 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
1620/// Permit required by private service admission. Field order carries the
1621/// release order; see the field comment.
1622pub(crate) struct GpuResourcePermit {
1623    generation: Arc<ResourceGeneration>,
1624    // Declare this last. This keep-alive guard is never read, but its `Drop`
1625    // releases the GPU lifecycle count, and that release must happen only
1626    // after `generation` (declared above) so close waiters never observe a
1627    // zero count while a generation is still retained. Reordering the fields
1628    // breaks the invariant.
1629    _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/// The detachable slot that holds the current resource generation.
1642///
1643/// Held strongly by public handles and by the close worker, weakly by
1644/// sessions. The slot is written twice in a backend's life: filled at
1645/// construction and taken by the close worker after it has set the lifecycle
1646/// close bit. Because `acquire_resource` reads it under the same mutex while
1647/// taking its GPU permit, "closed" and "holds the generation" are mutually
1648/// exclusive for every caller.
1649#[derive(Debug)]
1650pub(crate) struct ResourceCore {
1651    identity: u64,
1652    pub(crate) generation: Mutex<Option<Arc<ResourceGeneration>>>,
1653}
1654
1655/// State that outlives the GPU resources: the lifecycle gate, attempt
1656/// counters, the abandoned-report queue, metrics, and the observation
1657/// delivery executor.
1658///
1659/// Sessions hold this strongly so attempt begin and report delivery keep
1660/// working after GPU quiescence. The `lifecycle` clone is shared with the
1661/// close coordinator; both see the same close bit and permit counts.
1662#[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/// Everything one backend generation owns on the devices, plus the lease
1673/// that makes it the process's live backend.
1674///
1675/// Created before any GPU resource exists and retained by every
1676/// `GpuResourcePermit`, so the service and journal cannot be dropped while an
1677/// admitted attempt or a service read still references them. The close worker
1678/// drops the last reference after the service has drained and the GPU permit
1679/// count has reached zero. A cleanup panic passes it to `std::mem::forget`.
1680/// Forgetting keeps the owner lease claimed, which prevents a replacement
1681/// backend from claiming devices with unknown outstanding GPU work.
1682#[derive(Debug)]
1683pub(crate) struct ResourceGeneration {
1684    /// Set once, after the service install succeeds. `None` only during the
1685    /// construction window, when no session can exist yet.
1686    service: OnceLock<QueryService>,
1687    startup_records: OnceLock<Arc<[GpuBackendDeviceStartupRecord]>>,
1688    generation_id: u64,
1689    /// Keeps every installed GPU resource alive through close so the drain
1690    /// can run against them after the service itself has been dropped.
1691    construction_journal: Arc<ConstructionResourceJournal>,
1692    // Declared last and explicitly dropped last by the cleanup worker.
1693    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/// Append-only record of every GPU resource created during construction, in
1730/// creation order.
1731///
1732/// The service install records each resource immediately after creating it
1733/// and before the next probe checkpoint, so a failure at any checkpoint leaves
1734/// a journal that names everything built so far. The entries are clones of
1735/// the handles the service also holds; the journal keeps the underlying
1736/// pools and contexts alive until the generation drops, so close can drain
1737/// them after the service is gone and construction rollback can drain them
1738/// when no service was ever published.
1739#[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    /// Closes and drains every journaled runtime in reverse creation order
1771    /// under one deadline.
1772    ///
1773    /// The call is idempotent. Pools that are already closed report
1774    /// quiescence at once, so the close worker calls this repeatedly in
1775    /// bounded slices. Every entry
1776    /// is visited even after a failure; a permanent error (anything other than
1777    /// an elapsed cleanup deadline) is returned in preference to a deadline
1778    /// error, so a slow drain cannot mask a broken one. Nothing is removed from
1779    /// the journal here; the resources are released when the generation drops.
1780    pub(crate) fn close_and_drain_until(&self, deadline: Instant) -> Result<()> {
1781        // Recording is one append, so a recovered guard holds every resource
1782        // recorded before a panic; each still needs its rollback.
1783        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                // Memory managers, device contexts, and startup records have
1793                // no drain step. They are journaled for retention order only:
1794                // the runtimes above must stop before these are dropped with
1795                // the generation.
1796                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
1850/// Process-wide identity counter shared by generation ids and both core ids,
1851/// so no two backends built in one process can present the same identity.
1852fn 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        // The owner drop is idempotent after report-ready. A peer sink panic
1996        // cannot duplicate or lose the engine-owned exact terminal update.
1997        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            // This intentionally skips every stack destructor. The registered
2019            // process-exit owners must still terminate without waiting for the
2020            // live handle above to release its admission slot.
2021            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        // This fails before device installation, so use a bounded synthetic
2244        // ordinal that the fixed-cardinality backend metrics store accepts.
2245        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        // Ending the attempt releases its grant; the handle releases nothing.
2362        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        // The probe stops before device installation; keep the ordinal inside
2486        // the backend metrics store's frozen device-label domain.
2487        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        // A CPU-only attempt terminalizes without GPU cleanup and publishes
2692        // its report; only then may observation quiescence complete.
2693        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}