use std::sync::Arc;
use datafusion::{
common::Result as DataFusionResult,
physical_plan::{ChildrenPropertiesMode, ExecutionPlan, ReplaceChildrenOptions},
};
use query_planning::{
admission::{AdmissionDecision, AdmittedPlan},
optimizer::{
NativeOptimizerDiagnostics, SharedResidentExtractionDecision, SharedResidentExtractionDiagnostics,
SharedResidentExtractionReason, SharedResidentFragments, SharedResidentProducerPlan,
extract_shared_resident_fragments,
},
plan::QueryPlan,
};
use super::capability_envelope::native_admission_decision;
use crate::{
error::Error,
native_exec::{
AlgeonNativeExec, AlgeonNativeExecutionConfig, gpu_resident_producer, gpu_resident_producer_input_name,
},
};
pub(super) struct SharedIsland {
pub(super) root: Arc<dyn ExecutionPlan>,
pub(super) root_admission: Option<Arc<AdmittedPlan>>,
pub(super) native_optimizer: NativeOptimizerDiagnostics,
}
pub(super) fn share_island_subplans(
root: &Arc<dyn ExecutionPlan>,
native_execution_config: &AlgeonNativeExecutionConfig,
) -> DataFusionResult<Option<SharedIsland>> {
let Some(root_unit) = root.downcast_ref::<AlgeonNativeExec>() else {
return Ok(None);
};
if !root_unit.materializes_host() || root_unit.gpu_inputs().is_empty() {
return Ok(None);
}
let mut units = Vec::new();
collect_units(root, true, &mut units);
let SharedResidentFragments { fragments, producers, mut diagnostics } = extract_shared_resident_fragments(
units.iter().map(|unit| unit.native_plan().clone()).collect(),
)
.map_err(|error| {
Error::from_native(
"algeon_datafusion.native_optimizer.shared_resident",
"shared-resident extraction across native units produced a malformed plan",
error,
)
})?;
let admitted = if producers.is_empty() {
None
} else {
admit_shared_plans(&fragments, &producers, units.len(), native_execution_config)?
};
let Some((admitted_units, shared_inputs)) = admitted else {
if !producers.is_empty() {
record_not_admitted(&mut diagnostics);
}
return Ok(Some(SharedIsland {
root: Arc::clone(root),
root_admission: None,
native_optimizer: shared_resident_diagnostics(diagnostics),
}));
};
let root_admission = admitted_units[0].clone();
let mut rebind = IslandRebind { admitted_units, shared_inputs, next_unit: 0 };
Ok(Some(SharedIsland {
root: rebind.rebind(root, true)?,
root_admission,
native_optimizer: shared_resident_diagnostics(diagnostics),
}))
}
type AdmittedIsland = (Vec<Option<Arc<AdmittedPlan>>>, Vec<Vec<Arc<dyn ExecutionPlan>>>);
fn admit_shared_plans(
fragments: &[QueryPlan],
producers: &[SharedResidentProducerPlan],
unit_count: usize,
native_execution_config: &AlgeonNativeExecutionConfig,
) -> DataFusionResult<Option<AdmittedIsland>> {
let mut shared_inputs = vec![Vec::<Arc<dyn ExecutionPlan>>::new(); unit_count];
for producer in producers {
let AdmissionDecision::Admitted(admitted) = native_admission_decision(&producer.plan, native_execution_config)?
else {
return Ok(None);
};
let producer_exec: Arc<dyn ExecutionPlan> = Arc::new(AlgeonNativeExec::try_new_shared_producer(
admitted,
producer.input_name.clone(),
producer.consumer_fragments.len(),
native_execution_config.clone(),
)?);
for fragment in &producer.consumer_fragments {
shared_inputs[*fragment].push(Arc::clone(&producer_exec));
}
}
let mut admitted_units = vec![None; unit_count];
for (index, plan) in fragments.iter().enumerate() {
if shared_inputs[index].is_empty() {
continue;
}
let AdmissionDecision::Admitted(admitted) = native_admission_decision(plan, native_execution_config)? else {
return Ok(None);
};
admitted_units[index] = Some(admitted);
}
Ok(Some((admitted_units, shared_inputs)))
}
fn record_not_admitted(diagnostics: &mut SharedResidentExtractionDiagnostics) {
for candidate in &mut diagnostics.candidates {
if candidate.decision == SharedResidentExtractionDecision::Extracted {
candidate.decision = SharedResidentExtractionDecision::Rejected;
candidate.reason = SharedResidentExtractionReason::ResidentPlanNotAdmitted;
candidate.resident_input_name = None;
diagnostics.rejected_count += 1;
}
}
diagnostics.extracted_count = 0;
}
fn shared_resident_diagnostics(diagnostics: SharedResidentExtractionDiagnostics) -> NativeOptimizerDiagnostics {
NativeOptimizerDiagnostics { shared_resident_extraction: diagnostics, ..NativeOptimizerDiagnostics::default() }
}
fn island_unit(plan: &Arc<dyn ExecutionPlan>, is_root: bool) -> Option<&AlgeonNativeExec> {
plan.downcast_ref::<AlgeonNativeExec>().filter(|unit| is_root || !unit.materializes_host())
}
fn reads_island_inputs(plan: &dyn ExecutionPlan) -> bool {
gpu_resident_producer(plan).is_some()
}
fn collect_units<'a>(plan: &'a Arc<dyn ExecutionPlan>, is_root: bool, units: &mut Vec<&'a AlgeonNativeExec>) {
if let Some(unit) = island_unit(plan, is_root) {
units.push(unit);
for input in unit.gpu_inputs() {
collect_units(input, false, units);
}
} else if reads_island_inputs(plan.as_ref()) {
for child in plan.children() {
collect_units(child, false, units);
}
}
}
struct IslandRebind {
admitted_units: Vec<Option<Arc<AdmittedPlan>>>,
shared_inputs: Vec<Vec<Arc<dyn ExecutionPlan>>>,
next_unit: usize,
}
impl IslandRebind {
fn rebind(&mut self, plan: &Arc<dyn ExecutionPlan>, is_root: bool) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
if let Some(unit) = island_unit(plan, is_root) {
let index = self.next_unit;
self.next_unit += 1;
let mut inputs = unit
.gpu_inputs()
.iter()
.map(|input| self.rebind(input, false))
.collect::<DataFusionResult<Vec<_>>>()?;
let inputs_changed = inputs.iter().zip(unit.gpu_inputs()).any(|(new, old)| !Arc::ptr_eq(new, old));
let shared = std::mem::take(&mut self.shared_inputs[index]);
let Some(admitted) = self.admitted_units[index].take() else {
return if inputs_changed {
Arc::clone(plan)
.replace_children(inputs, ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute))
} else {
Ok(Arc::clone(plan))
};
};
inputs.extend(shared);
inputs.sort_by(|left, right| {
gpu_resident_producer_input_name(left.as_ref()).cmp(&gpu_resident_producer_input_name(right.as_ref()))
});
return Ok(Arc::new(unit.with_rebound_plan(admitted, inputs)?));
}
if !reads_island_inputs(plan.as_ref()) {
return Ok(Arc::clone(plan));
}
let children = plan.children().into_iter().cloned().collect::<Vec<_>>();
let rebound = children.iter().map(|child| self.rebind(child, false)).collect::<DataFusionResult<Vec<_>>>()?;
if rebound.iter().zip(&children).all(|(new, old)| Arc::ptr_eq(new, old)) {
return Ok(Arc::clone(plan));
}
Arc::clone(plan).replace_children(rebound, ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute))
}
}
#[cfg(test)]
mod tests {
use std::fs::File;
use arrow::{
array::{ArrayRef, Float32Array, Int64Array},
datatypes::{DataType, Field, Schema},
record_batch::RecordBatch,
};
use datafusion::{
execution::SessionStateBuilder,
prelude::{ParquetReadOptions, SessionContext},
};
use parquet::arrow::ArrowWriter;
use query_planning::{plan::PlanNode, source::SourceNode};
use tempfile::TempDir;
use super::*;
use crate::{
cuvs_sql::CuvsAlgoExec,
native_exec::below_native_admission,
planner::{AlgeonNativeOptimizerConfig, NativePlanningDiagnostics, plan_with_native_diagnostics},
session::AlgeonSessionStateBuilderExt,
};
async fn plan_over_points(sql: &str) -> (TempDir, Arc<dyn ExecutionPlan>, NativePlanningDiagnostics) {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("points.parquet");
let schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int64, false),
Field::new("d0", DataType::Float32, false),
Field::new("d1", DataType::Float32, false),
]));
let batch = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int64Array::from_iter_values((0..16).map(|row| row % 8))) as ArrayRef,
Arc::new(Float32Array::from_iter_values((0..16).map(|row| if row % 8 < 4 { 0.0 } else { 10.0 }))),
Arc::new(Float32Array::from_iter_values((0..16).map(|row| if row % 8 < 4 { 1.0 } else { 11.0 }))),
],
)
.expect("points batch");
let mut writer = ArrowWriter::try_new(File::create(&path).expect("points file"), schema, None).expect("writer");
writer.write(&batch).expect("write points");
writer.close().expect("close points");
let state = SessionStateBuilder::new_with_default_features()
.with_algeon_native(AlgeonNativeOptimizerConfig::default())
.try_with_cuvs_sql()
.expect("install cuVS SQL")
.build();
let ctx = SessionContext::new_with_state(state);
ctx.register_parquet("points", path.to_str().expect("UTF-8 path"), ParquetReadOptions::default())
.await
.expect("register points");
let logical = ctx.state().create_logical_plan(sql).await.expect("logical plan");
let (plan, diagnostics) = plan_with_native_diagnostics(&ctx.state(), &logical).await.expect("physical plan");
(dir, plan, diagnostics)
}
fn root_and_kmeans_input(plan: &Arc<dyn ExecutionPlan>) -> (&AlgeonNativeExec, &AlgeonNativeExec) {
let root = below_native_admission(plan).downcast_ref::<AlgeonNativeExec>().expect("the query root lowers");
let kmeans = root
.gpu_inputs()
.iter()
.find_map(|input| input.downcast_ref::<CuvsAlgoExec>())
.expect("the root reads the K-means result on the device");
let input = kmeans.children()[0].downcast_ref::<AlgeonNativeExec>().expect("the K-means input lowers");
(root, input)
}
fn shared_producers(unit: &AlgeonNativeExec) -> Vec<&AlgeonNativeExec> {
unit.gpu_inputs()
.iter()
.filter_map(|input| input.downcast_ref::<AlgeonNativeExec>())
.filter(|input| input.gpu_resident_input_name().is_some_and(|name| name.starts_with(SHARED_PREFIX)))
.collect()
}
fn resident_source_names(plan: &QueryPlan) -> Vec<&str> {
plan.nodes()
.iter()
.filter_map(|node| match &node.kind {
PlanNode::Source(source) => match source.as_ref() {
SourceNode::GpuResident(source) => Some(source.input_name.as_str()),
_ => None,
},
_ => None,
})
.collect()
}
const SHARED_PREFIX: &str = "__algeon_shared_resident_";
#[tokio::test]
async fn an_aggregate_cte_read_by_kmeans_and_a_join_is_computed_once() {
let (_dir, plan, diagnostics) = plan_over_points(
"WITH centers AS (\
SELECT id, CAST(SUM(d0) AS REAL) AS d0, CAST(SUM(d1) AS REAL) AS d1 FROM points GROUP BY id), \
assignments AS (\
SELECT id, cluster_id FROM cuvs_kmeans(\
input => (SELECT id, d0, d1 FROM centers ORDER BY id), n_clusters => 2)) \
SELECT a.cluster_id, COUNT(*) AS members, SUM(c.d0) AS d0_total, SUM(c.d1) AS d1_total \
FROM assignments a JOIN centers c ON c.id = a.id GROUP BY a.cluster_id",
)
.await;
let (root, kmeans_input) = root_and_kmeans_input(&plan);
let [shared] = shared_producers(root)[..] else { panic!("the root reads one shared producer") };
let shared_name = shared.gpu_resident_input_name().expect("a producer output name");
assert!(matches!(shared.native_plan().root_node().kind, PlanNode::Aggregate(_)));
let [kmeans_shared] = shared_producers(kmeans_input)[..] else {
panic!("the K-means input reads one shared producer")
};
assert_eq!(kmeans_shared.gpu_resident_input_name(), Some(shared_name));
assert!(resident_source_names(root.native_plan()).contains(&shared_name));
assert_eq!(resident_source_names(kmeans_input.native_plan()), [shared_name]);
assert!(matches!(kmeans_input.native_plan().root_node().kind, PlanNode::Sort(_)));
assert!(
!kmeans_input.native_plan().nodes().iter().any(|node| matches!(node.kind, PlanNode::Aggregate(_))),
"the K-means input must not aggregate the points again"
);
let extracted = diagnostics
.native_optimizer
.shared_resident_extraction
.candidates
.iter()
.filter(|candidate| candidate.decision == SharedResidentExtractionDecision::Extracted)
.map(|candidate| (candidate.attempt_fragment, candidate.resident_input_name.as_deref()))
.collect::<Vec<_>>();
assert_eq!(extracted, [(Some(0), Some(shared_name)), (Some(1), Some(shared_name))]);
}
#[tokio::test]
async fn a_filtered_scan_cte_read_by_kmeans_and_a_join_is_recomputed() {
let (_dir, plan, diagnostics) = plan_over_points(
"WITH kept AS (SELECT id, d0, d1 FROM points WHERE d0 >= 0), \
assignments AS (\
SELECT id, cluster_id FROM cuvs_kmeans(\
input => (SELECT id, d0, d1 FROM kept ORDER BY id), n_clusters => 2)) \
SELECT a.cluster_id, COUNT(*) AS members, SUM(k.d0) AS d0_total, SUM(k.d1) AS d1_total \
FROM assignments a JOIN kept k ON k.id = a.id GROUP BY a.cluster_id",
)
.await;
let (root, kmeans_input) = root_and_kmeans_input(&plan);
assert!(shared_producers(root).is_empty());
assert!(kmeans_input.gpu_inputs().is_empty());
let rejected_fragments = diagnostics
.native_optimizer
.shared_resident_extraction
.candidates
.iter()
.filter(|candidate| {
candidate.decision == SharedResidentExtractionDecision::Rejected
&& candidate.reason == SharedResidentExtractionReason::RootDoesNotReduceRows
})
.filter_map(|candidate| candidate.attempt_fragment)
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(rejected_fragments, [0, 1].into());
}
}