1use std::sync::Arc;
20
21use arrow::datatypes::SchemaRef;
22use cudf::{DeviceId, Error as CudfError, Runtime as CudfRuntime};
23use query_planning::{
24 admission::AttemptCapabilityEvidence,
25 capability::AggregateStrategyExecutionOptions,
26 chunk::SourceChunkPlanningOptions,
27 correlation::{QueryId, TraceId},
28 error::{QueryError, QueryErrorCode, Result},
29 execution_contracts::{MemoryControlPolicy, SourceChunkPlanningMode},
30 mode::StatisticalAggregateExecutionMode,
31 plan::PlanFingerprint,
32 policy::PlanningPolicy,
33};
34
35use super::{
36 GpuExecutionPool, GpuExecutorLease, MemoryGrant, ServiceStreamPool, SourceSchedulerPolicy,
37 attempt_domain::AttemptStreamProviderRoute,
38 cache::local_parquet::GpuLocalParquetSourceCache,
39 gpu::{GrantIdentity, ledger::ResidentOwnerLease},
40 io_lifecycle::{IoLifecycle, IoLifecycleLease},
41 resident::{GpuResidentInputMap, GpuResultHandle},
42 service_stream::StreamCleanupOutcome,
43};
44#[cfg(feature = "cugraph")]
45use super::{allocator_correlation::CacheScopeCorrelationGuard, cache::resident_graph::GpuResidentGraphCache};
46use crate::observability::{
47 AttemptId, PlanExecutionId, RuntimeExecutionObservationScope, RuntimeObservationHandle,
48 RuntimePlanExecutionLifecycle, RuntimeQueryLifecycle, RuntimeStreamLeaseScope, RuntimeTerminalOutcome,
49 SealedRuntimeObservationSnapshot,
50};
51
52mod admitted;
53mod auxiliary_slots;
54mod cancellation;
55mod epoch;
56mod lakehouse;
57mod leases;
58mod scope;
59#[cfg(any(test, feature = "test-hooks"))]
60mod standalone;
61mod stream_pool;
62
63pub(in crate::runtime) use admitted::AdmittedPlanIdentities;
64pub(crate) use auxiliary_slots::{AttemptAuxiliaryPermit, AttemptAuxiliarySlots};
65pub use cancellation::ExecutionCancellation;
66pub(crate) use epoch::{
67 QueryResourceAnchor, QueryResourceEpoch, QueryServiceEpoch, QueryServiceEpochUser, QueryServiceResourceEpoch,
68};
69use scope::QueryScope;
70pub(in crate::runtime) use scope::{AdmittedAttempt, DeviceRuntime, PlanScope, StreamBinding};
71#[cfg(any(test, feature = "test-hooks"))]
72pub use standalone::{RuntimeObservationBuildError, RuntimeObservedExecutionContext, StandaloneConfig};
73pub(crate) use stream_pool::QueryServiceStreamPool;
74
75#[derive(Debug, Clone)]
83pub struct ExecutionContext {
84 plan: Arc<PlanScope>,
89 query: QueryScope,
90 stream: Arc<StreamBinding>,
91 device: Arc<DeviceRuntime>,
92}
93
94#[derive(Debug)]
99pub(super) struct ActivatedDeferredResidentPhase {
100 consumer_execution_scope_fingerprint: PlanFingerprint,
101 inputs: Vec<ActivatedDeferredResidentInput>,
102 capability_evidence: AttemptCapabilityEvidence,
103 grant_identity: GrantIdentity,
104}
105
106#[derive(Debug)]
110pub(super) struct ActivatedDeferredResidentInput {
111 input_name: String,
112 facts: DeferredResidentFacts,
113 memory_identity: cudf::DataFrameMemoryIdentity,
114}
115
116#[derive(Debug, PartialEq)]
119pub(super) struct DeferredResidentFacts {
120 producer_execution_scope_fingerprint: PlanFingerprint,
121 schema: SchemaRef,
122 row_count: u64,
123}
124
125impl DeferredResidentFacts {
126 pub(super) fn read(input_name: &str, input: &GpuResultHandle) -> Result<Self> {
127 let producer_execution_scope_fingerprint = input.producer_execution_scope().ok_or_else(|| {
128 QueryError::new(
129 QueryErrorCode::InternalInvariantViolation,
130 "query_engine.runtime.context",
131 "an admitted deferred resident occurrence has no producer scope",
132 )
133 .with_fact("reason", "deferred_resident_input_scope_missing")
134 .with_fact("input_name", input_name)
135 })?;
136 let row_count = u64::try_from(input.frame().num_rows()).map_err(|_| {
137 QueryError::new(
138 QueryErrorCode::SizeOverflow,
139 "query_engine.runtime.context",
140 "the resident input row count exceeds the admission integer range",
141 )
142 .with_fact("reason", "deferred_resident_input_row_count_overflow")
143 .with_fact("input_name", input_name)
144 })?;
145 Ok(Self { producer_execution_scope_fingerprint, schema: Arc::clone(input.schema()), row_count })
146 }
147
148 pub(super) fn producer_execution_scope_fingerprint(&self) -> PlanFingerprint {
149 self.producer_execution_scope_fingerprint
150 }
151
152 pub(super) fn schema(&self) -> &SchemaRef {
153 &self.schema
154 }
155}
156
157impl ActivatedDeferredResidentPhase {
158 fn validate_inputs(&self, inputs: &GpuResidentInputMap, grant: &MemoryGrant) -> Result<()> {
159 for activated in &self.inputs {
160 let input = inputs.get(&activated.input_name).ok_or_else(|| {
161 QueryError::new(
162 QueryErrorCode::InternalInvariantViolation,
163 "query_engine.runtime.context",
164 "the activated deferred resident input is missing",
165 )
166 .with_fact("reason", "deferred_resident_input_missing")
167 .with_fact("input_name", &activated.input_name)
168 })?;
169 activated.validate_facts(input)?;
170 activated.validate_binding(input, grant, self.grant_identity)?;
171 }
172 Ok(())
173 }
174}
175
176impl ActivatedDeferredResidentInput {
177 pub(super) fn bind(
180 input_name: String,
181 facts: DeferredResidentFacts,
182 input: &GpuResultHandle,
183 grant: &MemoryGrant,
184 ) -> Result<Self> {
185 validate_grant_retention(&input_name, input, grant)?;
186 Ok(Self { input_name, facts, memory_identity: input.frame().memory_identity() })
187 }
188
189 fn validate_facts(&self, input: &GpuResultHandle) -> Result<()> {
190 let facts = DeferredResidentFacts::read(&self.input_name, input)?;
191 if facts != self.facts {
192 return Err(self
193 .identity_mismatch()
194 .with_fact("input_row_count", facts.row_count)
195 .with_fact("activated_row_count", self.facts.row_count));
196 }
197 Ok(())
198 }
199
200 fn validate_binding(&self, input: &GpuResultHandle, grant: &MemoryGrant, phase_grant: GrantIdentity) -> Result<()> {
201 validate_grant_retention(&self.input_name, input, grant)?;
202 if phase_grant != grant.identity() || input.frame().memory_identity() != self.memory_identity {
203 return Err(self.identity_mismatch());
204 }
205 Ok(())
206 }
207
208 fn identity_mismatch(&self) -> QueryError {
209 QueryError::new(
210 QueryErrorCode::InternalInvariantViolation,
211 "query_engine.runtime.context",
212 "the GPU-resident graph input differs from the frame that activated its admitted phase",
213 )
214 .with_fact("reason", "deferred_graph_input_identity_mismatch")
215 .with_fact("input_name", &self.input_name)
216 }
217}
218
219fn validate_grant_retention(input_name: &str, input: &GpuResultHandle, grant: &MemoryGrant) -> Result<()> {
222 let physical_bytes = input.device_allocated_size_bytes()?;
223 if physical_bytes > 0 && input.retained_owner_identity_for(grant).is_none() {
224 return Err(QueryError::new(
225 QueryErrorCode::InternalInvariantViolation,
226 "query_engine.runtime.context",
227 "a deferred resident input is not retained by the admitted grant",
228 )
229 .with_fact("reason", "deferred_resident_grant_owner_mismatch")
230 .with_fact("input_name", input_name)
231 .with_fact("input_device_bytes", physical_bytes));
232 }
233 Ok(())
234}
235
236#[derive(Debug)]
241pub struct RuntimeExecutionObservationLifecycle {
242 inner: RuntimeExecutionObservationLifecycleInner,
243}
244
245#[derive(Debug)]
246enum RuntimeExecutionObservationLifecycleInner {
247 Query(RuntimeQueryLifecycle),
248 Plan(RuntimePlanExecutionLifecycle),
249}
250
251impl RuntimeExecutionObservationLifecycle {
252 fn for_context(context: &ExecutionContext, plan_execution_id: PlanExecutionId) -> Self {
253 let inner = match &context.query {
256 QueryScope::Admitted(admitted) => RuntimeExecutionObservationLifecycleInner::Plan(
257 RuntimePlanExecutionLifecycle::from_attempt(&admitted.attempt, plan_execution_id),
258 ),
259 QueryScope::Standalone(query) => match &query.observation_owner {
260 Some(owner) => RuntimeExecutionObservationLifecycleInner::Query(RuntimeQueryLifecycle::from_owner(
261 owner.clone(),
262 plan_execution_id,
263 )),
264 None => RuntimeExecutionObservationLifecycleInner::Plan(RuntimePlanExecutionLifecycle::from_handle(
265 context.plan.runtime_observation.clone(),
266 plan_execution_id,
267 )),
268 },
269 };
270 Self { inner }
271 }
272
273 pub fn execution_scope(&self) -> RuntimeExecutionObservationScope {
275 match &self.inner {
276 RuntimeExecutionObservationLifecycleInner::Query(lifecycle) => lifecycle.execution_scope(),
277 RuntimeExecutionObservationLifecycleInner::Plan(lifecycle) => lifecycle.execution_scope(),
278 }
279 }
280
281 pub fn finish(self, outcome: RuntimeTerminalOutcome) -> Option<SealedRuntimeObservationSnapshot> {
285 match self.inner {
286 RuntimeExecutionObservationLifecycleInner::Query(lifecycle) => lifecycle.finish(outcome),
287 RuntimeExecutionObservationLifecycleInner::Plan(lifecycle) => {
288 lifecycle.finish(outcome);
289 None
290 },
291 }
292 }
293}
294
295impl ExecutionContext {
296 pub(in crate::runtime) fn admitted_root(
298 device: Arc<DeviceRuntime>,
299 attempt: AdmittedAttempt,
300 stream: StreamBinding,
301 plan: PlanScope,
302 ) -> Self {
303 Self { plan: Arc::new(plan), query: QueryScope::Admitted(Arc::new(attempt)), stream: Arc::new(stream), device }
304 }
305
306 fn admitted(&self) -> Option<&AdmittedAttempt> {
307 match &self.query {
308 QueryScope::Admitted(attempt) => Some(attempt),
309 QueryScope::Standalone(_) => None,
310 }
311 }
312
313 fn with_plan(&self, update: impl FnOnce(&mut PlanScope)) -> Self {
315 let mut plan = PlanScope::clone(&self.plan);
316 update(&mut plan);
317 Self { plan: Arc::new(plan), ..self.clone() }
318 }
319
320 fn with_stream(&self, stream: StreamBinding) -> Self {
322 Self { stream: Arc::new(stream), ..self.clone() }
323 }
324
325 pub(super) fn with_query_resource_anchor(&self, anchor: Arc<QueryResourceAnchor>) -> Self {
327 debug_assert!(
328 self.admitted().is_some_and(|attempt| anchor.belongs_to(&attempt.query_epoch)),
329 "query resource anchor must belong to the context query epoch"
330 );
331 self.with_plan(|plan| plan.query_anchor = Some(anchor))
332 }
333
334 fn acquire_resource_epoch(&self, operation: &'static str) -> Result<Option<Arc<QueryServiceResourceEpoch>>> {
339 let Some(attempt) = self.admitted() else {
340 return Ok(None);
341 };
342 QueryServiceResourceEpoch::acquire(&attempt.service_epoch, self.plan.query_anchor.as_ref(), operation).map(Some)
343 }
344
345 pub fn runtime(&self) -> Result<CudfRuntime> {
347 let operation = "query_engine.runtime.context.runtime";
348 drop(self.acquire_resource_epoch(operation)?);
354 Ok(self.stream.runtime().as_ref().clone())
355 }
356
357 pub fn acquire_io_lease(&self, operation: &'static str) -> Result<Option<IoLifecycleLease>> {
363 self.plan.io_lifecycle.as_ref().map(|lifecycle| lifecycle.acquire(operation)).transpose()
364 }
365
366 pub fn with_io_lifecycle(self, lifecycle: IoLifecycle) -> Self {
368 self.with_plan(|plan| plan.io_lifecycle = Some(lifecycle))
369 }
370
371 #[doc(hidden)]
382 pub fn synchronize_stream(&self) -> std::result::Result<(), CudfError> {
383 self.stream.runtime().synchronize()
384 }
385
386 pub fn shared_runtime(&self) -> Result<Arc<CudfRuntime>> {
391 self.runtime().map(Arc::new)
392 }
393
394 pub fn gpu_executor(&self) -> Result<GpuExecutorLease> {
405 self.gpu_execution_pool().map(if matches!(*self.stream, StreamBinding::AttemptWork { owned: Some(_), .. }) {
406 GpuExecutorLease::from_pool_for_auxiliary_fragment
407 } else {
408 GpuExecutorLease::from_pool
409 })
410 }
411
412 pub(crate) fn gpu_execution_pool(&self) -> Result<GpuExecutionPool> {
417 let resource_epoch = self.acquire_resource_epoch("query_engine.runtime.context.gpu_execution_pool")?;
418 Ok(match resource_epoch {
419 Some(resource_epoch) => self.device.gpu_execution_pool.with_resource_epoch(resource_epoch),
420 None => self.device.gpu_execution_pool.clone(),
421 })
422 }
423
424 #[doc(hidden)]
429 pub fn attempt_stream_provider_route(&self) -> Option<Arc<AttemptStreamProviderRoute>> {
430 self.stream.route().cloned()
431 }
432
433 pub fn query_id(&self) -> Option<&QueryId> {
435 self.plan.query_id.as_ref()
436 }
437
438 pub fn trace_id(&self) -> Option<&TraceId> {
440 self.plan.trace_id.as_ref()
441 }
442
443 #[doc(hidden)]
448 pub fn attempt_id(&self) -> Option<AttemptId> {
449 self.admitted().map(|attempt| attempt.attempt_id)
450 }
451
452 #[cfg(feature = "cugraph")]
456 #[doc(hidden)]
457 pub fn begin_cache_scope_allocator_correlation(
458 &self,
459 scope: Arc<cudf::CacheAllocationScope>,
460 ) -> Option<CacheScopeCorrelationGuard> {
461 self.admitted()?.cache_scope_allocator_correlation.begin(scope)
462 }
463
464 pub fn runtime_observation(&self) -> &RuntimeObservationHandle {
466 &self.plan.runtime_observation
467 }
468
469 #[doc(hidden)]
472 pub fn begin_runtime_execution_observation(
473 &self,
474 plan_execution_id: PlanExecutionId,
475 ) -> RuntimeExecutionObservationLifecycle {
476 RuntimeExecutionObservationLifecycle::for_context(self, plan_execution_id)
477 }
478
479 pub fn runtime_execution_scope(&self) -> Option<&RuntimeExecutionObservationScope> {
481 self.plan.runtime_execution_scope.as_ref()
482 }
483
484 #[doc(hidden)]
489 pub fn with_runtime_observation_handle(&self, observation: RuntimeObservationHandle) -> Result<Self> {
490 self.validate_observation_identity(&observation)?;
491 Ok(self.with_plan(|plan| plan.runtime_observation = observation))
492 }
493
494 #[doc(hidden)]
496 pub fn with_runtime_execution_scope(&self, scope: RuntimeExecutionObservationScope) -> Result<Self> {
497 self.validate_observation_identity(scope.observation())?;
498 Ok(self.with_plan(|plan| {
499 plan.runtime_observation = scope.observation().clone();
500 plan.runtime_stream_lease_scope.plan_execution_id = Some(scope.plan_execution_id().clone());
501 plan.runtime_execution_scope = Some(scope);
502 }))
503 }
504
505 fn validate_observation_identity(&self, observation: &RuntimeObservationHandle) -> Result<()> {
506 if let Some(query_id) = &self.plan.query_id {
507 validate_runtime_observation_query_id(observation, query_id)?;
508 }
509 if let Some(trace_id) = &self.plan.trace_id {
510 validate_runtime_observation_trace_id(observation, trace_id)?;
511 }
512 Ok(())
513 }
514
515 pub fn with_runtime_gpu_stream_scope(&self, scope: RuntimeStreamLeaseScope) -> Self {
517 self.with_plan(|plan| {
518 plan.runtime_stream_lease_scope.gpu_job_id = scope.gpu_job_id;
519 plan.runtime_stream_lease_scope.gpu_worker_id = scope.gpu_worker_id;
520 })
521 }
522
523 pub fn with_query_id(&self, query_id: impl Into<QueryId>) -> Result<Self> {
525 let query_id = query_id.into();
526 validate_runtime_observation_query_id(&self.plan.runtime_observation, &query_id)?;
527 Ok(self.with_plan(|plan| plan.query_id = Some(query_id)))
528 }
529
530 pub fn with_trace_id(&self, trace_id: impl Into<TraceId>) -> Result<Self> {
532 let trace_id = trace_id.into();
533 validate_runtime_observation_trace_id(&self.plan.runtime_observation, &trace_id)?;
534 Ok(self.with_plan(|plan| plan.trace_id = Some(trace_id)))
535 }
536
537 pub fn is_query_service_admitted(&self) -> bool {
539 self.admitted().is_some()
540 }
541
542 #[doc(hidden)]
545 pub fn resident_source_cache(&self) -> Option<GpuLocalParquetSourceCache> {
546 self.device.resident_source_cache.clone()
547 }
548
549 #[cfg(feature = "cugraph")]
552 #[doc(hidden)]
553 pub fn resident_graph_cache(&self) -> Option<GpuResidentGraphCache> {
554 self.device.resident_graph_cache.clone()
555 }
556
557 pub(super) fn install_root_stream_cleanup_completion(
564 &self,
565 before_quiescence: impl FnOnce(StreamCleanupOutcome) + Send + 'static,
566 after_quiescence: impl FnOnce() + Send + 'static,
567 ) -> bool {
568 let StreamBinding::AttemptRoot { lease, .. } = &*self.stream else {
569 return false;
570 };
571 lease.install_cleanup_completion(before_quiescence, after_quiescence)
572 }
573
574 pub(super) fn has_root_stream_lease(&self) -> bool {
575 matches!(*self.stream, StreamBinding::AttemptRoot { .. })
576 }
577
578 pub fn cancellation(&self) -> &ExecutionCancellation {
580 match &self.query {
581 QueryScope::Standalone(query) => &query.cancellation,
582 QueryScope::Admitted(attempt) => &attempt.cancellation,
583 }
584 }
585
586 pub fn check_cancellation(&self, operation: &'static str) -> Result<()> {
593 self.check_service_epoch(operation)?;
594 self.cancellation().check(operation)
595 }
596
597 fn check_service_epoch(&self, operation: &'static str) -> Result<()> {
598 let Some(attempt) = self.admitted() else {
599 return Ok(());
600 };
601 attempt.service_epoch.check_open(operation)?;
602 attempt.query_epoch.check_open(operation)
603 }
604
605 pub fn device(&self) -> DeviceId {
607 self.stream.runtime().device()
608 }
609
610 pub fn memory_control_policy(&self) -> MemoryControlPolicy {
612 match &self.query {
613 QueryScope::Standalone(query) => query.memory_control_policy,
614 QueryScope::Admitted(attempt) => attempt.memory_control_policy,
615 }
616 }
617
618 pub fn memory_grant(&self) -> Result<Option<MemoryGrant>> {
625 let Some(attempt) = self.admitted() else {
626 return Ok(None);
627 };
628 self.admitted_memory_grant(attempt, "query_engine.runtime.context.memory_grant").map(Some)
629 }
630
631 fn admitted_memory_grant(&self, attempt: &AdmittedAttempt, operation: &'static str) -> Result<MemoryGrant> {
632 let resource_epoch = self.acquire_resource_epoch(operation)?;
633 Ok(match resource_epoch {
634 Some(resource_epoch) => attempt.memory_grant.with_resource_epoch(resource_epoch),
635 None => attempt.memory_grant.clone(),
636 })
637 }
638
639 #[doc(hidden)]
640 pub fn retain_resident_output_owner(&self, bytes: u64) -> Result<Option<ResidentOwnerLease>> {
641 match self.memory_grant()? {
642 Some(grant) => grant.retain_resident_owner(bytes),
643 None => Ok(None),
644 }
645 }
646
647 pub fn execution_budget_token(&self, request: cudf::ExecutionBudgetRequest) -> Result<cudf::ExecutionBudgetToken> {
651 let runtime = self.runtime()?;
652 Ok(cudf::ExecutionBudgetToken::from_shared_lease(&runtime, request, self.stream.member().clone()))
653 }
654
655 pub fn attempt_allocation_domain_id(&self) -> Option<u64> {
657 self.admitted().map(|attempt| attempt.allocation_domain.domain_id())
658 }
659
660 #[cfg(test)]
662 pub(crate) fn attempt_allocation_grant_identity(&self) -> Option<GrantIdentity> {
663 self.admitted().map(|attempt| attempt.allocation_domain.grant_identity())
664 }
665
666 pub fn attempt_allocation_domain_usage(&self) -> Result<Option<cudf::QueryMemoryDomainUsage>> {
668 self.admitted().map(|attempt| attempt.allocation_domain.usage()).transpose()
669 }
670
671 pub fn device_budget_bytes(&self) -> u64 {
673 self.memory_control_policy().device_budget_bytes
674 }
675
676 pub fn source_scheduler_policy(&self) -> SourceSchedulerPolicy {
678 self.device.source_scheduler_policy
679 }
680
681 pub fn source_chunk_planning_mode(&self) -> SourceChunkPlanningMode {
683 self.device.source_chunk_planning_mode
684 }
685 pub fn statistical_aggregate_execution_mode(&self) -> StatisticalAggregateExecutionMode {
687 self.device.statistical_aggregate_execution_mode
688 }
689
690 pub fn datafusion_order_tracking(&self) -> bool {
698 self.plan.datafusion_order_tracking
699 }
700
701 pub fn with_datafusion_order_tracking(self, enabled: bool) -> Self {
703 self.with_plan(|plan| plan.datafusion_order_tracking = enabled)
704 }
705
706 pub fn aggregate_strategy_execution_options(&self) -> AggregateStrategyExecutionOptions {
708 self.device.aggregate_strategy_execution_options
709 }
710
711 pub fn planning_policy(&self) -> PlanningPolicy {
714 self.device.planning_policy
715 }
716
717 pub fn source_chunk_planning_options(&self) -> SourceChunkPlanningOptions {
719 self.device.source_chunk_planning_options
720 }
721}
722
723pub(in crate::runtime) fn memory_control_policy_for_grant(
726 policy: MemoryControlPolicy,
727 grant: &MemoryGrant,
728) -> MemoryControlPolicy {
729 MemoryControlPolicy { device_budget_bytes: grant.budget_bytes(), ..policy }
730}
731
732fn validate_runtime_observation_query_id(observation: &RuntimeObservationHandle, query_id: &QueryId) -> Result<()> {
733 let Some(observation_query_id) = observation.query_id() else {
734 return Ok(());
735 };
736 if observation_query_id.as_str() == query_id.as_str() {
737 return Ok(());
738 }
739 Err(QueryError::new(
740 QueryErrorCode::InvalidPlan,
741 "query_engine.runtime_observation.identity",
742 "execution context query id conflicts with its runtime observation session",
743 )
744 .with_fact("reason", "runtime_observation_query_id_mismatch")
745 .with_fact("context_query_id", query_id)
746 .with_fact("observation_query_id", observation_query_id))
747}
748
749fn validate_runtime_observation_trace_id(observation: &RuntimeObservationHandle, trace_id: &TraceId) -> Result<()> {
750 if !observation.is_enabled() {
752 return Ok(());
753 }
754 let observation_trace_id = observation.trace_id().map(TraceId::as_str);
755 if observation_trace_id == Some(trace_id.as_str()) {
756 return Ok(());
757 }
758 Err(QueryError::new(
759 QueryErrorCode::InvalidPlan,
760 "query_engine.runtime_observation.identity",
761 "execution context trace id conflicts with its runtime observation session",
762 )
763 .with_fact("reason", "runtime_observation_trace_id_mismatch")
764 .with_fact("context_trace_id", trace_id)
765 .with_fact("observation_trace_id", observation_trace_id.unwrap_or("none")))
766}
767
768pub(in crate::runtime) fn validate_service_stream_pool_device(
769 pool: &ServiceStreamPool,
770 runtime_device: DeviceId,
771) -> Result<()> {
772 if pool.device() != runtime_device {
773 return Err(QueryError::new(
774 QueryErrorCode::InvalidPlan,
775 "query_engine.service_stream_pool.device",
776 "service stream pool device must match the execution runtime device",
777 )
778 .with_fact("pool_device_ordinal", pool.device().ordinal())
779 .with_fact("runtime_device_ordinal", runtime_device.ordinal()));
780 }
781 Ok(())
782}