1use std::{
30 cell::Cell,
31 future::Future,
32 panic::AssertUnwindSafe,
33 sync::{
34 Arc, Mutex,
35 atomic::{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering},
36 mpsc,
37 },
38 time::Instant,
39};
40#[cfg(any(test, feature = "test-hooks"))]
41use std::{
42 collections::HashMap,
43 sync::{OnceLock, Weak},
44};
45
46use futures::FutureExt;
47use query_planning::error::{QueryError, QueryErrorCode, Result};
48use serde::Serialize;
49#[cfg(any(test, feature = "test-hooks"))]
50use tokio::runtime::EnterGuard;
51use tokio::sync::{OwnedSemaphorePermit, Semaphore, TryAcquireError};
52
53use super::{ExecutionCancellation, context::QueryServiceResourceEpoch};
54use crate::observability::{GpuJobLifecycleState, RuntimeJobId, RuntimeObservationHandle, RuntimeStreamLeaseScope};
55
56mod inner;
57mod job;
58mod lease;
59mod supervisor;
60
61use inner::{GpuExecutionPoolInner, PoolDrain, pool_closed_error};
62use job::{
63 AbortOnDropJoinHandle, JOB_LIFECYCLE_PENDING, JobCounterGuard, JobObservation, JobTerminalGuard, PoolCounters,
64 QueryCounters, current_pool_id, current_worker_index, decrement, increment_with_peak, job_cancelled_error,
65 job_join_panic_error, job_panic_error, run_counted_job, run_counted_job_scoped, validate_worker_count,
66};
67use lease::{BlockingCompletionNotify, BlockingWorkOutcome};
68pub use lease::{GpuExecutorLease, PoolBlockingWork};
69use supervisor::RuntimeSupervisor;
70
71const BUILD_OPERATION: &str = "query_engine.gpu_execution_pool.build";
72const RUN_OPERATION: &str = "query_engine.gpu_execution_pool.run";
73#[cfg(any(test, feature = "test-hooks"))]
74const SHARED_OPERATION: &str = "query_engine.gpu_execution_pool.shared";
75const SHUTDOWN_OPERATION: &str = "query_engine.gpu_execution_pool.shutdown";
76
77static NEXT_POOL_ID: AtomicU64 = AtomicU64::new(1);
78
79thread_local! {
80 static CURRENT_POOL_ID: Cell<Option<u64>> = const { Cell::new(None) };
81 static CURRENT_WORKER_INDEX: Cell<Option<usize>> = const { Cell::new(None) };
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum GpuExecutionJobKind {
87 ContextSetup,
89 QueryExecution,
91 AttemptWork,
95 Cleanup,
97}
98
99pub(crate) const RESERVED_LIFECYCLE_WORKERS: usize = 1;
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
113pub(crate) enum GpuExecutionJobClass {
114 Lifecycle,
116 Execution,
118}
119
120impl GpuExecutionJobClass {
121 pub const fn as_str(self) -> &'static str {
123 match self {
124 Self::Lifecycle => "root",
125 Self::Execution => "auxiliary",
126 }
127 }
128}
129
130impl GpuExecutionJobKind {
131 pub const fn as_str(self) -> &'static str {
133 match self {
134 Self::ContextSetup => "context_setup",
135 Self::QueryExecution => "query_execution",
136 Self::AttemptWork => "attempt_work",
137 Self::Cleanup => "cleanup",
138 }
139 }
140
141 pub(crate) const fn job_class(self) -> GpuExecutionJobClass {
149 match self {
150 Self::ContextSetup | Self::QueryExecution | Self::Cleanup => GpuExecutionJobClass::Lifecycle,
151 Self::AttemptWork => GpuExecutionJobClass::Execution,
152 }
153 }
154
155 pub(crate) const fn runs_outside_lifecycle_reserve(self) -> bool {
158 matches!(self, Self::QueryExecution | Self::AttemptWork)
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
164pub struct GpuExecutionPoolSnapshot {
165 pub pool_id: u64,
170 pub device_ordinal: usize,
172 pub worker_count: usize,
174 pub active_jobs: usize,
176 pub(crate) peak_active_jobs: usize,
178 pub queued_jobs: usize,
180 pub(crate) peak_queued_jobs: usize,
182 pub active_query_jobs: usize,
184 pub peak_active_query_jobs: usize,
186 pub(crate) root_job_capacity: usize,
188 pub(crate) aux_job_capacity: usize,
190 pub(crate) root_jobs_available: usize,
192 pub(crate) aux_jobs_available: usize,
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
198pub struct GpuExecutionQuerySnapshot {
199 pub(crate) active_query_jobs: usize,
201 pub peak_active_query_jobs: usize,
203}
204
205#[derive(Debug, Clone, Default)]
212pub(crate) struct GpuExecutionQueryTracker {
213 counters: Arc<QueryCounters>,
214}
215
216impl GpuExecutionQueryTracker {
217 pub(crate) fn new() -> Self {
218 Self::default()
219 }
220
221 pub(crate) fn snapshot(&self) -> GpuExecutionQuerySnapshot {
223 let active_query_jobs = self.counters.active.load(Ordering::Acquire);
224 let peak_active_query_jobs = self.counters.peak.load(Ordering::Acquire).max(active_query_jobs);
225 GpuExecutionQuerySnapshot { active_query_jobs, peak_active_query_jobs }
226 }
227
228 fn start_query(&self) {
229 increment_with_peak(&self.counters.active, &self.counters.peak);
230 }
231
232 fn finish_query(&self) {
233 decrement(&self.counters.active);
234 }
235}
236
237#[derive(Clone)]
244pub(crate) struct GpuExecutionPool {
245 inner: Arc<GpuExecutionPoolInner>,
246 resource_epoch: Option<Arc<QueryServiceResourceEpoch>>,
251}
252
253impl GpuExecutionPool {
254 #[cfg(any(test, feature = "test-hooks"))]
257 pub(crate) fn enter_runtime(&self) -> EnterGuard<'_> {
258 self.inner.handle.enter()
259 }
260
261 #[cfg(any(test, feature = "test-hooks"))]
263 pub(crate) fn new(device_ordinal: usize, worker_count: usize) -> Result<Self> {
264 Self::new_with_tracker(device_ordinal, worker_count, GpuExecutionQueryTracker::new())
265 }
266
267 #[cfg(any(test, feature = "test-hooks"))]
274 pub(crate) fn new_with_tracker(
275 device_ordinal: usize,
276 worker_count: usize,
277 query_tracker: GpuExecutionQueryTracker,
278 ) -> Result<Self> {
279 Self::new_with_tracker_and_classes(device_ordinal, worker_count, worker_count, worker_count, query_tracker)
280 }
281
282 pub(crate) fn new_with_tracker_and_classes(
284 device_ordinal: usize,
285 worker_count: usize,
286 lifecycle_job_capacity: usize,
287 execution_job_capacity: usize,
288 query_tracker: GpuExecutionQueryTracker,
289 ) -> Result<Self> {
290 validate_worker_count(device_ordinal, worker_count)?;
291 for (field, capacity) in
292 [("root_job_capacity", lifecycle_job_capacity), ("aux_job_capacity", execution_job_capacity)]
293 {
294 if capacity == 0 {
295 return Err(QueryError::new(
296 QueryErrorCode::InvalidConfiguration,
297 "query_engine.gpu.execution_pool",
298 "GPU execution pool job-class capacity must be greater than zero",
299 )
300 .with_fact("device_ordinal", device_ordinal)
301 .with_fact("field", field)
302 .with_fact("reason", "zero"));
303 }
304 }
305
306 let id = NEXT_POOL_ID.fetch_add(1, Ordering::Relaxed);
307 let next_worker_index = Arc::new(AtomicUsize::new(0));
308 let (handle, supervisor) = RuntimeSupervisor::start(device_ordinal, worker_count, id, next_worker_index)?;
309
310 let inner = Arc::new(GpuExecutionPoolInner {
311 id,
312 device_ordinal,
313 worker_count,
314 handle,
315 supervisor: Arc::clone(&supervisor),
316 submissions_closed: AtomicBool::new(false),
317 submission_gate: Mutex::new(()),
318 concurrency: Arc::new(Semaphore::new(worker_count)),
319 execution_workers: Arc::new(Semaphore::new(worker_count - RESERVED_LIFECYCLE_WORKERS)),
320 lifecycle_job_permits: Arc::new(Semaphore::new(lifecycle_job_capacity)),
321 execution_job_permits: Arc::new(Semaphore::new(execution_job_capacity)),
322 release_cleanup_permits: Arc::new(Semaphore::new(lifecycle_job_capacity)),
329 lifecycle_job_capacity,
330 execution_job_capacity,
331 counters: Arc::new(PoolCounters::default()),
332 drain: Arc::new(PoolDrain::default()),
333 query_tracker,
334 next_job_id: AtomicU64::new(1),
335 });
336 supervisor.register_process_exit(Arc::downgrade(&inner));
337 Ok(Self { inner, resource_epoch: None })
338 }
339
340 #[cfg(test)]
341 fn class_semaphore(&self, kind: GpuExecutionJobKind) -> &Arc<Semaphore> {
342 self.class_semaphore_for_class(kind.job_class())
343 }
344
345 fn class_semaphore_for_class(&self, class: GpuExecutionJobClass) -> &Arc<Semaphore> {
346 match class {
347 GpuExecutionJobClass::Lifecycle => &self.inner.lifecycle_job_permits,
348 GpuExecutionJobClass::Execution => &self.inner.execution_job_permits,
349 }
350 }
351
352 #[cfg(test)]
353 fn class_saturated_error(&self, kind: GpuExecutionJobKind) -> QueryError {
354 self.class_saturated_error_for_class(kind, kind.job_class())
355 }
356
357 fn class_saturated_error_for_class(&self, kind: GpuExecutionJobKind, class: GpuExecutionJobClass) -> QueryError {
358 QueryError::new(
359 QueryErrorCode::ServiceOverloaded,
360 "query_engine.gpu.execution_pool",
361 "GPU execution pool job-class capacity is saturated",
362 )
363 .with_fact("device_ordinal", self.inner.device_ordinal)
364 .with_fact("job_kind", kind.as_str())
365 .with_fact("job_class", class.as_str())
366 .with_fact("reason", "gpu_execution_pool_class_saturated")
367 }
368
369 fn release_cleanup_saturated_error(&self) -> QueryError {
370 QueryError::new(
371 QueryErrorCode::ServiceOverloaded,
372 "query_engine.gpu.execution_pool",
373 "GPU execution pool bounded release-cleanup capacity is saturated",
374 )
375 .with_fact("device_ordinal", self.inner.device_ordinal)
376 .with_fact("job_kind", GpuExecutionJobKind::Cleanup.as_str())
377 .with_fact("job_class", "release_cleanup")
378 .with_fact("reason", "gpu_execution_pool_release_cleanup_saturated")
379 }
380
381 #[cfg(any(test, feature = "test-hooks"))]
388 pub(crate) fn shared_for_device(device_ordinal: usize, worker_count: usize) -> Result<Self> {
389 validate_worker_count(device_ordinal, worker_count)?;
390
391 static SHARED_POOLS: OnceLock<Mutex<HashMap<usize, Weak<GpuExecutionPoolInner>>>> = OnceLock::new();
392 let shared_pools = SHARED_POOLS.get_or_init(|| Mutex::new(HashMap::new()));
393 let mut shared_pools = shared_pools.lock().map_err(|_error| {
394 QueryError::new(
395 QueryErrorCode::Internal,
396 SHARED_OPERATION,
397 "GPU execution pool registry mutex was poisoned",
398 )
399 .with_fact("device_ordinal", device_ordinal)
400 .with_fact("worker_count", worker_count)
401 .with_fact("reason", "registry_poisoned")
402 })?;
403
404 if let Some(inner) = shared_pools.get(&device_ordinal).and_then(Weak::upgrade) {
405 if inner.worker_count != worker_count {
406 return Err(QueryError::new(
407 QueryErrorCode::InvalidPlan,
408 SHARED_OPERATION,
409 "CUDA device already has a shared GPU execution pool with a different worker count",
410 )
411 .with_fact("device_ordinal", device_ordinal)
412 .with_fact("existing_worker_count", inner.worker_count)
413 .with_fact("requested_worker_count", worker_count)
414 .with_fact("reason", "worker_count_conflict"));
415 }
416 return Ok(Self { inner, resource_epoch: None });
417 }
418
419 let pool = Self::new(device_ordinal, worker_count)?;
420 shared_pools.insert(device_ordinal, Arc::downgrade(&pool.inner));
421 Ok(pool)
422 }
423
424 pub async fn run<F, T>(&self, kind: GpuExecutionJobKind, future: F) -> Result<T>
431 where
432 F: Future<Output = T> + Send + 'static,
433 T: Send + 'static,
434 {
435 self.run_observed(kind, RuntimeObservationHandle::disabled(), future).await
436 }
437
438 pub(crate) async fn run_observed<F, T>(
443 &self,
444 kind: GpuExecutionJobKind,
445 observation: RuntimeObservationHandle,
446 future: F,
447 ) -> Result<T>
448 where
449 F: Future<Output = T> + Send + 'static,
450 T: Send + 'static,
451 {
452 self.clone().run_observed_scoped(kind, observation, move |_scope| future).await
453 }
454
455 pub fn run_observed_scoped<B, F, T>(
458 self,
459 kind: GpuExecutionJobKind,
460 observation: RuntimeObservationHandle,
461 build_future: B,
462 ) -> impl Future<Output = Result<T>> + Send + 'static
463 where
464 B: FnOnce(RuntimeStreamLeaseScope) -> F + Send + 'static,
465 F: Future<Output = T> + Send + 'static,
466 T: Send + 'static,
467 {
468 self.run_observed_scoped_in_class(kind, kind.job_class(), observation, build_future)
469 }
470
471 pub(crate) async fn run_observed_scoped_in_class<B, F, T>(
479 self,
480 kind: GpuExecutionJobKind,
481 class: GpuExecutionJobClass,
482 observation: RuntimeObservationHandle,
483 build_future: B,
484 ) -> Result<T>
485 where
486 B: FnOnce(RuntimeStreamLeaseScope) -> F + Send + 'static,
487 F: Future<Output = T> + Send + 'static,
488 T: Send + 'static,
489 {
490 self.check_resource_epoch(RUN_OPERATION)?;
491 let class_permit = Arc::clone(self.class_semaphore_for_class(class))
497 .try_acquire_owned()
498 .map_err(|_error| self.class_saturated_error_for_class(kind, class))?;
499 self.run_observed_scoped_with_class_permit(kind, class_permit, observation, build_future).await
500 }
501
502 pub(crate) async fn wait_for_class_permit(
511 &self,
512 kind: GpuExecutionJobKind,
513 class: GpuExecutionJobClass,
514 cancellation: &ExecutionCancellation,
515 ) -> Result<OwnedSemaphorePermit> {
516 self.check_resource_epoch(RUN_OPERATION)?;
517 let semaphore = self.class_semaphore_for_class(class);
518 match Arc::clone(semaphore).try_acquire_owned() {
519 Ok(permit) => return Ok(permit),
520 Err(TryAcquireError::Closed) => return Err(self.class_saturated_error_for_class(kind, class)),
521 Err(TryAcquireError::NoPermits) => {},
522 }
523 tokio::select! {
524 biased;
525 permit = Arc::clone(self.class_semaphore_for_class(class)).acquire_owned() => {
526 permit.map_err(|_closed| self.class_saturated_error_for_class(kind, class))
527 },
528 error = cancellation.terminated(RUN_OPERATION) => Err(error),
529 }
530 }
531
532 pub(crate) async fn run_observed_scoped_with_class_permit<B, F, T>(
534 self,
535 kind: GpuExecutionJobKind,
536 class_permit: OwnedSemaphorePermit,
537 observation: RuntimeObservationHandle,
538 build_future: B,
539 ) -> Result<T>
540 where
541 B: FnOnce(RuntimeStreamLeaseScope) -> F + Send + 'static,
542 F: Future<Output = T> + Send + 'static,
543 T: Send + 'static,
544 {
545 let job = self.new_job_observation(kind, observation);
546 if current_pool_id() == Some(self.inner.id) {
552 let _class_permit = class_permit;
553 let worker_id = job.started(current_worker_index());
554 let mut terminal = JobTerminalGuard::inline(Arc::clone(&job));
555 let scope = RuntimeStreamLeaseScope::default().with_gpu_worker(job.job_id.clone(), worker_id);
556 return match AssertUnwindSafe(build_future(scope)).catch_unwind().await {
557 Ok(value) => {
558 terminal.terminal(GpuJobLifecycleState::Finished);
559 Ok(value)
560 },
561 Err(_panic) => {
562 terminal.terminal(GpuJobLifecycleState::Panicked);
563 Err(job_panic_error(self.inner.device_ordinal, kind))
564 },
565 };
566 }
567
568 let job_guard = JobCounterGuard::new(
574 kind,
575 Arc::clone(&self.inner.counters),
576 Arc::clone(&self.inner.drain),
577 self.inner.query_tracker.clone(),
578 Arc::clone(&job),
579 );
580 let seats = self.inner.worker_seats();
581 let resource_epoch = self.resource_epoch.clone();
582 let task = self.inner.spawn_task(async move {
583 let _resource_epoch = resource_epoch;
584 let _class_permit = class_permit;
585 let seat = seats.occupy(kind).await;
586 run_counted_job_scoped(job_guard, seat, build_future).await
587 })?;
588
589 match AbortOnDropJoinHandle::new(task, Arc::clone(&job)).await {
590 Ok(Ok(value)) => Ok(value),
591 Ok(Err(_panic)) => {
592 job.terminal(GpuJobLifecycleState::Panicked);
593 Err(job_panic_error(self.inner.device_ordinal, kind))
594 },
595 Err(error) if error.is_cancelled() => {
596 job.cancellation_requested();
597 job.terminal(GpuJobLifecycleState::Cancelled);
598 Err(job_cancelled_error(self.inner.device_ordinal, kind, error))
599 },
600 Err(error) => {
601 job.terminal(GpuJobLifecycleState::Failed { error_code: QueryErrorCode::Internal.to_string() });
602 Err(job_join_panic_error(self.inner.device_ordinal, kind, error))
603 },
604 }
605 }
606
607 #[cfg(test)]
613 pub(crate) fn spawn<F>(&self, kind: GpuExecutionJobKind, future: F) -> Result<()>
614 where F: Future<Output = ()> + Send + 'static {
615 self.spawn_observed(kind, RuntimeObservationHandle::disabled(), future)
616 }
617
618 #[cfg(test)]
619 pub(crate) fn spawn_observed<F>(
620 &self,
621 kind: GpuExecutionJobKind,
622 observation: RuntimeObservationHandle,
623 future: F,
624 ) -> Result<()>
625 where
626 F: Future<Output = ()> + Send + 'static,
627 {
628 self.check_resource_epoch(RUN_OPERATION)?;
629 let class_permit = Arc::clone(self.class_semaphore(kind))
630 .try_acquire_owned()
631 .map_err(|_error| self.class_saturated_error(kind))?;
632 let job = self.new_job_observation(kind, observation);
633 let job_guard = JobCounterGuard::new(
634 kind,
635 Arc::clone(&self.inner.counters),
636 Arc::clone(&self.inner.drain),
637 self.inner.query_tracker.clone(),
638 job,
639 );
640 let seats = self.inner.worker_seats();
641 let resource_epoch = self.resource_epoch.clone();
642 self.inner.spawn_task(async move {
643 let _resource_epoch = resource_epoch;
644 let _class_permit = class_permit;
645 let seat = seats.occupy(kind).await;
646 let _ = run_counted_job(job_guard, seat, future).await;
647 })?;
648 Ok(())
649 }
650
651 #[cfg(test)]
654 pub(crate) fn spawn_observed_with_completion<F, C>(
655 &self,
656 kind: GpuExecutionJobKind,
657 observation: RuntimeObservationHandle,
658 future: F,
659 completion: C,
660 ) -> Result<()>
661 where
662 F: Future<Output = ()> + Send + 'static,
663 C: FnOnce() + Send + 'static,
664 {
665 self.check_resource_epoch(RUN_OPERATION)?;
666 let class_permit = Arc::clone(self.class_semaphore(kind))
667 .try_acquire_owned()
668 .map_err(|_error| self.class_saturated_error(kind))?;
669 let job = self.new_job_observation(kind, observation);
670 let job_guard = JobCounterGuard::new_with_completion(
671 kind,
672 Arc::clone(&self.inner.counters),
673 Arc::clone(&self.inner.drain),
674 self.inner.query_tracker.clone(),
675 job,
676 Box::new(completion),
677 );
678 let seats = self.inner.worker_seats();
679 let resource_epoch = self.resource_epoch.clone();
680 self.inner.spawn_task(async move {
681 let _resource_epoch = resource_epoch;
682 let _class_permit = class_permit;
683 let seat = seats.occupy(kind).await;
684 let _ = run_counted_job(job_guard, seat, future).await;
685 })?;
686 Ok(())
687 }
688
689 pub(crate) fn spawn_release_cleanup_observed_with_completion<F, C>(
696 &self,
697 observation: RuntimeObservationHandle,
698 future: F,
699 completion: C,
700 ) -> Result<()>
701 where
702 F: Future<Output = ()> + Send + 'static,
703 C: FnOnce() + Send + 'static,
704 {
705 self.check_resource_epoch(RUN_OPERATION)?;
706 let release_permit = Arc::clone(&self.inner.release_cleanup_permits)
707 .try_acquire_owned()
708 .map_err(|_error| self.release_cleanup_saturated_error())?;
709 let kind = GpuExecutionJobKind::Cleanup;
710 let job = self.new_job_observation(kind, observation);
711 let job_guard = JobCounterGuard::new_with_completion(
712 kind,
713 Arc::clone(&self.inner.counters),
714 Arc::clone(&self.inner.drain),
715 self.inner.query_tracker.clone(),
716 job,
717 Box::new(completion),
718 );
719 let seats = self.inner.worker_seats();
720 let resource_epoch = self.resource_epoch.clone();
721 self.inner.spawn_task(async move {
722 let _resource_epoch = resource_epoch;
723 let _release_permit = release_permit;
724 let seat = seats.occupy(kind).await;
725 let _ = run_counted_job(job_guard, seat, future).await;
726 })?;
727 Ok(())
728 }
729
730 pub(crate) fn submit_blocking<F, T>(
747 &self,
748 kind: GpuExecutionJobKind,
749 observation: RuntimeObservationHandle,
750 work: F,
751 ) -> Result<PoolBlockingWork<T>>
752 where
753 F: FnOnce() -> T + Send + 'static,
754 T: Send + 'static,
755 {
756 self.submit_blocking_inner(kind, observation, work, || {})
757 }
758
759 pub(crate) fn submit_blocking_with_completion<F, T, C>(
763 &self,
764 kind: GpuExecutionJobKind,
765 observation: RuntimeObservationHandle,
766 work: F,
767 completion: C,
768 ) -> Result<PoolBlockingWork<T>>
769 where
770 F: FnOnce() -> T + Send + 'static,
771 T: Send + 'static,
772 C: FnOnce() + Send + 'static,
773 {
774 self.submit_blocking_inner(kind, observation, work, completion)
775 }
776
777 fn submit_blocking_inner<F, T, C>(
778 &self,
779 kind: GpuExecutionJobKind,
780 observation: RuntimeObservationHandle,
781 work: F,
782 completion: C,
783 ) -> Result<PoolBlockingWork<T>>
784 where
785 F: FnOnce() -> T + Send + 'static,
786 T: Send + 'static,
787 C: FnOnce() + Send + 'static,
788 {
789 self.check_resource_epoch(RUN_OPERATION)?;
790 let class = kind.job_class();
791 let class_permit = Arc::clone(self.class_semaphore_for_class(class))
792 .try_acquire_owned()
793 .map_err(|_error| self.class_saturated_error_for_class(kind, class))?;
794 let job = self.new_job_observation(kind, observation);
795 let guard = JobCounterGuard::new(
796 kind,
797 Arc::clone(&self.inner.counters),
798 Arc::clone(&self.inner.drain),
799 self.inner.query_tracker.clone(),
800 Arc::clone(&job),
801 );
802 let (sender, receiver) = mpsc::channel();
803 let device_ordinal = self.inner.device_ordinal;
804 let resource_epoch = self.resource_epoch.clone();
805 {
806 let _submission = self.inner.submission_gate.lock().map_err(|_| {
807 QueryError::new(QueryErrorCode::Internal, RUN_OPERATION, "GPU execution submission gate was poisoned")
808 .with_fact("device_ordinal", device_ordinal)
809 .with_fact("reason", "gpu_execution_pool_submission_gate_poisoned")
810 })?;
811 if self.inner.submissions_closed.load(Ordering::Acquire) {
812 return Err(pool_closed_error(device_ordinal));
813 }
814 let completion = BlockingCompletionNotify::new(completion);
815 self.inner.handle.spawn_blocking(move || {
816 let mut completion = completion;
817 let _resource_epoch = resource_epoch;
818 let mut guard = guard;
819 let _scope = guard.activate();
820 let outcome = match std::panic::catch_unwind(AssertUnwindSafe(work)) {
821 Ok(value) => {
822 guard.terminal.terminal(GpuJobLifecycleState::Finished);
823 BlockingWorkOutcome::Completed(value)
824 },
825 Err(_) => {
826 guard.terminal.terminal(GpuJobLifecycleState::Panicked);
827 BlockingWorkOutcome::Panicked
828 },
829 };
830 drop(class_permit);
835 drop(_resource_epoch);
836 drop(guard);
837 let _ = sender.send(outcome);
838 completion.notify();
839 });
840 }
841 Ok(PoolBlockingWork { receiver, job, device_ordinal, kind })
842 }
843
844 fn new_job_observation(
848 &self,
849 kind: GpuExecutionJobKind,
850 observation: RuntimeObservationHandle,
851 ) -> Arc<JobObservation> {
852 let sequence = self.inner.next_job_id.fetch_add(1, Ordering::Relaxed);
853 let job = Arc::new(JobObservation {
854 observation,
855 job_id: RuntimeJobId::new(format!("gpu-pool-{}-job-{sequence}", self.inner.id)),
856 job_kind: kind,
857 device_ordinal: u32::try_from(self.inner.device_ordinal).unwrap_or(u32::MAX),
858 lifecycle: AtomicU8::new(JOB_LIFECYCLE_PENDING),
859 });
860 job.enqueued();
861 job
862 }
863
864 pub(crate) fn snapshot(&self) -> GpuExecutionPoolSnapshot {
866 let counters = &self.inner.counters;
867 let active_jobs = counters.active.load(Ordering::Acquire);
868 let queued_jobs = counters.queued.load(Ordering::Acquire);
869 let active_query_jobs = counters.active_queries.load(Ordering::Acquire);
870 GpuExecutionPoolSnapshot {
871 pool_id: self.inner.id,
872 device_ordinal: self.inner.device_ordinal,
873 worker_count: self.inner.worker_count,
874 root_job_capacity: self.inner.lifecycle_job_capacity,
875 aux_job_capacity: self.inner.execution_job_capacity,
876 root_jobs_available: self.inner.lifecycle_job_permits.available_permits(),
877 aux_jobs_available: self.inner.execution_job_permits.available_permits(),
878 active_jobs,
879 peak_active_jobs: counters.peak_active.load(Ordering::Acquire).max(active_jobs),
880 queued_jobs,
881 peak_queued_jobs: counters.peak_queued.load(Ordering::Acquire).max(queued_jobs),
882 active_query_jobs,
883 peak_active_query_jobs: counters.peak_active_queries.load(Ordering::Acquire).max(active_query_jobs),
884 }
885 }
886
887 #[cfg(test)]
889 pub(crate) fn query_tracker(&self) -> GpuExecutionQueryTracker {
890 self.inner.query_tracker.clone()
891 }
892
893 pub(crate) fn close_submissions(&self) -> Result<()> {
899 self.inner.close_submissions()
900 }
901
902 pub(crate) fn request_supervisor_exit(&self) -> Result<()> {
905 self.inner.request_supervisor_exit()
906 }
907
908 pub(crate) fn wait_for_drain_until(&self, deadline: Instant) -> Result<()> {
910 self.inner.wait_for_drain_until(deadline)
911 }
912
913 pub(crate) fn wait_for_supervisor_exit_until(&self, deadline: Instant) -> Result<()> {
915 self.inner.supervisor.wait_for_exit_until(deadline)
916 }
917
918 pub(crate) fn join_supervisor(&self) -> Result<()> {
921 self.inner.supervisor.join()
922 }
923
924 pub(crate) fn close_and_wait_until(&self, deadline: Instant) -> Result<()> {
927 self.close_submissions()?;
928 self.request_supervisor_exit()?;
929 let drain_result = self.wait_for_drain_until(deadline);
930 let exit_result = self.wait_for_supervisor_exit_until(deadline);
931 let exit_observed = match &exit_result {
932 Ok(()) => true,
933 Err(error) => matches!(
934 error.fact_value("reason"),
935 Some("runtime_supervisor_panicked" | "runtime_supervisor_build_failed")
936 ),
937 };
938 let join_result = if exit_observed { self.join_supervisor() } else { Ok(()) };
939
940 let mut first_deadline = None;
941 for result in [drain_result, exit_result, join_result] {
942 match result {
943 Ok(()) => {},
944 Err(error) if error.is_cleanup_deadline_elapsed() => {
945 if first_deadline.is_none() {
946 first_deadline = Some(error);
947 }
948 },
949 Err(error) => return Err(error),
950 }
951 }
952 first_deadline.map_or(Ok(()), Err)
953 }
954
955 fn close_and_join_for_process_exit(&self) {
959 let submission = self.inner.submission_gate.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
963 self.inner.submissions_closed.store(true, Ordering::Release);
964 drop(submission);
967 self.inner.supervisor.request_exit_from_drop();
968 let mut state = self.inner.drain.changed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
969 while self.inner.counters.active.load(Ordering::Acquire) != 0
970 || self.inner.counters.queued.load(Ordering::Acquire) != 0
971 {
972 state = self.inner.drain.wake.wait(state).unwrap_or_else(std::sync::PoisonError::into_inner);
973 }
974 drop(state);
977 self.inner.supervisor.join_for_process_exit();
978 }
979
980 pub(crate) fn with_resource_epoch(&self, resource_epoch: Arc<QueryServiceResourceEpoch>) -> Self {
985 Self { inner: Arc::clone(&self.inner), resource_epoch: Some(resource_epoch) }
986 }
987
988 fn check_resource_epoch(&self, operation: &'static str) -> Result<()> {
996 if self.inner.submissions_closed.load(Ordering::Acquire) {
997 return Err(QueryError::new(QueryErrorCode::Cancelled, operation, "GPU execution pool is closing")
998 .with_fact("device_ordinal", self.inner.device_ordinal)
999 .with_fact("reason", "gpu_execution_pool_closing"));
1000 }
1001 self.resource_epoch.as_ref().map_or(Ok(()), |epoch| epoch.check_open(operation))
1002 }
1003}
1004
1005impl std::fmt::Debug for GpuExecutionPool {
1006 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1007 formatter.debug_struct("GpuExecutionPool").field("snapshot", &self.snapshot()).finish()
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests;