use std::{
cell::Cell,
future::Future,
panic::AssertUnwindSafe,
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering},
mpsc,
},
time::Instant,
};
#[cfg(any(test, feature = "test-hooks"))]
use std::{
collections::HashMap,
sync::{OnceLock, Weak},
};
use futures::FutureExt;
use query_planning::error::{QueryError, QueryErrorCode, Result};
use serde::Serialize;
#[cfg(any(test, feature = "test-hooks"))]
use tokio::runtime::EnterGuard;
use tokio::sync::{OwnedSemaphorePermit, Semaphore, TryAcquireError};
use super::{ExecutionCancellation, context::QueryServiceResourceEpoch};
use crate::observability::{GpuJobLifecycleState, RuntimeJobId, RuntimeObservationHandle, RuntimeStreamLeaseScope};
mod inner;
mod job;
mod lease;
mod supervisor;
use inner::{GpuExecutionPoolInner, PoolDrain, pool_closed_error};
use job::{
AbortOnDropJoinHandle, JOB_LIFECYCLE_PENDING, JobCounterGuard, JobObservation, JobTerminalGuard, PoolCounters,
QueryCounters, current_pool_id, current_worker_index, decrement, increment_with_peak, job_cancelled_error,
job_join_panic_error, job_panic_error, run_counted_job, run_counted_job_scoped, validate_worker_count,
};
use lease::{BlockingCompletionNotify, BlockingWorkOutcome};
pub use lease::{GpuExecutorLease, PoolBlockingWork};
use supervisor::RuntimeSupervisor;
const BUILD_OPERATION: &str = "query_engine.gpu_execution_pool.build";
const RUN_OPERATION: &str = "query_engine.gpu_execution_pool.run";
#[cfg(any(test, feature = "test-hooks"))]
const SHARED_OPERATION: &str = "query_engine.gpu_execution_pool.shared";
const SHUTDOWN_OPERATION: &str = "query_engine.gpu_execution_pool.shutdown";
static NEXT_POOL_ID: AtomicU64 = AtomicU64::new(1);
thread_local! {
static CURRENT_POOL_ID: Cell<Option<u64>> = const { Cell::new(None) };
static CURRENT_WORKER_INDEX: Cell<Option<usize>> = const { Cell::new(None) };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GpuExecutionJobKind {
ContextSetup,
QueryExecution,
AttemptWork,
Cleanup,
}
pub(crate) const RESERVED_LIFECYCLE_WORKERS: usize = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum GpuExecutionJobClass {
Lifecycle,
Execution,
}
impl GpuExecutionJobClass {
pub const fn as_str(self) -> &'static str {
match self {
Self::Lifecycle => "root",
Self::Execution => "auxiliary",
}
}
}
impl GpuExecutionJobKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::ContextSetup => "context_setup",
Self::QueryExecution => "query_execution",
Self::AttemptWork => "attempt_work",
Self::Cleanup => "cleanup",
}
}
pub(crate) const fn job_class(self) -> GpuExecutionJobClass {
match self {
Self::ContextSetup | Self::QueryExecution | Self::Cleanup => GpuExecutionJobClass::Lifecycle,
Self::AttemptWork => GpuExecutionJobClass::Execution,
}
}
pub(crate) const fn runs_outside_lifecycle_reserve(self) -> bool {
matches!(self, Self::QueryExecution | Self::AttemptWork)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct GpuExecutionPoolSnapshot {
pub pool_id: u64,
pub device_ordinal: usize,
pub worker_count: usize,
pub active_jobs: usize,
pub(crate) peak_active_jobs: usize,
pub queued_jobs: usize,
pub(crate) peak_queued_jobs: usize,
pub active_query_jobs: usize,
pub peak_active_query_jobs: usize,
pub(crate) root_job_capacity: usize,
pub(crate) aux_job_capacity: usize,
pub(crate) root_jobs_available: usize,
pub(crate) aux_jobs_available: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct GpuExecutionQuerySnapshot {
pub(crate) active_query_jobs: usize,
pub peak_active_query_jobs: usize,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct GpuExecutionQueryTracker {
counters: Arc<QueryCounters>,
}
impl GpuExecutionQueryTracker {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn snapshot(&self) -> GpuExecutionQuerySnapshot {
let active_query_jobs = self.counters.active.load(Ordering::Acquire);
let peak_active_query_jobs = self.counters.peak.load(Ordering::Acquire).max(active_query_jobs);
GpuExecutionQuerySnapshot { active_query_jobs, peak_active_query_jobs }
}
fn start_query(&self) {
increment_with_peak(&self.counters.active, &self.counters.peak);
}
fn finish_query(&self) {
decrement(&self.counters.active);
}
}
#[derive(Clone)]
pub(crate) struct GpuExecutionPool {
inner: Arc<GpuExecutionPoolInner>,
resource_epoch: Option<Arc<QueryServiceResourceEpoch>>,
}
impl GpuExecutionPool {
#[cfg(any(test, feature = "test-hooks"))]
pub(crate) fn enter_runtime(&self) -> EnterGuard<'_> {
self.inner.handle.enter()
}
#[cfg(any(test, feature = "test-hooks"))]
pub(crate) fn new(device_ordinal: usize, worker_count: usize) -> Result<Self> {
Self::new_with_tracker(device_ordinal, worker_count, GpuExecutionQueryTracker::new())
}
#[cfg(any(test, feature = "test-hooks"))]
pub(crate) fn new_with_tracker(
device_ordinal: usize,
worker_count: usize,
query_tracker: GpuExecutionQueryTracker,
) -> Result<Self> {
Self::new_with_tracker_and_classes(device_ordinal, worker_count, worker_count, worker_count, query_tracker)
}
pub(crate) fn new_with_tracker_and_classes(
device_ordinal: usize,
worker_count: usize,
lifecycle_job_capacity: usize,
execution_job_capacity: usize,
query_tracker: GpuExecutionQueryTracker,
) -> Result<Self> {
validate_worker_count(device_ordinal, worker_count)?;
for (field, capacity) in
[("root_job_capacity", lifecycle_job_capacity), ("aux_job_capacity", execution_job_capacity)]
{
if capacity == 0 {
return Err(QueryError::new(
QueryErrorCode::InvalidConfiguration,
"query_engine.gpu.execution_pool",
"GPU execution pool job-class capacity must be greater than zero",
)
.with_fact("device_ordinal", device_ordinal)
.with_fact("field", field)
.with_fact("reason", "zero"));
}
}
let id = NEXT_POOL_ID.fetch_add(1, Ordering::Relaxed);
let next_worker_index = Arc::new(AtomicUsize::new(0));
let (handle, supervisor) = RuntimeSupervisor::start(device_ordinal, worker_count, id, next_worker_index)?;
let inner = Arc::new(GpuExecutionPoolInner {
id,
device_ordinal,
worker_count,
handle,
supervisor: Arc::clone(&supervisor),
submissions_closed: AtomicBool::new(false),
submission_gate: Mutex::new(()),
concurrency: Arc::new(Semaphore::new(worker_count)),
execution_workers: Arc::new(Semaphore::new(worker_count - RESERVED_LIFECYCLE_WORKERS)),
lifecycle_job_permits: Arc::new(Semaphore::new(lifecycle_job_capacity)),
execution_job_permits: Arc::new(Semaphore::new(execution_job_capacity)),
release_cleanup_permits: Arc::new(Semaphore::new(lifecycle_job_capacity)),
lifecycle_job_capacity,
execution_job_capacity,
counters: Arc::new(PoolCounters::default()),
drain: Arc::new(PoolDrain::default()),
query_tracker,
next_job_id: AtomicU64::new(1),
});
supervisor.register_process_exit(Arc::downgrade(&inner));
Ok(Self { inner, resource_epoch: None })
}
#[cfg(test)]
fn class_semaphore(&self, kind: GpuExecutionJobKind) -> &Arc<Semaphore> {
self.class_semaphore_for_class(kind.job_class())
}
fn class_semaphore_for_class(&self, class: GpuExecutionJobClass) -> &Arc<Semaphore> {
match class {
GpuExecutionJobClass::Lifecycle => &self.inner.lifecycle_job_permits,
GpuExecutionJobClass::Execution => &self.inner.execution_job_permits,
}
}
#[cfg(test)]
fn class_saturated_error(&self, kind: GpuExecutionJobKind) -> QueryError {
self.class_saturated_error_for_class(kind, kind.job_class())
}
fn class_saturated_error_for_class(&self, kind: GpuExecutionJobKind, class: GpuExecutionJobClass) -> QueryError {
QueryError::new(
QueryErrorCode::ServiceOverloaded,
"query_engine.gpu.execution_pool",
"GPU execution pool job-class capacity is saturated",
)
.with_fact("device_ordinal", self.inner.device_ordinal)
.with_fact("job_kind", kind.as_str())
.with_fact("job_class", class.as_str())
.with_fact("reason", "gpu_execution_pool_class_saturated")
}
fn release_cleanup_saturated_error(&self) -> QueryError {
QueryError::new(
QueryErrorCode::ServiceOverloaded,
"query_engine.gpu.execution_pool",
"GPU execution pool bounded release-cleanup capacity is saturated",
)
.with_fact("device_ordinal", self.inner.device_ordinal)
.with_fact("job_kind", GpuExecutionJobKind::Cleanup.as_str())
.with_fact("job_class", "release_cleanup")
.with_fact("reason", "gpu_execution_pool_release_cleanup_saturated")
}
#[cfg(any(test, feature = "test-hooks"))]
pub(crate) fn shared_for_device(device_ordinal: usize, worker_count: usize) -> Result<Self> {
validate_worker_count(device_ordinal, worker_count)?;
static SHARED_POOLS: OnceLock<Mutex<HashMap<usize, Weak<GpuExecutionPoolInner>>>> = OnceLock::new();
let shared_pools = SHARED_POOLS.get_or_init(|| Mutex::new(HashMap::new()));
let mut shared_pools = shared_pools.lock().map_err(|_error| {
QueryError::new(
QueryErrorCode::Internal,
SHARED_OPERATION,
"GPU execution pool registry mutex was poisoned",
)
.with_fact("device_ordinal", device_ordinal)
.with_fact("worker_count", worker_count)
.with_fact("reason", "registry_poisoned")
})?;
if let Some(inner) = shared_pools.get(&device_ordinal).and_then(Weak::upgrade) {
if inner.worker_count != worker_count {
return Err(QueryError::new(
QueryErrorCode::InvalidPlan,
SHARED_OPERATION,
"CUDA device already has a shared GPU execution pool with a different worker count",
)
.with_fact("device_ordinal", device_ordinal)
.with_fact("existing_worker_count", inner.worker_count)
.with_fact("requested_worker_count", worker_count)
.with_fact("reason", "worker_count_conflict"));
}
return Ok(Self { inner, resource_epoch: None });
}
let pool = Self::new(device_ordinal, worker_count)?;
shared_pools.insert(device_ordinal, Arc::downgrade(&pool.inner));
Ok(pool)
}
pub async fn run<F, T>(&self, kind: GpuExecutionJobKind, future: F) -> Result<T>
where
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
self.run_observed(kind, RuntimeObservationHandle::disabled(), future).await
}
pub(crate) async fn run_observed<F, T>(
&self,
kind: GpuExecutionJobKind,
observation: RuntimeObservationHandle,
future: F,
) -> Result<T>
where
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
self.clone().run_observed_scoped(kind, observation, move |_scope| future).await
}
pub fn run_observed_scoped<B, F, T>(
self,
kind: GpuExecutionJobKind,
observation: RuntimeObservationHandle,
build_future: B,
) -> impl Future<Output = Result<T>> + Send + 'static
where
B: FnOnce(RuntimeStreamLeaseScope) -> F + Send + 'static,
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
self.run_observed_scoped_in_class(kind, kind.job_class(), observation, build_future)
}
pub(crate) async fn run_observed_scoped_in_class<B, F, T>(
self,
kind: GpuExecutionJobKind,
class: GpuExecutionJobClass,
observation: RuntimeObservationHandle,
build_future: B,
) -> Result<T>
where
B: FnOnce(RuntimeStreamLeaseScope) -> F + Send + 'static,
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
self.check_resource_epoch(RUN_OPERATION)?;
let class_permit = Arc::clone(self.class_semaphore_for_class(class))
.try_acquire_owned()
.map_err(|_error| self.class_saturated_error_for_class(kind, class))?;
self.run_observed_scoped_with_class_permit(kind, class_permit, observation, build_future).await
}
pub(crate) async fn wait_for_class_permit(
&self,
kind: GpuExecutionJobKind,
class: GpuExecutionJobClass,
cancellation: &ExecutionCancellation,
) -> Result<OwnedSemaphorePermit> {
self.check_resource_epoch(RUN_OPERATION)?;
let semaphore = self.class_semaphore_for_class(class);
match Arc::clone(semaphore).try_acquire_owned() {
Ok(permit) => return Ok(permit),
Err(TryAcquireError::Closed) => return Err(self.class_saturated_error_for_class(kind, class)),
Err(TryAcquireError::NoPermits) => {},
}
tokio::select! {
biased;
permit = Arc::clone(self.class_semaphore_for_class(class)).acquire_owned() => {
permit.map_err(|_closed| self.class_saturated_error_for_class(kind, class))
},
error = cancellation.terminated(RUN_OPERATION) => Err(error),
}
}
pub(crate) async fn run_observed_scoped_with_class_permit<B, F, T>(
self,
kind: GpuExecutionJobKind,
class_permit: OwnedSemaphorePermit,
observation: RuntimeObservationHandle,
build_future: B,
) -> Result<T>
where
B: FnOnce(RuntimeStreamLeaseScope) -> F + Send + 'static,
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
let job = self.new_job_observation(kind, observation);
if current_pool_id() == Some(self.inner.id) {
let _class_permit = class_permit;
let worker_id = job.started(current_worker_index());
let mut terminal = JobTerminalGuard::inline(Arc::clone(&job));
let scope = RuntimeStreamLeaseScope::default().with_gpu_worker(job.job_id.clone(), worker_id);
return match AssertUnwindSafe(build_future(scope)).catch_unwind().await {
Ok(value) => {
terminal.terminal(GpuJobLifecycleState::Finished);
Ok(value)
},
Err(_panic) => {
terminal.terminal(GpuJobLifecycleState::Panicked);
Err(job_panic_error(self.inner.device_ordinal, kind))
},
};
}
let job_guard = JobCounterGuard::new(
kind,
Arc::clone(&self.inner.counters),
Arc::clone(&self.inner.drain),
self.inner.query_tracker.clone(),
Arc::clone(&job),
);
let seats = self.inner.worker_seats();
let resource_epoch = self.resource_epoch.clone();
let task = self.inner.spawn_task(async move {
let _resource_epoch = resource_epoch;
let _class_permit = class_permit;
let seat = seats.occupy(kind).await;
run_counted_job_scoped(job_guard, seat, build_future).await
})?;
match AbortOnDropJoinHandle::new(task, Arc::clone(&job)).await {
Ok(Ok(value)) => Ok(value),
Ok(Err(_panic)) => {
job.terminal(GpuJobLifecycleState::Panicked);
Err(job_panic_error(self.inner.device_ordinal, kind))
},
Err(error) if error.is_cancelled() => {
job.cancellation_requested();
job.terminal(GpuJobLifecycleState::Cancelled);
Err(job_cancelled_error(self.inner.device_ordinal, kind, error))
},
Err(error) => {
job.terminal(GpuJobLifecycleState::Failed { error_code: QueryErrorCode::Internal.to_string() });
Err(job_join_panic_error(self.inner.device_ordinal, kind, error))
},
}
}
#[cfg(test)]
pub(crate) fn spawn<F>(&self, kind: GpuExecutionJobKind, future: F) -> Result<()>
where F: Future<Output = ()> + Send + 'static {
self.spawn_observed(kind, RuntimeObservationHandle::disabled(), future)
}
#[cfg(test)]
pub(crate) fn spawn_observed<F>(
&self,
kind: GpuExecutionJobKind,
observation: RuntimeObservationHandle,
future: F,
) -> Result<()>
where
F: Future<Output = ()> + Send + 'static,
{
self.check_resource_epoch(RUN_OPERATION)?;
let class_permit = Arc::clone(self.class_semaphore(kind))
.try_acquire_owned()
.map_err(|_error| self.class_saturated_error(kind))?;
let job = self.new_job_observation(kind, observation);
let job_guard = JobCounterGuard::new(
kind,
Arc::clone(&self.inner.counters),
Arc::clone(&self.inner.drain),
self.inner.query_tracker.clone(),
job,
);
let seats = self.inner.worker_seats();
let resource_epoch = self.resource_epoch.clone();
self.inner.spawn_task(async move {
let _resource_epoch = resource_epoch;
let _class_permit = class_permit;
let seat = seats.occupy(kind).await;
let _ = run_counted_job(job_guard, seat, future).await;
})?;
Ok(())
}
#[cfg(test)]
pub(crate) fn spawn_observed_with_completion<F, C>(
&self,
kind: GpuExecutionJobKind,
observation: RuntimeObservationHandle,
future: F,
completion: C,
) -> Result<()>
where
F: Future<Output = ()> + Send + 'static,
C: FnOnce() + Send + 'static,
{
self.check_resource_epoch(RUN_OPERATION)?;
let class_permit = Arc::clone(self.class_semaphore(kind))
.try_acquire_owned()
.map_err(|_error| self.class_saturated_error(kind))?;
let job = self.new_job_observation(kind, observation);
let job_guard = JobCounterGuard::new_with_completion(
kind,
Arc::clone(&self.inner.counters),
Arc::clone(&self.inner.drain),
self.inner.query_tracker.clone(),
job,
Box::new(completion),
);
let seats = self.inner.worker_seats();
let resource_epoch = self.resource_epoch.clone();
self.inner.spawn_task(async move {
let _resource_epoch = resource_epoch;
let _class_permit = class_permit;
let seat = seats.occupy(kind).await;
let _ = run_counted_job(job_guard, seat, future).await;
})?;
Ok(())
}
pub(crate) fn spawn_release_cleanup_observed_with_completion<F, C>(
&self,
observation: RuntimeObservationHandle,
future: F,
completion: C,
) -> Result<()>
where
F: Future<Output = ()> + Send + 'static,
C: FnOnce() + Send + 'static,
{
self.check_resource_epoch(RUN_OPERATION)?;
let release_permit = Arc::clone(&self.inner.release_cleanup_permits)
.try_acquire_owned()
.map_err(|_error| self.release_cleanup_saturated_error())?;
let kind = GpuExecutionJobKind::Cleanup;
let job = self.new_job_observation(kind, observation);
let job_guard = JobCounterGuard::new_with_completion(
kind,
Arc::clone(&self.inner.counters),
Arc::clone(&self.inner.drain),
self.inner.query_tracker.clone(),
job,
Box::new(completion),
);
let seats = self.inner.worker_seats();
let resource_epoch = self.resource_epoch.clone();
self.inner.spawn_task(async move {
let _resource_epoch = resource_epoch;
let _release_permit = release_permit;
let seat = seats.occupy(kind).await;
let _ = run_counted_job(job_guard, seat, future).await;
})?;
Ok(())
}
pub(crate) fn submit_blocking<F, T>(
&self,
kind: GpuExecutionJobKind,
observation: RuntimeObservationHandle,
work: F,
) -> Result<PoolBlockingWork<T>>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
self.submit_blocking_inner(kind, observation, work, || {})
}
pub(crate) fn submit_blocking_with_completion<F, T, C>(
&self,
kind: GpuExecutionJobKind,
observation: RuntimeObservationHandle,
work: F,
completion: C,
) -> Result<PoolBlockingWork<T>>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
C: FnOnce() + Send + 'static,
{
self.submit_blocking_inner(kind, observation, work, completion)
}
fn submit_blocking_inner<F, T, C>(
&self,
kind: GpuExecutionJobKind,
observation: RuntimeObservationHandle,
work: F,
completion: C,
) -> Result<PoolBlockingWork<T>>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
C: FnOnce() + Send + 'static,
{
self.check_resource_epoch(RUN_OPERATION)?;
let class = kind.job_class();
let class_permit = Arc::clone(self.class_semaphore_for_class(class))
.try_acquire_owned()
.map_err(|_error| self.class_saturated_error_for_class(kind, class))?;
let job = self.new_job_observation(kind, observation);
let guard = JobCounterGuard::new(
kind,
Arc::clone(&self.inner.counters),
Arc::clone(&self.inner.drain),
self.inner.query_tracker.clone(),
Arc::clone(&job),
);
let (sender, receiver) = mpsc::channel();
let device_ordinal = self.inner.device_ordinal;
let resource_epoch = self.resource_epoch.clone();
{
let _submission = self.inner.submission_gate.lock().map_err(|_| {
QueryError::new(QueryErrorCode::Internal, RUN_OPERATION, "GPU execution submission gate was poisoned")
.with_fact("device_ordinal", device_ordinal)
.with_fact("reason", "gpu_execution_pool_submission_gate_poisoned")
})?;
if self.inner.submissions_closed.load(Ordering::Acquire) {
return Err(pool_closed_error(device_ordinal));
}
let completion = BlockingCompletionNotify::new(completion);
self.inner.handle.spawn_blocking(move || {
let mut completion = completion;
let _resource_epoch = resource_epoch;
let mut guard = guard;
let _scope = guard.activate();
let outcome = match std::panic::catch_unwind(AssertUnwindSafe(work)) {
Ok(value) => {
guard.terminal.terminal(GpuJobLifecycleState::Finished);
BlockingWorkOutcome::Completed(value)
},
Err(_) => {
guard.terminal.terminal(GpuJobLifecycleState::Panicked);
BlockingWorkOutcome::Panicked
},
};
drop(class_permit);
drop(_resource_epoch);
drop(guard);
let _ = sender.send(outcome);
completion.notify();
});
}
Ok(PoolBlockingWork { receiver, job, device_ordinal, kind })
}
fn new_job_observation(
&self,
kind: GpuExecutionJobKind,
observation: RuntimeObservationHandle,
) -> Arc<JobObservation> {
let sequence = self.inner.next_job_id.fetch_add(1, Ordering::Relaxed);
let job = Arc::new(JobObservation {
observation,
job_id: RuntimeJobId::new(format!("gpu-pool-{}-job-{sequence}", self.inner.id)),
job_kind: kind,
device_ordinal: u32::try_from(self.inner.device_ordinal).unwrap_or(u32::MAX),
lifecycle: AtomicU8::new(JOB_LIFECYCLE_PENDING),
});
job.enqueued();
job
}
pub(crate) fn snapshot(&self) -> GpuExecutionPoolSnapshot {
let counters = &self.inner.counters;
let active_jobs = counters.active.load(Ordering::Acquire);
let queued_jobs = counters.queued.load(Ordering::Acquire);
let active_query_jobs = counters.active_queries.load(Ordering::Acquire);
GpuExecutionPoolSnapshot {
pool_id: self.inner.id,
device_ordinal: self.inner.device_ordinal,
worker_count: self.inner.worker_count,
root_job_capacity: self.inner.lifecycle_job_capacity,
aux_job_capacity: self.inner.execution_job_capacity,
root_jobs_available: self.inner.lifecycle_job_permits.available_permits(),
aux_jobs_available: self.inner.execution_job_permits.available_permits(),
active_jobs,
peak_active_jobs: counters.peak_active.load(Ordering::Acquire).max(active_jobs),
queued_jobs,
peak_queued_jobs: counters.peak_queued.load(Ordering::Acquire).max(queued_jobs),
active_query_jobs,
peak_active_query_jobs: counters.peak_active_queries.load(Ordering::Acquire).max(active_query_jobs),
}
}
#[cfg(test)]
pub(crate) fn query_tracker(&self) -> GpuExecutionQueryTracker {
self.inner.query_tracker.clone()
}
pub(crate) fn close_submissions(&self) -> Result<()> {
self.inner.close_submissions()
}
pub(crate) fn request_supervisor_exit(&self) -> Result<()> {
self.inner.request_supervisor_exit()
}
pub(crate) fn wait_for_drain_until(&self, deadline: Instant) -> Result<()> {
self.inner.wait_for_drain_until(deadline)
}
pub(crate) fn wait_for_supervisor_exit_until(&self, deadline: Instant) -> Result<()> {
self.inner.supervisor.wait_for_exit_until(deadline)
}
pub(crate) fn join_supervisor(&self) -> Result<()> {
self.inner.supervisor.join()
}
pub(crate) fn close_and_wait_until(&self, deadline: Instant) -> Result<()> {
self.close_submissions()?;
self.request_supervisor_exit()?;
let drain_result = self.wait_for_drain_until(deadline);
let exit_result = self.wait_for_supervisor_exit_until(deadline);
let exit_observed = match &exit_result {
Ok(()) => true,
Err(error) => matches!(
error.fact_value("reason"),
Some("runtime_supervisor_panicked" | "runtime_supervisor_build_failed")
),
};
let join_result = if exit_observed { self.join_supervisor() } else { Ok(()) };
let mut first_deadline = None;
for result in [drain_result, exit_result, join_result] {
match result {
Ok(()) => {},
Err(error) if error.is_cleanup_deadline_elapsed() => {
if first_deadline.is_none() {
first_deadline = Some(error);
}
},
Err(error) => return Err(error),
}
}
first_deadline.map_or(Ok(()), Err)
}
fn close_and_join_for_process_exit(&self) {
let submission = self.inner.submission_gate.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
self.inner.submissions_closed.store(true, Ordering::Release);
drop(submission);
self.inner.supervisor.request_exit_from_drop();
let mut state = self.inner.drain.changed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
while self.inner.counters.active.load(Ordering::Acquire) != 0
|| self.inner.counters.queued.load(Ordering::Acquire) != 0
{
state = self.inner.drain.wake.wait(state).unwrap_or_else(std::sync::PoisonError::into_inner);
}
drop(state);
self.inner.supervisor.join_for_process_exit();
}
pub(crate) fn with_resource_epoch(&self, resource_epoch: Arc<QueryServiceResourceEpoch>) -> Self {
Self { inner: Arc::clone(&self.inner), resource_epoch: Some(resource_epoch) }
}
fn check_resource_epoch(&self, operation: &'static str) -> Result<()> {
if self.inner.submissions_closed.load(Ordering::Acquire) {
return Err(QueryError::new(QueryErrorCode::Cancelled, operation, "GPU execution pool is closing")
.with_fact("device_ordinal", self.inner.device_ordinal)
.with_fact("reason", "gpu_execution_pool_closing"));
}
self.resource_epoch.as_ref().map_or(Ok(()), |epoch| epoch.check_open(operation))
}
}
impl std::fmt::Debug for GpuExecutionPool {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.debug_struct("GpuExecutionPool").field("snapshot", &self.snapshot()).finish()
}
}
#[cfg(test)]
mod tests;