Skip to content

feat: prototype Comet Native Executors built on DataFusion Ballista - #7289

Draft
andygrove wants to merge 8 commits into
apache:mainfrom
andygrove:native-executors-prototype
Draft

andygrove wants to merge 8 commits into
apache:mainfrom
andygrove:native-executors-prototype

Conversation

@andygrove

@andygrove andygrove commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Which issue does this PR close?

Part of #7288.

The native side of this prototype is up for review on its own in #7299. Until #7299 merges, this branch also holds its commits, the first five below. Once it merges, I'll rebase this PR onto main so it holds only the driver side.

Rationale for this change

#7288 proposes Comet Native Executors: an optional deployment mode where a Spark driver plans each query and a cluster of native processes built on Apache DataFusion Ballista runs it, with no JVM in the data plane. This draft is the prototype that the issue's first sub-task calls for. It runs simple queries and TPC-H q1 end to end, to check the design before building it out:

  • The driver and the cluster talk only through Comet protobuf over Arrow Flight. The driver never links Ballista.
  • The cluster is a separate Cargo workspace on the Ballista 55 crates. The scheduler wraps Ballista's scheduler and uses Comet's planner to check each block.
  • Executors rebuild each block with Comet's planner, with no JVM.

It is not meant to merge as is.

What changes are included in this PR?

The first five commits are #7299, which describes them: the wire protocol, Comet native blocks as DataFusion operators, the comet-scheduler and comet-executor binaries, their tests through Flight, and a CI job for the new workspace. On top of them, one commit per part:

  1. Driver.
    • A JNI Flight client in libcomet, behind a native-executors Cargo feature that is off by default. It fills in the query's Comet version and plan format, submits it to the scheduler, and returns the result partitions' endpoints to the JVM as a ResultEndpoints message.
    • Two internal configs: spark.comet.nativeExecutors.enabled and spark.comet.nativeExecutors.scheduler.url.
    • CometNativeExecutorsExec, a leaf that CometRule substitutes for the whole converted plan, with AQE on or off. Each partition of its RDD reads one result partition from the executor that holds it.
    • A stage-graph builder that turns each native block into a stage and each native shuffle into an input. It declares each stage's output as Comet's shuffle writer declares its input, and refuses, naming the operator, any query that can't run entirely natively.
  2. Sorts. A range exchange is sent with its sort keys. Ballista can't plan a range shuffle for a submitted physical plan yet (Static planner drops a RepartitionExec with range partitioning instead of planning a range shuffle datafusion-ballista#2680), so the scheduler reads the exchange as one partition and the sort runs in a single task. That is correct for a global sort, just serial. It is what TPC-H q1 needed.
  3. CI. CometNativeExecutorsSuite is listed in dev/ci/check-suites.py as a manual suite.

What works: Parquet scans, filters, projections, aggregates, shuffled joins and sorts, across hash, range and single-partition exchanges. That includes TPC-H q1. On debug builds at SF1, q1 runs as three stages (scan and partial aggregate, final aggregate, sort) in about 4 seconds.

Not yet: broadcast joins, subqueries and DPP, writes, cancellation, metrics in the Spark UI, Spark-compatible hash partitioning, and parallel sorts.

Ballista enhancements this needs, filed upstream:

Default builds are unaffected. The configs default to off, the Cargo feature is off, and Ballista exists only in the separate workspace.

How are these changes tested?

  • Native side: feat: add comet-scheduler and comet-executor for Comet Native Executors #7299's tests. tests/assembly.rs runs stage graphs on DataFusion alone, and tests/cluster.rs runs queries on the real binaries through Flight, in CI.

  • Scala: CometNativeExecutorsSuite (9 tests) starts a real comet-scheduler and comet-executor, runs queries through Spark with the feature on, and compares the rows with Spark's. It also asserts that the plan ran on the cluster. Queries covered:

    • scan, filter and project
    • an aggregate across a hash shuffle, with AQE on and off
    • a global aggregate
    • sort-merge and hash joins
    • TPC-H q1, with and without its order by, checking row order too
    • a broadcast join the cluster can't run, which fails naming the operator

    The q1 tests read lineitem from SPARK_TPCH_DATA when it is set, and otherwise generated data with the same schema. Both pass on SF1 data from tpchgen-cli -s 1 -T lineitem -f parquet.

    The suite is manual: it skips itself unless COMET_NATIVE_EXECUTORS_BIN_DIR names the binaries. To run it:

    cargo build --manifest-path native/Cargo.toml -p datafusion-comet --features native-executors
    CARGO_TARGET_DIR=native/target cargo build --manifest-path native/native-executors/Cargo.toml
    COMET_NATIVE_EXECUTORS_BIN_DIR=native/target/debug ./mvnw test -Dtest=none \
      -Dsuites=org.apache.comet.exec.CometNativeExecutorsSuite
  • Existing tests: clippy is clean for the main workspace, for core with the new feature, and for the new workspace.

A Spark driver submits a query to a Comet Native Executor cluster as a
QueryRequest: a stage graph of Comet native blocks, connected by the
exchanges between them, with each stage's Spark output schema and, for
stages that read files, each partition's plan. A Fragment carries a
stage from the scheduler to its executors.

The proto crate also exports a fingerprint of the .proto files, so that
a cluster can refuse a query from a client that speaks another plan
format. See apache#7288.
@andygrove
andygrove force-pushed the native-executors-prototype branch from 9319751 to d0f80c4 Compare October 28, 2026 16:12
CometFragmentExec runs a stage's native block for each of its
partitions, with the block's input leaves fed by other DataFusion plans,
such as a distributed engine's shuffle readers, instead of by the JVM.
PhysicalPlanner::with_native_inputs is the seam: an input leaf (Scan or
ShuffleScan) takes the next native input, cast to the types Spark
declared for it, and an expression that calls into the JVM fails to
plan.

Each partition's output is cast to the stage's declared Spark schema
before it leaves the stage, as a Spark task's shuffle writer casts its
input, since a hash exchange places rows by the types of their values
too. The block runs in a Comet session over the host's memory pool and
disk manager.

A partition descriptor child, an in-memory source with one row per
partition, carries each partition's index in its stage and, for a stage
that reads files, the partition's own copy of the block. An engine that
splits a stage between tasks slices it with the stage's other leaves.

block_schema plans a block with empty inputs, so that a scheduler can
check a block before running it. See apache#7288.
@andygrove
andygrove force-pushed the native-executors-prototype branch from d0f80c4 to ef46676 Compare October 28, 2026 16:49
…ista

A new Cargo workspace, native/native-executors, builds the two processes
of a Comet Native Executor cluster on the Ballista 55 crates. It is a
workspace of its own, so the main Comet build never depends on Ballista.

comet-scheduler wraps Ballista's scheduler in an Arrow Flight service.
GetFlightInfo takes a QueryRequest, checks that the client runs the same
Comet build, plans each block with Comet's planner, turns the stage
graph into a Ballista physical plan, runs it, and returns one endpoint
per result partition with an opaque ticket for the executor's DoGet.

Hash exchanges become RepartitionExec(Hash), and single-partition and
broadcast exchanges become CoalescePartitionsExec, which Ballista's
planner turns into stage boundaries. A stage that reads exchanges has
as many partitions as they deliver. Ballista can't plan a range shuffle
for a physical plan yet (apache/datafusion-ballista#2680), so a range
exchange is read as one partition, and the stage that sorts it runs in
a single task.

comet-executor is Ballista's executor with Comet's codec, which carries
each CometFragmentExec's Fragment through Ballista's stage and task
plans. It needs no JVM. Both binaries take their settings from the
environment, with Ballista's defaults otherwise. See apache#7288.
tests/cluster.rs starts the two binaries, submits stage graphs to the
scheduler's Flight service as a driver would, and reads every result
partition from the executors with DoGet. It covers a hash shuffle, a
range exchange, and a query from another Comet build, which the
scheduler refuses.
native/native-executors is a Cargo workspace of its own, so the Rust
jobs don't cover it. The new linux-test-native-executors job runs
clippy and the workspace's tests, including the ones that start a
scheduler and an executor, and the lint job checks its formatting.
With spark.comet.nativeExecutors.enabled, CometRule replaces the plan
Comet would execute with a CometNativeExecutorsExec leaf, which runs the
whole query on the cluster at spark.comet.nativeExecutors.scheduler.url.
This works with AQE on, where Comet's query-stage-prep rule sees the
whole plan, and off. The leaf is columnar, so Spark Connect's
execute-and-submitJob path and collect() both read it through local
tasks on the driver, each of which fetches one result partition.

The stage graph builder turns each native block into a stage and each
native shuffle into an input, and refuses, naming the operator, any
query that cannot run entirely natively, including a block whose plan
calls back into the JVM. Operators that only turn the result into JVM
objects, as Dataset.rdd adds, stay on the driver.

The driver's Flight client is a JNI library in libcomet behind the
native-executors Cargo feature, off by default. CometNativeExecutorsSuite
starts a scheduler and an executor and checks queries against Spark; it
runs when COMET_NATIVE_EXECUTORS_BIN_DIR names the binaries. See apache#7288.
The driver now serializes a range exchange as a RangePartition with its
sort keys and no bounds. Until Ballista can plan a range shuffle for a
physical plan (apache/datafusion-ballista#2680), the scheduler reads a
range exchange as one partition, so the stage that sorts it runs in a
single task. That is correct for a global sort, if serial.

CometNativeExecutorsSuite runs TPC-H q1, with and without its order by,
on the lineitem table under SPARK_TPCH_DATA when it is set, and on
generated data with the same schema otherwise. The query that the
cluster must refuse is now a broadcast join. See apache#7288.
It runs only against comet-scheduler and comet-executor binaries named
by COMET_NATIVE_EXECUTORS_BIN_DIR, so the PR build matrix would only
skip it.
@andygrove
andygrove force-pushed the native-executors-prototype branch from ef46676 to eca9510 Compare October 28, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:scan Parquet scan / data reading enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant