Repository navigation
fix: [branch-1.1] keep one row per key in a native PartialMerge that runs short of memory (#6867) - #7020
Conversation
… 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)
kazuyukitanimura
left a comment
There was a problem hiding this comment.
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. |
Which issue does this PR close?
Closes #6857 on
branch-1.1. #6867 already closed it onmain.Rationale for this change
This backports #6867 to
branch-1.1.Comet runs a Spark
PartialMergeaggregate as a DataFusionPartialaggregate. WhenPartialruns 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 thePartialMergein 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 gthen returns too large acount(DISTINCT d), with no error (priority:critical). ThePartialMergeandPartialmapping 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.rsthan 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: thePartialMergeruns as DataFusionSingle, which spills.MergeAsPartialevaluates to a struct of its state, and a projection unpacks the struct into the columns thePartialplan emitted.SpillReplayPoolalso coversSingleHashAggregateStream.The adaptations are recorded in the commit message:
merge_as_partial.rsis ported by hand onto this branch's file, which has noPartialMergeStateDecoderorcollect_liststate fields (feat: add native distinct-combined collect_list aggregate support #4727). I started from this branch's file and applied only the fix: keep one row per key in a native PartialMerge that runs short of memory #6867 changes. The only difference frommain's diff is the decoder-related lines, which are not here.ordered_by_grouping_keyssort, which is a later change. So the planner hunk keepsmain's unpacking and nothing after it, and the tests for that sort are not taken. ThePartialMergeplanner test is otherwise identical, minus a..Default::default()that clippy flags once the missing field is gone.spill_replay.rshas nounwrap_spill_replay, because fix: count pool overcommit as untracked memory in the native memory usage log #6271 is not on this branch. That only moved the conflict context.CometAggregateSuite: thecollect_listPartialMergetest is not taken, because this branch falls back to Spark forcollect_listnext to a distinct aggregate (feat: add native distinct-combined collect_list aggregate support #4727 is not here). I ran it to confirm: it fails with "Expected a native PartialMerge aggregate", which is the fallback and not a wrong result.functionsimport line is unchanged, since the dropped test was the only user of the extra imports.How are these changes tested?
Run locally on this branch with JDK 17:
CometAggregateSuite, theaggregatefixtures inCometSqlFileTestSuiteandCometExecSuitepass (327 tests). That includes the two newPartialMergetests. Scalastyle and spotless ran in the same build.cargo test -p datafusion-comet --libfor the planner,merge_as_partialand memory pool tests passes (73 tests, including the new planner test).cargo fmt --checkand workspacecargo clippy --all-targets -D warningsare clean. My local toolchain is Rust 1.97, and CI uses stable.PartialMergeasPartialagain, without the unpacking, rebuiltlibcomet, and ran the two new tests. Both failed withResults do not match. Themd5of 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 norun-*label is needed.