Skip to content

feat: Add sort-based shuffle implementation - #81

Open
andygrove wants to merge 18 commits into
mainfrom
sort-based-shuffle
Open

feat: Add sort-based shuffle implementation#81
andygrove wants to merge 18 commits into
mainfrom
sort-based-shuffle

Conversation

@andygrove

@andygrove andygrove commented Jan 17, 2026

Copy link
Copy Markdown
Owner

Summary

This PR implements a sort-based shuffle mechanism for Ballista, similar to Spark's approach. Instead of creating one file per output partition (N×M files for N input partitions and M output partitions), each input partition writes to a single consolidated file with an index file mapping partition IDs to batch ranges.

Key Features

  • Consolidated output files: Each input partition produces one data file + one index file (2×N files instead of N×M)
  • Arrow IPC File format: Uses FileWriter/FileReader for efficient random access to specific record batches
  • Memory management: Configurable buffer sizes with spill-to-disk support to prevent OOM
  • Backward compatible: Sort-based shuffle is opt-in via configuration; default remains hash-based shuffle

Architecture

ballista/core/src/execution_plans/sort_shuffle/
├── mod.rs          # Module exports
├── config.rs       # SortShuffleConfig with buffer/memory settings
├── buffer.rs       # PartitionBuffer for in-memory buffering
├── spill.rs        # SpillManager for disk spilling
├── index.rs        # ShuffleIndex for batch range tracking
├── writer.rs       # SortShuffleWriterExec execution plan
└── reader.rs       # Reading logic with FileReader random access

Configuration Options

  • ballista.shuffle.sort_based.enabled - Enable sort-based shuffle (default: false)
  • ballista.shuffle.sort_based.buffer_size - Per-partition buffer size in bytes (default: 1MB)
  • ballista.shuffle.sort_based.memory_limit - Total memory limit for buffers (default: 256MB)

Performance

The sort-based shuffle can be more efficient when:

  • There are many output partitions (reduces file count from N×M to 2×N)
  • Network overhead of many small files is significant
  • Sequential I/O patterns are preferred over random I/O

Benchmark (cargo run --release --bin shuffle_bench):

  • Hash shuffle: Creates M files per input partition
  • Sort shuffle: Creates 1 data + 1 index file per input partition
  • FileReader random access: Directly seeks to needed batches without scanning

Testing

  • Unit tests for each component (buffer, spill, index, reader, writer)
  • End-to-end integration tests (ballista/client/tests/sort_shuffle.rs)
  • Comparison tests verifying sort shuffle produces same results as hash shuffle

Changes

Core

  • Added sort_shuffle module with all components
  • Added ShuffleWriter trait for polymorphic shuffle writer handling
  • Updated shuffle_reader.rs to detect and read sort-based shuffle format

Executor

  • Updated DefaultExecutionEngine to handle both ShuffleWriterExec and SortShuffleWriterExec
  • Added ShuffleWriterVariant enum for type-safe dispatch

Scheduler/Planner

  • Updated DistributedPlanner to create SortShuffleWriterExec when enabled
  • Updated ExecutionStageBuilder to use ShuffleWriter trait

Benchmarks

  • Added shuffle_bench binary for comparing hash vs sort shuffle performance

Related Issues

This PR relates to several open issues in apache/datafusion-ballista:


🤖 Generated with Claude Code

andygrove and others added 15 commits January 17, 2026 10:36
This adds an alternative shuffle implementation that writes a single
consolidated file per input partition (sorted by output partition ID)
along with an index file, similar to Apache Spark's sort-based shuffle.

This approach reduces file count from N × M (N input partitions ×
M output partitions) to 2 × N files (one data + one index file per
input partition).

Key components:
- SortShuffleWriterExec: ExecutionPlan implementation
- PartitionBuffer: In-memory buffering per partition
- SpillManager: Spill-to-disk when memory pressure is high
- ShuffleIndex: Index file I/O (little-endian i64 offsets)
- SortShuffleConfig: Configuration (buffer size, memory limit, spill threshold)

