Skip to content

fix: [branch-1.1] keep one row per key in a native PartialMerge that runs short of memory (#6867) - #7020

Merged
andygrove merged 1 commit into
apache:branch-1.1from
andygrove:backport-6867-branch-1.1
Oct 15, 2026
Merged

andygrove merged 1 commit into
apache:branch-1.1from
andygrove:backport-6867-branch-1.1

Conversation

@andygrove

Copy link
Copy Markdown
Member

Which issue does this PR close?

Closes #6857 on branch-1.1. #6867 already closed it on main.

Rationale for this change

This backports #6867 to branch-1.1.

Comet runs a Spark PartialMerge aggregate as a DataFusion Partial aggregate. When Partial runs short of memory it emits the groups it holds and starts again, which is fine for a real partial aggregate. But Spark's single-distinct plan counts the rows of the PartialMerge in the stage above it, so a key that leaves the stage twice is counted twice. SELECT g, sum(v), count(DISTINCT d) FROM t GROUP BY g then returns too large a count(DISTINCT d), with no error (priority:critical). The PartialMerge and Partial mapping in the planner is the same on this branch, and the two tests below fail here without the change.

This is not a 1.1.0 regression, and it needs memory pressure on the aggregate. I am proposing it because the query shape is common and the result is silently wrong, in the same way as #6270 and #6700. It is also the largest of the backports so far, so I'd like a closer look at the hand port in merge_as_partial.rs than a clean cherry-pick would need.

What changes are included in this PR?

One cherry-pick with -x, resolved by hand where this branch differs. See #6867 for the design: the PartialMerge runs as DataFusion Single, which spills. MergeAsPartial evaluates to a struct of its state, and a projection unpacks the struct into the columns the Partial plan emitted. SpillReplayPool also covers SingleHashAggregateStream.

The adaptations are recorded in the commit message:

How are these changes tested?

Run locally on this branch with JDK 17:

  • On the default Spark 4.1 profile, CometAggregateSuite, the aggregate fixtures in CometSqlFileTestSuite and CometExecSuite pass (327 tests). That includes the two new PartialMerge tests. Scalastyle and spotless ran in the same build.
  • On Spark 3.5 and Scala 2.12, the sources and tests compile.
  • cargo test -p datafusion-comet --lib for the planner, merge_as_partial and memory pool tests passes (73 tests, including the new planner test). cargo fmt --check and workspace cargo clippy --all-targets -D warnings are clean. My local toolchain is Rust 1.97, and CI uses stable.
  • To check that the tests catch the bug on this branch, I changed the planner to run PartialMerge as Partial again, without the unpacking, rebuilt libcomet, and ran the two new tests. Both failed with Results do not match. The md5 of the library Maven copied matched the rebuilt one both times.

By CI's path routing, which I checked with dev/ci/compute-changes.py, this PR runs the all-profiles Linux build, macOS, the Spark SQL jobs for 3.5, 4.0 and 4.1, and the four Iceberg versions, so no run-* label is needed.

… memory (apache#6867)

* fix: keep one row per key in a native PartialMerge that runs short of memory

Comet ran Spark's PartialMerge aggregate as DataFusion Partial, which emits
the groups it holds when it runs short of memory and starts again. In Spark's
single-distinct plan the stage above the PartialMerge counts its rows, so
count(DISTINCT) counted every copy of a key.

Run a PartialMerge as DataFusion Single instead, which spills. MergeAsPartial
evaluates to a struct of its state, and a projection unpacks it into the state
columns that the Partial plan emitted. SpillReplayPool also covers
SingleHashAggregateStream, whose spill replay starves the same way as the
final aggregates' do (apache#6254).

Closes apache#6857.

* Emit PartialMerge states a column at a time for row-only aggregates

MergeAsPartial now wraps an inner aggregate that has no GroupsAccumulator
(first, last and any_value without an ordering) in a GroupsAccumulatorAdapter
itself. Under Single mode, DataFusion otherwise wrapped the wrapper's row
accumulators and evaluated every group to a one-row struct.

Also write the planner's two output wrappers the same way, check the planner
test's output with assert_batches_sorted_eq and cover first there, and drop two
settings that the memory-pressure tests don't read.

Adapted for branch-1.1:
- merge_as_partial.rs is ported by hand onto this branch's file, which has no
  PartialMergeStateDecoder or collect_list state fields (apache#4727). Only the apache#6867 changes are
  taken. The return-field override for the inner accumulator is the part of
  `inner_accumulator_args` that does not involve the decoder.
- The planner has no `ordered_by_grouping_keys` sort (a later change), so the PartialMerge
  output is the unpacked projection and nothing is sorted after it. The tests for that sort
  are not taken, and the PartialMerge planner test drops the `..Default::default()` that the
  missing field made necessary.
- spill_replay.rs has no `unwrap_spill_replay` (apache#6271 is not on this branch), which only moved the
  conflict context. Only apache#6867's lines apply.
- CometAggregateSuite: the collect_list PartialMerge test is not taken, because this branch
  falls back to Spark for collect_list next to a distinct aggregate (apache#4727 is not here).

(cherry picked from commit 46f6311)
@github-actions github-actions Bot added bug Something isn't working area:aggregation Hash aggregates, aggregate expressions area:memory Memory pools, reservations, OOM handling labels Oct 15, 2026

@kazuyukitanimura kazuyukitanimura left a comment

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.

docs/source/contributor-guide/memory_management.md (pool-stack diagram, and the section "Final aggregates reading their spill files back"). Thanks for the careful
port. The SpillReplayPool description says it covers final aggregates only. With this change it also covers the SingleHashAggregateStream that Comet now uses for
a PartialMerge, as the spill_replay.rs header says. Could you update both places in this PR? Also please open a matching fix for main, since #6867 did not touch
the page and the two branches should agree.

@andygrove
andygrove merged commit 27b4c1d into apache:branch-1.1 Oct 15, 2026
109 checks passed
@andygrove
andygrove deleted the backport-6867-branch-1.1 branch October 15, 2026 20:23
@andygrove

Copy link
Copy Markdown
Member Author

docs/source/contributor-guide/memory_management.md (pool-stack diagram, and the section "Final aggregates reading their spill files back"). Thanks for the careful port. The SpillReplayPool description says it covers final aggregates only. With this change it also covers the SingleHashAggregateStream that Comet now uses for a PartialMerge, as the spill_replay.rs header says. Could you update both places in this PR? Also please open a matching fix for main, since #6867 did not touch the page and the two branches should agree.

I went ahead and merged since this fixes a correctness bug. The suggested doc edit is for the contributors guide which we do not maintain on release branches. It only gets published from the main branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:aggregation Hash aggregates, aggregate expressions area:memory Memory pools, reservations, OOM handling bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants