Skip to content

make dynamic filters in ExecutionPlan nodes discoverable #23814

Description

@jayshrivastava

Is your feature request related to a problem or challenge?

Disclaimer: I'm writing this from the perspective of datafusion-distributed, but I imagine that this problem is common to other projects (ballista, comet) that distribute datafusion queries. I am not familiar with comet or ballista, so I would appreciate any feedback from folks who work on those projects.

More context in the (WIP) Dynamic Filtering in DataFusion-Distributed RFC

Background

Datafusion natively preserves dynamic filter producer<->consumer relationships across network boundaries only when you serialize the producer (HashJoinExec) and the consumer (DataSourceExec) in serialization run. There's tests which assert that this works.

                   ┌───────────────────────────────────────────────┐                                                
                   │          ┌───────────────────────┐            │                                                
                   │          │    RepartitionExec    │            │                                                
                   │          │                       │            │                                                
                   │          └───────────────────────┘            │                                                
                   │          ┌───────────────────────┐            │                                                
                   │          │     HashJoinExec      │            │                                                
 ┌───────────────┐ │          │  mode = Partitioned   │            │                                                
 │   Machine 1   │ │          └───────────────────────┘            │                                                
 └───────────────┘ │ ┌───────────────────┐  ┌───────────────────┐  │                                                
                   │ │  DataSourceExec   │  │  ProjectionExec   │  │                                                
                   │ └───────────────────┘  └───────────────────┘  │                                                
                   │                        ┌───────────────────┐  │                                                
                   │                        │  DataSourceExec   │  │                                                
                   │                        └───────────────────┘  │                                                
                   └───────────────────────────────────────────────┘                                                
                                           │                                                                        
                                           │    machine 1 serializes the plan                                       
                                           │    machine 2 deserializes the plan and preserves dynamic filter        
                                           │    relationships. dynamic filtering works                                                       
                                           ▼                                                                        
                   ┌───────────────────────────────────────────────┐                                                
                   │           ┌───────────────────────┐           │                                                
                   │           │    RepartitionExec    │           │                                                
                   │           │                       │           │                                                
                   │           └───────────────────────┘           │                                                
                   │           ┌───────────────────────┐           │                                                
                   │           │     HashJoinExec      │           │                                                
                   │           │  mode = Partitioned   │           │                                                
┌───────────────┐  │           └───────────────────────┘           │                                                
│   Machine 2   │  │  ┌───────────────────┐  ┌───────────────────┐ │                                                
└───────────────┘  │  │  DataSourceExec   │  │  ProjectionExec   │ │                                                
                   │  └───────────────────┘  └───────────────────┘ │                                                
                   │                         ┌───────────────────┐ │                                                
                   │                         │  DataSourceExec   │ │                                                
                   │                         └───────────────────┘ │                                                
                   └───────────────────────────────────────────────┘                                                

The problem is that datafusion doesn't support the case where you split up the consumer from the producer:

                   ┌───────────────────────────────────────────────┐                       
                   │          ┌───────────────────────┐            │                       
                   │          │    RepartitionExec    │            │                       
                   │          │                       │            │                       
                   │          └───────────────────────┘            │                       
                   │          ┌───────────────────────┐            │                       
                   │          │     HashJoinExec      │            │                       
 ┌───────────────┐ │          │  mode = Partitioned   │            │                       
 │   Machine 1   │ │          └───────────────────────┘            │                       
 └───────────────┘ │ ┌───────────────────┐  ┌───────────────────┐  │                       
                   │ │  DataSourceExec   │  │  ProjectionExec   │  │                       
                   │ └───────────────────┘  └───────────────────┘  │                       
                   │                        ┌───────────────────┐  │                       
                   │                        │  DataSourceExec   │  │                       
                   │                        └───────────────────┘  │                       
                   └───────────────────────────────────────────────┘                       
                                           │                                               
                                           │                                               
                                           │    machine 1 sends the probe side to machine 2
                                           │    how will the hash join send updates to the data source?                                           
                                           ▼                                               
                   ┌───────────────────────────────────────────────┐                       
                   │                                               │                       
                   │                                               │                       
                   │                                               │                       
                   │                                               │                       
                   │                                               │                       
                   │                                               │                       
                   │                                               │                       
┌───────────────┐  │                                               │                       
│   Machine 2   │  │                         ┌───────────────────┐ │                       
└───────────────┘  │                         │  ProjectionExec   │ │                       
                   │                         └───────────────────┘ │                       
                   │                         ┌───────────────────┐ │                       
                   │                         │  DataSourceExec   │ │                       
                   │                         └───────────────────┘ │                       
                   └───────────────────────────────────────────────┘                                                                                                                                                                                                                             

Describe the solution you'd like

Proposal:

  1. Distributed projects (ballista, comet, datafusion-distributed) should be responsible for implementing dynamic filter update routing and propagation during query execution
  2. Vanilla datafusion should provide a way to find and extract dynamic filters from ExecutionPlan to allow a project like datafusion-distributed to implement (1). The minimal required features are
    (a) identify which dynamic filters are producers and consumers in an ExecutionPlan tree
    (b) get access to all &DynamicFilterPhysicalExpr in an ExecutionPlan (as long as one is holding a reference to a DynamicFilterPhysicalExpr, they can read and write updates to it)
    (c) allow easy extension for custom ExecutionPlan implementations

Option 1 - New ExecutionPlan API

trait ExecutionPlan {
    fn dynamic_filter_exprs(&self) -> Vec<DynamicFilterNodeBehavior> 
}

enum DynamicFilterNodeBehavior {
    Producer(Arc<dyn PhysicalExpr>),
    Consumer(Arc<dyn PhysicalExpr>),
}

Notes

  • Previously, a similar API was reverted in Revert "Add ExecutionPlan::apply_expressions() (#20337)" #22437 because implementing it was complex and there was no use case in vanilla datafusion to assert correctness
    • The proposed dynamic_filter_exprs() API is simpler to implement, making the breaking change less annoying
    • Still no use case in vanilla datafusion

Pros:

  • Does exactly what we want

Cons:

  • Breaking change. It must be implemented by every ExecutionPlan. We cannot default to vec![] because ExecutionPlan nodes may fail to declare their dynamic filters.
  • Very specific. We usually treat dynamic filters as any other filter expression. However, this method cuts through the abstraction and brings them to the forefront.

Option 2 - Store Dynamic Filters in the Session (SessionState)

We can try updating the SessionState to store which plan nodes are producers and consumers of dynamic filters. This can be populated during the FilterPushdown optimization rule using the existing API:

trait PhysicalOptimizerRule {
    fn optimize_with_context(
        &self,
        plan: Arc<dyn ExecutionPlan>,
        context: &dyn PhysicalOptimizerContext,
    ) -> Result<Arc<dyn ExecutionPlan>>
}

The PhysicalOptimizerContext only stores a statistics registry today. We would need to extend it to store dynamic filters:

trait PhysicalOptimizerContext {
      fn runtime_filter_registry(
          &self,
      )
  }

Here's a rough sketch of what we would store in the Session.

struct FilterPushdownReport {
    bindings: HashMap<(u64, u64), Vec<RuntimeFilterBinding>>, // (expression_id, query_id) -> bindings
}

enum RuntimeFilterBinding {
    Producer {
        node: Arc<dyn ExecutionPlan>,
        expression: Arc<dyn PhysicalExpr>,
    },
    Consumer {
        node: Arc<dyn ExecutionPlan>,
        expression: Arc<dyn PhysicalExpr>,
        usage: FilterUsage, // Pruning or FullyEvaluated
    },
}

Pros

  • Not a breaking change

Cons

Open questions

  • Is it acceptable to add features in vanilla datafusion which aren't used by vanilla datafusion? Who is responsible for testing these? (Related: Add an example of an in-process model of distributed execution #23282).
    • It would be nice if vanilla datafusion committed to a minimal feature (ie. "DynamicFilterPhysicalExpr can be discovered from ExecutionPlan") with minimal tests support use cases like datafusion-distributed, ballista, and comet.

Describe alternatives you've considered

We could downcast ExecutionPlan to concrete types and try to discover dynamic filters like that, however this method is less extensible (how would we support custom ExecutionPlan implementations?) and it isn't guaranteed to work because not every concrete type will make their dynamic filters pub.

As of writing, these public methods on some execution plan nodes exist, byt they were only added to support serialization and will likely be removed by #23494.

HashJoinExec::dynamic_filter_expr()
AggregateExec::dynamic_filter_expr()
SortExec::dynamic_filter_expr()

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions