Skip to content

Fix GroupValues retained memory accounting - #25188

Draft
kosiew wants to merge 19 commits into
apache:mainfrom
kosiew:memcalc-11-23393
Draft

Fix GroupValues retained memory accounting#25188
kosiew wants to merge 19 commits into
apache:mainfrom
kosiew:memcalc-11-23393

Conversation

@kosiew

@kosiew kosiew commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

Aggregate memory accounting can under-report retained memory in GroupValues. In particular, owner descriptors and reusable allocations such as group-index lists, emit scratch storage, and vectorized-operation buffers can remain allocated without being reflected in GroupValues::size().

Under memory pressure, partial hash aggregation can also materialize the complete aggregate state before slicing it into output batches. This can require the full state to fit in memory at the point where early emission is intended to relieve memory pressure.

This PR makes retained-state accounting include the memory owned by the GroupValues implementations and changes partial aggregate early emission to drain state incrementally in bounded batches.

What changes are included in this PR?

  • Updates GroupValuesPrimitive, GroupValuesRows, and GroupValuesColumn memory accounting to include their owner descriptors and retained owned allocations.
  • Accounts for retained group-index lists, emit scratch storage, vectorized-operation buffers, and concrete GroupColumn owner state.
  • Shrinks reusable column scratch buffers when clear_shrink is requested.
  • Changes the dictionary group-value hash cache to use a weak reference so the cache does not retain an input dictionary allocation.
  • Rebuilds aggregate accumulators after state emission so accumulator implementations that retain capacity do not keep emitted allocations charged during spill recovery.
  • Changes partial hash aggregate early emission to materialize and release at most one output batch at a time rather than first materializing the complete partial state.
  • Updates memory-limited aggregate tests and expected memory/spill metrics to reflect the corrected accounting.

Are these changes tested?

Yes. The patch adds and updates focused regression coverage, including:

  • size_includes_boxed_owner_descriptor
  • size_includes_owner_and_retained_allocations
  • size_includes_boxed_primitive_and_row_backed_owners
  • size_includes_collision_emit_and_vectorized_buffers
  • size_retains_vectorized_and_emit_scratch_capacity
  • clear_shrink_releases_vectorized_and_emit_scratch_capacity
  • size_retains_reusable_buffers_after_emit
  • hash_cache_does_not_retain_dictionary_values
  • test_partial_hash_stream_incrementally_emits_on_memory_pressure
  • test_partial_hash_stream_emits_bounded_batch_when_full_state_does_not_fit
  • test_partial_hash_stream_releases_groups_after_incremental_emit

Existing spilling and nested-nullability aggregate tests are also adjusted to exercise spilling under the corrected memory accounting, including native DISTINCT aggregation.

Are there any user-facing changes?

There are no public API changes.

Queries using hash aggregation under constrained memory should have more accurate memory accounting, and partial aggregation can release state incrementally during memory pressure instead of requiring the complete partial aggregate state to be materialized before emission.

LLM-generated code disclosure

This PR includes LLM-generated code and comments. All LLM-generated content has been manually reviewed.

- GroupValues::size() charges owner + retained buffers.
- Column: added group-index, emit, vectorized, and vec backing.
- Added capacity/reuse tests.
- Updated tight spill test pools.
…) calls

- Updated `row.rs` to subtract inline `RowConverter`/`Rows` descriptors when calculating nested `.size()` calls.
- The outer `GroupValuesRows` descriptor is now charged only once, preventing duplicate size accounting.
- This resolves overcounting of size for nested rows, improving the accuracy of size calculations.
- No functional changes to the row data itself; only the size accounting logic has been refined.
- Enhances performance and reliability for operations that rely on precise size metrics.
- Added a dedicated integration test in `datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs`
- The test validates scratch‑capacity and reuse behavior across:
- Five distinct vectorized buffers
- The `emit_scratch` logic path
- Verifies that buffer growth (delta) correctly allocates additional capacity
- Confirms that clearing the buffers retains the allocated charge, ensuring proper reuse without unnecessary reallocations
…pe row‑backed inline nested descriptors, add primitive row‑backed multi‑column tests, and enable 1,024 spill limit

- All missing GroupColumn owner descriptors charged.
- Row‑backed inline nested descriptors deduped.
- Added primitive, row‑backed, multi‑column tests.
- Existing 1,024 spill limit now spills; ordered test passes.
- Added `size_retains_reusable_buffers_after_emit` test to verify reusable buffers are retained after emit.
- Checks `rows_buffer` and hash scratch retention after `EmitTo::First`.
- Re‑interns values and rechecks accounting to ensure correct behavior after re‑emission.
- Moved comment to correct emit test.
@github-actions github-actions Bot added the physical-plan Changes to the physical-plan crate label Sep 11, 2026
- Increase SLT peak from **9.2 KB** to **9.4 KB**.
- Retain spill test plan while adding assertions for **spill count** and **spill bytes**.
- Update pool sizes: **non‑distinct 1,000,000** entries and **DISTINCT 4,256,000** entries.
- Revised overall plan to reflect the metric adjustments and new test assertions.
@github-actions github-actions Bot added core Core DataFusion crate sqllogictest SQL Logic Tests (.slt) labels Sep 11, 2026
…TINCT pool

- Modified the partial‑aggregation logic to skip only when memory limits are exceeded, rather than under broader conditions.
- Reduced the DISTINCT pool size from `4_256_000` to `1_000_000` to lower memory consumption and improve performance in constrained environments.
@codecov-commenter

codecov-commenter commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.86352% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.90%. Comparing base (6263df2) to head (cf77086).
⚠️ Report is 22 commits behind head on main.

Files with missing lines Patch % Lines
...c/aggregates/aggregate_hash_table/partial_table.rs 81.81% 2 Missing and 6 partials ⚠️
.../src/aggregates/group_values/multi_group_by/mod.rs 95.78% 0 Missing and 7 partials ⚠️
...fusion/physical-plan/src/aggregates/hash_stream.rs 76.66% 5 Missing and 2 partials ⚠️
...n/physical-plan/src/aggregates/group_values/row.rs 89.28% 0 Missing and 6 partials ⚠️
...plan/src/aggregates/aggregate_hash_table/common.rs 80.00% 0 Missing and 1 partial ⚠️
.../aggregates/aggregate_hash_table/common_ordered.rs 83.33% 0 Missing and 1 partial ⚠️
...gregates/group_values/single_group_by/primitive.rs 93.75% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25188      +/-   ##
==========================================
- Coverage   81.91%   81.90%   -0.02%     
==========================================
  Files        1132     1133       +1     
  Lines      421280   424954    +3674     
  Branches   421280   424954    +3674     
==========================================
+ Hits       345111   348041    +2930     
- Misses      55767    56274     +507     
- Partials    20402    20639     +237     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…llability spill test

- Updated `nested_nullability.rs` test budget from `1_000_000` to `4_256_000`.
- Identified cause: DISTINCT struct state experiences a transient peak that exhausts the 1 MiB fair pool during spilling.
- Adjusted the budget to accommodate the peak memory usage, ensuring the test passes reliably.
- Pins `target_partitions=1` for the DISTINCT spill scenario, ensuring a deterministic topology.
- Retains the 1 MiB pool and associated spill assertions to validate correctness.
- Removes scheduler‑dependent Partial/Final aggregate competition that was causing out‑of‑memory (OOM) failures.
…iation during spill recovery

- After materializing spill state, rebuild accumulators from empty equivalents.
- Releases retained accumulator capacity before reservation reconciliation.
- Covers hash + ordered single/final spill paths.
- Updated `nested_nullability.rs` to wrap `FairSpillPool` in `TrackConsumersPool`.
- Next OOM includes:
- consumer names
- spillability
- live reservations
- peak bytes
- Fair allocation behavior remains unchanged.
…ll regression and adjust spill behavior

- Disables `single_distinct_aggregation_to_group_by` only in the DISTINCT spill regression.
- Asserts that the physical plan contains exactly one `AggregateExec`.
- Retains the 1‑partition + 1 MiB fair spill pool configuration.
- Leaves rewrite coverage elsewhere unchanged.
…st and rebuild empty partial table

- Partial OOM drain now emits states via `EmitTo::First(batch_size)` instead of full materialization.
- Eliminates full state‑batch materialization before slicing, reducing memory overhead.
- Rebuilds an empty partial table after drain and resumes input processing to maintain continuity.
- Addresses a 500 B grouping‑set regression and adds an `early_emit_count` assertion to catch premature emissions.
- Updates partial hash tests to verify bounded incremental output behavior.
…r messages to match FinalHashAggregateStream
…dd regression test

- Shrinks the group‑index and emits the scratch vector.
- Releases and rebuilds the vector’s scratch capacity.
- Resets the Boolean bitmap capacity safely.
- Adds a zero‑reset regression test to verify the behavior.
…he ownership

- `cached_values` changed from `Weak<dyn Array>` to `Option<Weak<dyn Array>>`.
- Cache retains its identity hint; it no longer owns or pins the input allocation.
- Added regression test to confirm a live identity cache while the input dictionary is released after drop.
- Reused `clear()` in the shrink path, resolving warnings‑denied dead‑code lint.
…_by): shrink_to remaining_row_indices instead of clear

Optimizes memory usage by calling `shrink_to(num_rows)` on `self.remaining_row_indices` rather than `clear()`. This avoids reallocating a new vector each time the buffer is reused, reducing overhead and improving performance in vectorized aggregations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Core DataFusion crate physical-plan Changes to the physical-plan crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants