Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
31f8b6e
feat: make AQE respect broadcast_join_threshold_bytes
andygrove Jul 17, 2026
aade4aa
feat: add Ballista broadcast_join_threshold_rows and use it in AQE
andygrove Jul 17, 2026
2729768
fix: default broadcast_join_threshold_rows to 1M to match existing be…
andygrove Jul 17, 2026
bd940a3
refactor: derive new_with_ballista join thresholds from Ballista conf…
andygrove Jul 17, 2026
a42245a
feat: demote DataFusion CollectLeft joins over the Ballista threshold
andygrove Jul 17, 2026
6809fa5
docs: apply prettier formatting to AQE tuning-guide table
andygrove Jul 17, 2026
bf185ce
test: disable AQE broadcast via Ballista threshold in repartition tests
andygrove Jul 17, 2026
806fcb7
fix: size the AQE broadcast decision by bytes, not row count
andygrove Jul 17, 2026
f33bef6
test: cover broadcast thresholds with declared statistics
andygrove Jul 17, 2026
b3bd7a7
Merge remote-tracking branch 'apache/main' into fix/2081-size-aware-b…
andygrove Jul 18, 2026
affeec0
feat: log AQE join decisions at INFO and shuffle spills at WARN
andygrove Jul 18, 2026
b53c1e3
feat: default sort-shuffle spill cap to 0 (uncapped) and plumb it thr…
andygrove Jul 18, 2026
c6a474a
feat: add hash_join_max_build_partition_bytes config
andygrove Jul 19, 2026
99e66ee
feat: add max_per_partition_build_bytes helper for hash-join fit check
andygrove Jul 19, 2026
5174608
feat: fall back to SMJ when hash-join build exceeds per-slot budget
andygrove Jul 19, 2026
45d9844
feat: log hash-join build-fit decision at INFO
andygrove Jul 19, 2026
afef9af
refactor: hoist build-size call and tidy hash-join fit-check docs
andygrove Jul 19, 2026
3fe1771
docs: add hash-join + safety-fallback SF1000 suite results (2x16, p64)
andygrove Jul 19, 2026
becb376
Merge upstream/main into fix/2081-size-aware-broadcast
andygrove Jul 20, 2026
ad16347
docs: refresh SF1000 Ballista AQE-on results on #2084 build
andygrove Jul 21, 2026
fac1fa2
revert: keep upstream 256 MiB sort-shuffle spill-cap default
andygrove Jul 21, 2026
45db50c
docs: note SF1000 results use uncapped sort-shuffle spill override
andygrove Jul 21, 2026
2939ece
Update ballista/scheduler/src/state/aqe/execution_plan/dynamic_join.rs
andygrove Jul 21, 2026
b563844
refactor: log per-partition shuffle write at debug; fix log import
andygrove Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions ballista/core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ pub const BALLISTA_BROADCAST_JOIN_THRESHOLD_BYTES: &str =
pub const BALLISTA_BROADCAST_JOIN_THRESHOLD_ROWS: &str =
"ballista.optimizer.broadcast_join_threshold_rows";

/// Configuration key for the maximum per-partition hash-join build-side bytes
/// permitted for a Partitioned hash join under AQE. When a build partition
/// exceeds this, the join falls back to SortMergeJoin (spillable). `0` disables
/// the check (hash join is used regardless of build size).
pub const BALLISTA_HASH_JOIN_MAX_BUILD_PARTITION_BYTES: &str =
"ballista.optimizer.hash_join_max_build_partition_bytes";

/// Configuration key to enable AQE coalesce-shuffle-partitions rule.
/// Disabled by default — opt in when the workload benefits from larger
/// downstream tasks more than from preserved parallelism.
Expand Down Expand Up @@ -264,6 +271,12 @@ static CONFIG_ENTRIES: LazyLock<HashMap<String, ConfigEntry>> = LazyLock::new(||
promotion via the row-count path.".to_string(),
DataType::UInt64,
Some((1_000_000).to_string())),
ConfigEntry::new(BALLISTA_HASH_JOIN_MAX_BUILD_PARTITION_BYTES.to_string(),
"Maximum per-partition hash-join build-side bytes for a Partitioned \
hash join under AQE. A build partition larger than this falls back to \
SortMergeJoin (spillable). 0 (the default) disables the check.".to_string(),
DataType::UInt64,
Some("0".to_string())),
ConfigEntry::new(BALLISTA_CLIENT_PULL.to_string(),
"Should client employ pull or push job tracking. In pull mode client will make a request to server in the loop, until job finishes. Pull mode is kept for legacy clients.".to_string(),
DataType::Boolean,
Expand Down Expand Up @@ -615,6 +628,11 @@ impl BallistaConfig {
self.get_usize_setting(BALLISTA_BROADCAST_JOIN_THRESHOLD_ROWS)
}

/// Maximum per-partition hash-join build-side bytes before falling back to SMJ.
pub fn hash_join_max_build_partition_bytes(&self) -> usize {
self.get_usize_setting(BALLISTA_HASH_JOIN_MAX_BUILD_PARTITION_BYTES)
}

/// Returns whether the AQE coalesce-shuffle-partitions rule is enabled.
pub fn coalesce_enabled(&self) -> bool {
self.get_bool_setting(BALLISTA_COALESCE_ENABLED)
Expand Down Expand Up @@ -884,4 +902,12 @@ mod tests {
assert_eq!(16777216, config.grpc_client_max_message_size());
Ok(())
}

#[test]
fn hash_join_max_build_partition_bytes_defaults_to_zero() {
assert_eq!(
BallistaConfig::default().hash_join_max_build_partition_bytes(),
0
);
}
}
9 changes: 5 additions & 4 deletions ballista/core/src/execution_plans/sort_shuffle/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ use datafusion::physical_plan::{
SendableRecordBatchStream, Statistics, displayable,
};
use futures::{StreamExt, TryStreamExt};
use log::{debug, warn};
use log::{debug, info, warn};

/// Result of finalizing shuffle output: (data_path, index_path, partition_write_stats)
/// where partition_write_stats is (partition_id, num_batches, num_rows, num_bytes)
Expand Down Expand Up @@ -439,8 +439,9 @@ impl SortShuffleWriterExec {
let mut hash_buffer: Vec<u64> = Vec::new();
let mut spill_events: u64 = 0;
// Absolute buffered-bytes counter, independent of the runtime
// `MemoryPool`. Drives spill decisions so the writer bounds its
// RSS even when the pool is unbounded.
// `MemoryPool`. When `memory_limit` is non-zero it caps this counter
// as a second spill trigger; a `memory_limit` of 0 disables the cap
// so spilling is driven solely by memory-pool pressure.
let mut buffered_bytes: usize = 0;
// A limit of 0 disables the per-task budget, leaving the runtime
// `MemoryPool` as the sole spill trigger.
Expand Down Expand Up @@ -567,7 +568,7 @@ impl SortShuffleWriterExec {
write_time,
);
} else {
debug!(
info!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this got flipped back to info

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b563844 — demoted to debug!. It fires once per shuffle partition; the spill case stays at warn!.

"Sort shuffle write for partition {} completed. \
Output: {:?}, Index: {:?}, Rows: {}, \
repart_time={:?} spill_time={:?} write_time={:?}, \
Expand Down
14 changes: 14 additions & 0 deletions ballista/core/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ pub trait SessionConfigExt {
/// disables promotion via the row-count path.
fn with_ballista_broadcast_join_threshold_rows(self, threshold_rows: usize) -> Self;

/// Returns the maximum per-partition hash-join build-side bytes before
/// falling back to SortMergeJoin under AQE. `0` disables the check.
fn ballista_hash_join_max_build_partition_bytes(&self) -> usize;

/// retrieves grpc client max message size
fn ballista_grpc_client_max_message_size(&self) -> usize;

Expand Down Expand Up @@ -513,6 +517,16 @@ impl SessionConfigExt for SessionConfig {
}
}

fn ballista_hash_join_max_build_partition_bytes(&self) -> usize {
self.options()
.extensions
.get::<BallistaConfig>()
.map(|c| c.hash_join_max_build_partition_bytes())
.unwrap_or_else(|| {
BallistaConfig::default().hash_join_max_build_partition_bytes()
})
}

fn ballista_shuffle_reader_maximum_concurrent_requests(&self) -> usize {
self.options()
.extensions
Expand Down
39 changes: 39 additions & 0 deletions ballista/core/src/serde/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,45 @@ mod test {
assert_eq!(stored.groups[0].upstream_indices, vec![0, 1, 2, 3]);
}

#[tokio::test]
async fn sort_shuffle_writer_memory_limit_survives_roundtrip() {
use datafusion::physical_plan::empty::EmptyExec;

let schema = create_test_schema();
let input: Arc<dyn ExecutionPlan> = Arc::new(EmptyExec::new(schema.clone()));
let partitioning =
Partitioning::Hash(vec![col("id", schema.as_ref()).unwrap()], 4);

let config = SortShuffleConfig::new(true, 4096)
.with_memory_limit_per_task_bytes(1024 * 1024 * 1024);
let original = SortShuffleWriterExec::try_new(
"job-1".to_string().into(),
3,
input.clone(),
String::new(),
partitioning,
config,
)
.unwrap();

let codec = BallistaPhysicalExtensionCodec::default();
let mut buf: Vec<u8> = vec![];
codec.try_encode(Arc::new(original), &mut buf).unwrap();

let ctx = SessionContext::new().task_ctx();
let decoded = codec.try_decode(&buf, &[input], &ctx).unwrap();
let decoded = decoded
.downcast_ref::<SortShuffleWriterExec>()
.expect("Expected SortShuffleWriterExec");

assert_eq!(
decoded.config().memory_limit_per_task_bytes,
1024 * 1024 * 1024,
"memory limit override must survive serialization to the executor"
);
assert_eq!(decoded.config().batch_size, 4096);
}

#[tokio::test]
async fn test_shuffle_reader_exec_coalesced_roundtrip_multi_group_mixed_sizes() {
let schema = create_test_schema();
Expand Down
Loading