use std::{collections::BTreeSet, sync::Arc};
use datafusion::{
common::Result as DataFusionResult,
execution::context::TaskContext,
physical_plan::{ExecutionPlan, coop::CooperativeExec, metrics::ExecutionPlanMetricsSet},
};
use futures::future::BoxFuture;
use query_planning::{
admission::AttemptCapabilityEvidence,
plan::{PlanFingerprint, QueryPlan},
};
use query_runtime::runtime::{ExecutionContext, resident::GpuResultHandle};
use super::{admission::QueryAdmissionTicket, native_node::native_node, shared_resident::shared_resident_results};
use crate::error::{Error, ErrorCode, Result};
pub(crate) trait GpuResidentProducer: Send + Sync {
fn gpu_result_input_name(&self) -> &str;
#[cfg(feature = "cugraph")]
fn gpu_result_is_repeatable(&self) -> bool {
false
}
fn reusable_gpu_result_scope(&self) -> Result<Option<Vec<PlanFingerprint>>> {
Ok(None)
}
fn gpu_result_capability_evidence(&self) -> Result<AttemptCapabilityEvidence> {
Err(Error::new(
ErrorCode::InvalidExecutionPlan,
"algeon_datafusion.native_exec.gpu_result_capability_evidence",
format!(
"GPU-resident producer {} does not expose capability evidence for its output",
self.gpu_result_input_name()
),
))
}
fn host_result_capability_evidence(&self) -> Result<AttemptCapabilityEvidence> {
self.gpu_result_capability_evidence()
}
fn host_result_query_plan(&self) -> Result<Option<QueryPlan>> {
Ok(None)
}
fn host_result_has_deferred_graph_facts(&self) -> bool {
false
}
fn hard_output_row_upper_bound(&self) -> Option<u64> {
None
}
fn unique_output_key_columns(&self) -> Option<&'static [&'static str]> {
None
}
fn native_relation_input_names(&self) -> Option<&[String]> {
None
}
fn rewrite_subsumed_composed_consumer(&self, _consumer_plan: &QueryPlan) -> Result<Option<Arc<dyn ExecutionPlan>>> {
Ok(None)
}
fn prepared_admission_tickets(&self) -> Vec<Arc<QueryAdmissionTicket>> {
Vec::new()
}
fn execution_metrics(&self) -> Option<&ExecutionPlanMetricsSet> {
None
}
fn prepare_execution_plan<'a>(
&'a self,
plan: Arc<dyn ExecutionPlan>,
_task_context: Arc<TaskContext>,
) -> BoxFuture<'a, DataFusionResult<Arc<dyn ExecutionPlan>>> {
Box::pin(async move { Ok(plan) })
}
fn execute_gpu_result_with_context<'a>(
&'a self,
native_context: &'a ExecutionContext,
partition: usize,
task_context: Arc<TaskContext>,
) -> BoxFuture<'a, DataFusionResult<(String, GpuResultHandle)>>;
}
pub(crate) fn gpu_resident_producer(plan: &dyn ExecutionPlan) -> Option<&dyn GpuResidentProducer> {
native_node(plan).and_then(|node| node.gpu_resident_producer()).or_else(|| {
plan.downcast_ref::<CooperativeExec>()
.and_then(|cooperative| native_node(cooperative.input().as_ref()))
.and_then(|node| node.gpu_resident_producer())
})
}
pub(crate) fn gpu_resident_producer_input_name(plan: &dyn ExecutionPlan) -> Option<String> {
gpu_resident_producer(plan).map(|producer| producer.gpu_result_input_name().to_string())
}
pub(crate) fn gpu_resident_producer_hard_output_row_upper_bound(plan: &dyn ExecutionPlan) -> Option<u64> {
gpu_resident_producer(plan).and_then(GpuResidentProducer::hard_output_row_upper_bound)
}
pub(crate) fn gpu_resident_producer_unique_output_key(plan: &dyn ExecutionPlan) -> Option<BTreeSet<String>> {
let key: BTreeSet<String> = gpu_resident_producer(plan).and_then(|producer| {
producer.unique_output_key_columns().map(|columns| columns.iter().map(|column| (*column).to_owned()).collect())
})?;
key.iter().all(|column| plan.schema().fields().iter().any(|field| field.name() == column)).then_some(key)
}
pub(crate) fn rewrite_gpu_resident_producer_for_subsumed_composed_consumer(
plan: &dyn ExecutionPlan,
consumer_plan: &QueryPlan,
) -> Result<Option<Arc<dyn ExecutionPlan>>> {
gpu_resident_producer(plan)
.map(|producer| producer.rewrite_subsumed_composed_consumer(consumer_plan))
.unwrap_or(Ok(None))
}
pub(crate) fn prepared_gpu_resident_producer_admission_tickets(
plan: &dyn ExecutionPlan,
) -> Vec<Arc<QueryAdmissionTicket>> {
gpu_resident_producer(plan).map_or_else(Vec::new, GpuResidentProducer::prepared_admission_tickets)
}
pub(crate) async fn prepare_gpu_resident_producer_for_execution(
plan: Arc<dyn ExecutionPlan>,
task_context: Arc<TaskContext>,
) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
let Some(producer) = gpu_resident_producer(plan.as_ref()) else {
return Ok(plan);
};
producer.prepare_execution_plan(Arc::clone(&plan), task_context).await
}
pub(crate) async fn execute_gpu_resident_producer_with_context(
plan: &dyn ExecutionPlan,
native_context: &ExecutionContext,
partition: usize,
task_context: Arc<TaskContext>,
) -> DataFusionResult<(String, GpuResultHandle)> {
if let Some(producer) = gpu_resident_producer(plan) {
let reuse = if partition == 0 {
shared_resident_results(&task_context)
.map(|results| {
let identity = results.reusable_identity(plan)?;
Ok::<_, Error>(identity.map(|identity| (results, identity)))
})
.transpose()?
.flatten()
} else {
None
};
if let Some((results, identity)) = &reuse
&& let Some(handle) = results.take_reusable(identity)
{
return Ok((producer.gpu_result_input_name().to_string(), handle));
}
let output = producer.execute_gpu_result_with_context(native_context, partition, task_context).await?;
if let Some((results, identity)) = reuse {
results.retain_reusable(identity, &output.1);
}
return Ok(output);
}
Err(Error::new(
ErrorCode::InvalidExecutionPlan,
"algeon_datafusion.native_gpu_producer.execute_gpu",
"execution plan is not a GPU-resident producer",
)
.with_fact("plan_name", plan.name())
.into())
}