New configuration options:
- ballista.shuffle.sort_based.enabled (default: false)
- ballista.shuffle.sort_based.buffer_size (default: 1MB)
- ballista.shuffle.sort_based.memory_limit (default: 256MB)
- ballista.shuffle.sort_based.spill_threshold (default: 0.8)

This implementation is inspired by Apache DataFusion Comet's shuffle writer:
https://github.com/apache/datafusion-comet/blob/main/native/core/src/execution/shuffle/shuffle_writer.rs

Work remaining:
- Update DistributedPlanner to use SortShuffleWriterExec when enabled
- Update ShuffleReaderExec to detect and read sort shuffle format
- Add protobuf serialization for the new execution plan
- End-to-end integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit integrates the sort-based shuffle writer with the distributed
planner and execution graph:

- Add ShuffleWriter trait to provide a common interface for both
  ShuffleWriterExec and SortShuffleWriterExec
- Update DistributedPlanner to return Vec<Arc<dyn ShuffleWriter>>
- Update ExecutionStageBuilder to work with ShuffleWriter trait
- Add protobuf serialization for SortShuffleWriterExec
- Update execution_graph, execution_stage, and diagram modules

The planner will now create SortShuffleWriterExec when:
- ballista.shuffle.sort_based.enabled is true
- The partitioning is Hash partitioning

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…chmarks

- Update fetch_partition_local to detect sort-based shuffle format
  by checking for the presence of an index file
- When sort shuffle is detected, read partition data using the
  index file to locate the correct byte range
- Add shuffle_bench binary for comparing hash vs sort shuffle performance
  - Configurable number of rows, partitions, batch size
  - Reports timing, file count, and throughput metrics

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update DefaultExecutionEngine to handle both ShuffleWriterExec and
  SortShuffleWriterExec by introducing ShuffleWriterVariant enum
- Fix sort shuffle writer to store cumulative batch counts in index
  instead of placeholder byte offsets
- Fix sort shuffle reader to correctly identify partition boundaries
  using the batch count index
- Add comprehensive end-to-end integration tests for sort-based shuffle
  covering aggregations, GROUP BY, ORDER BY, and comparison with
  hash-based shuffle

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update writer to use FileWriter instead of StreamWriter
- Update reader to use FileReader with set_index() for direct batch access
- FileReader supports random access via the IPC footer, eliminating the
  need to scan through preceding batches to reach the target partition
- Index still stores cumulative batch counts for partition boundaries

This improves read performance for later partitions (e.g., partition 15
of 16) by directly seeking to the needed batches instead of reading
and discarding all preceding data.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use is_multiple_of() instead of manual modulo check.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add FinalizeResult type alias to reduce type complexity
- Add #[allow(clippy::too_many_arguments)] for finalize_output function
- Use iter_mut().enumerate() instead of index-based loop

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When running the tpch benchmark, the --query parameter is now optional.
If not specified, all 22 TPC-H queries will be run sequentially.

Changes:
- Make --query optional for both datafusion and ballista benchmarks
- Run all 22 queries when --query is not specified
- Only print SQL queries when --debug flag is enabled
- Write a single JSON output file for the entire benchmark run
- Fix parquet file path resolution for datafusion benchmarks
- Simplify output when iterations=1 (no iteration number, no average)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add CLI option to enable sort-based shuffle in the TPC-H benchmark
when running against Ballista.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…eader

Change sort shuffle partition reading to return a lazy stream that yields
batches on-demand rather than collecting all batches into memory upfront.
This reduces memory usage for large partitions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add algorithm description from Apache Spark documentation and improve
wording for clarity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update the flight server's do_get handler to detect sort-based shuffle
files and read only the relevant partition using the index file. This
enables remote shuffle reads for sort-based shuffle.

Also add detection in IO_BLOCK_TRANSPORT to return an informative error
since block transport doesn't support sort-based shuffle (it transfers
the entire file which contains all partitions).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
andygrove and others added 3 commits January 18, 2026 09:34
Use rstest to parameterize sort shuffle tests to run with both local
reads and remote reads via the flight service. This ensures the flight
service correctly handles sort-based shuffle partition requests.

Tests now run in two modes:
- Local: reads shuffle data from local filesystem (default)
- RemoteFlight: forces remote read through flight service

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant