feat: allow fractional desired task counts - #617
Conversation
| ChildrenIsolatorUnionExec: task_count=Desired(1) | ||
| FilterExec: task_count=Maximum(1) | ||
| RepartitionExec: task_count=Maximum(1) | ||
| DistributedLeafExec: task_count=Maximum(1) | ||
| ProjectionExec: task_count=Maximum(1) | ||
| FilterExec: task_count=Maximum(1) | ||
| RepartitionExec: task_count=Maximum(1) | ||
| DistributedLeafExec: task_count=Maximum(1) |
There was a problem hiding this comment.
🤔 I'm not sure why there's a Maximum(1) getting stamped in the two separate branches.
How it is that we are not seeing a Desired(0.5) instead?
There was a problem hiding this comment.
The Maximum(1) annotations are the final per-child allocations: after the union resolves to one task, propagate_task_count_until_network_boundaries stamps each allocated child count as a hard cap. The union's Desired(1) is the key assertion that the two 0.5 hints were summed before rounding; early rounding would produce Desired(2). I added that explanation beside the snapshot.
There was a problem hiding this comment.
Sounds good. Can we add another test that double checks what happens when the two leaves of the union report 0.0 task count?
There was a problem hiding this comment.
Added in 01c6441 as test_union_all_zero_task_count_leaves. Two Desired(0.0) leaves still sum before rounding, then ChildrenIsolatorUnionExec rejects the resulting zero-task stage as an internal planning error rather than silently collapsing to one task.
| @@ -30,7 +30,9 @@ pub(crate) fn file_scan_config_desired_task_count( | |||
| .div_ceil(d_cfg.file_scan_config_bytes_per_partition) | |||
| .div_ceil(cfg.target_partitions()); | |||
|
|
|||
| Some(Ok(DesiredTaskCountEventResponse::desired(task_count))) | |||
| Some(Ok(DesiredTaskCountEventResponse::desired( | |||
| task_count as f64, | |||
| ))) | |||
There was a problem hiding this comment.
Here, we should no longer be doing a .div_ceil(), we should be doing normal f64 divisions so that we get true fractional numbers.
There was a problem hiding this comment.
Done in 01c6441. The default file-scan handler now does total_bytes as f64 / bytes_per_partition / target_partitions (and the work-unit scan helper matches). as_usize() still ceils only when the planner resolves an integer task count.
|
CI exposed three stale inline snapshot expectations that should now remain fractional. I updated only those expectations in 849f66d; the broad local library suite passes with 289 tests passed, 0 failed, 1 ignored, and the known Windows-only socket-text assertion filtered out. The replacement workflow is waiting for external-contributor approval: https://github.com/datafusion-contrib/datafusion-distributed/actions/runs/31325189612 |
| assert_snapshot!(annotated, @" | ||
| HashJoinExec: task_count=Desired(2) | ||
| NetworkShuffleExec: task_count=Desired(2) | ||
| HashJoinExec: task_count=Desired(1.3333333333333333) |
There was a problem hiding this comment.
For the Debug implementation, it might be enough to just show 2 decimal numbers.
There was a problem hiding this comment.
TaskCountAnnotation::Debug now prints whole numbers as Desired(4) and fractions with two decimals (Desired(1.33)). Snapshot expectations in this file were updated to match.
|
@shinzoxD let me know if you still want to work on this, otherwise we can have other contributors help here |
|
Yes, still working on this. The remaining review items are:
I'll push those shortly. |
| // Two 0.0 leaf hints sum to Desired(0) before rounding. The isolator | ||
| // then rejects a zero-task union as an internal planning error rather | ||
| // than silently collapsing to a single task. | ||
| let err = annotate_test_plan_result(test_plan_builder, query) | ||
| .await | ||
| .expect_err("zero-task union should fail planning"); |
There was a problem hiding this comment.
This should not error out. A Desired(0) is a valid outcome (e.g. all datasets in the leaves of the union are empty). Sounds like something we need to fix.
There was a problem hiding this comment.
Addressed in 84bacd0: Desired(0) is now a valid empty-stage outcome. Two zero-weight union leaves plan as Desired(0) / Maximum(0) instead of an internal error.
| async fn annotate_test_plan(test_plan_builder: TestPlanBuilder, query: &str) -> String { | ||
| annotate_test_plan_result(test_plan_builder, query) | ||
| .await | ||
| .expect("failed to annotate plan") | ||
| } | ||
|
|
||
| async fn annotate_test_plan_result( | ||
| test_plan_builder: TestPlanBuilder, | ||
| query: &str, | ||
| ) -> datafusion::error::Result<String> { |
There was a problem hiding this comment.
We never want to assert errors here, I'd rollback this change.
There was a problem hiding this comment.
Addressed in 84bacd0: rolled back annotate_test_plan_result. The helper is success-only again; the zero-task case now snapshots the valid Desired(0) plan instead of asserting an error.
| // about the task count assigned to the network boundary in the consumer stage, | ||
| // and we don't want it to affect other task count decisions. | ||
| (Some(Arc::new(stats)), Desired(1)) | ||
| (Some(Arc::new(stats)), Desired(1.0)) |
There was a problem hiding this comment.
🤔 I think we should be returning 0.0 here. The dynamic planner might now decide to that maybe 0.2 tasks are enough, and this might get added between children of a UNION, so we would not want the 0.2 to be masked by the greater 1.0 in this case, we want the lower fractional values to be summed across children in a UNION.
There was a problem hiding this comment.
Addressed in 84bacd0: the dynamic planner now returns Desired(0.0) for the consumer-side network-boundary hint so small fractional values are not masked by 1.0 and can be summed across UNION children.
| let task_count = total_bytes | ||
| .div_ceil(d_cfg.file_scan_config_bytes_per_partition) | ||
| .div_ceil(cfg.target_partitions()); | ||
| let bytes_per_partition = d_cfg.file_scan_config_bytes_per_partition.max(1) as f64; | ||
| let target_partitions = cfg.target_partitions().max(1) as f64; | ||
| let task_count = total_bytes as f64 / bytes_per_partition / target_partitions; |
84bacd0 to
fe45919
Compare
|
Rebased onto The only conflict was in
New HEAD: |
|
Remaining review items from that thread are in: union zero-task leaves, fractional file-scan, two-decimal Debug, Desired(0) valid, and dynamic planner 0.0. PR is mergeable on current main. |
fe45919 to
4446393
Compare
|
Rebased onto latest Fractional behavior is unchanged: union children sum as |
|
Rebase onto main pulled in |
Keep Desired Debug to two decimal places for non-integers, compute default file-scan task counts with true f64 division, and add a union regression for 0.0 leaf hints. Two zero hints still sum before rounding and are then rejected as a zero-task ChildrenIsolatorUnionExec.
Empty union children can sum to Desired(0). That is a valid empty stage, not an internal planning error. Return Desired(0.0) from the dynamic planner so UNION children can sum small fractional hints instead of being masked by 1.0.
v4.0.0 is now released, so the Desired(f64) public break belongs in the next major upgrade guide rather than the historical 3.0.0 notes.
037b3fd to
f0f0642
Compare
Summary
DesiredTaskCountHandlerresponses to use fractionalf64task-count hintsCloses #562
Validation
cargo fmt --all -- --checkcargo check --libcargo clippy --lib -- -D warningscargo test --features integration --lib -- --skip protocol::grpc::channel_resolver::tests::fails_establishing_connection(289 passed, 0 failed, 1 ignored, 1 filtered out)cargo check --example custom_worker_url_routing --features integrationThe library suite and example check passed locally with the benchmark-only dev-dependency edge temporarily excluded to avoid its unrelated vendored-OpenSSL Windows build. The snapshot path filter was normalized only for the Windows test run, and the Windows-only connection-failure assertion was skipped because its socket error text differs by platform.
Cargo.toml,Cargo.lock, and the snapshot helper were restored with no diff before commit.Assisted-by: OpenAI Codex