Skip to content

fix(datafusion): size the memory pool by the effective partition count - #9183

Merged
wjones127 merged 1 commit into
mainfrom
lu/fix-mem-pool-partition-sizing
Sep 17, 2026
Merged

wjones127 merged 1 commit into
mainfrom
lu/fix-mem-pool-partition-sizing

Conversation

@LuQQiu

@LuQQiu LuQQiu commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

LanceExecutionOptions::mem_pool_size() multiplies the 150MB per-partition default by target_partition.unwrap_or(1), but when target_partition is None the session it serves does not run with 1 partition: new_session_context skips with_target_partitions, so DataFusion falls back to its own default, get_available_parallelism() (all CPU cores).

The result is a FairSpillPool sized for one partition feeding a plan that runs one partition per core. A sort-heavy plan (BTree scalar index training, for example) spawns one ExternalSorterMerge per partition, each reserving ~40MB that cannot spill; on a 12-core machine that is ~480MB of demand against a 150MB pool. The failure is load-dependent and flaky:

ResourcesExhausted: Additional allocation failed for ExternalSorterMerge[0] ...
30.0 MB remain available for the total memory pool: fair(pool_size: 150.0 MB)

We hit this repeatedly in lance frag_reuse clone tests whenever several create_index calls run concurrently, and worked around it there with serial_test. Any concurrent index-training workload can hit it.

Fix

mem_pool_size() now multiplies by the effective partition count: target_partition.unwrap_or_else(get_available_parallelism), using the exact same datafusion_common::utils::get_available_parallelism that DataFusion's session default uses, so the pool and the session can no longer disagree. Behavior is unchanged for every explicit configuration:

  • target_partition: Some(n) still yields 150MB * n
  • an explicit mem_pool_size option still sets the total pool, unscaled
  • a valid LANCE_MEM_POOL_SIZE env value still sets the total pool, unscaled
  • the env-parse-error fallback now agrees with the main fallback

Validation

lance-datafusion 144 + 5 tests green, including the sizing test updated to derive its expected None value from the same parallelism source (no hardcoded core count); fmt and clippy clean; cargo check -p lance clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DgAshYD7wVzPVdjXRuWPPs

mem_pool_size() multiplied the 150MB per-partition default by
target_partition.unwrap_or(1), but when target_partition is None the
session config is not overridden and DataFusion falls back to its
default target_partitions, which is the available parallelism (number
of CPU cores). The FairSpillPool was therefore sized for one partition
while sort-heavy plans (e.g. BTree scalar index training) spawned one
ExternalSorterMerge consumer per core, each reserving ~40MB
unspillable against a 150MB pool. On multi-core machines this caused
load-dependent, flaky ResourcesExhausted failures under concurrent
create_index workloads (observed in the frag_reuse clone tests, which
had been worked around with serial_test).

Size the pool by the effective partition count instead: when
target_partition is None, multiply the per-partition default by
datafusion_common's get_available_parallelism(), the same source
DataFusion uses for its target_partitions default. The env-parse-error
fallback branch uses the same value, so the two fallbacks agree.
Explicit mem_pool_size (option or LANCE_MEM_POOL_SIZE env var) still
sets the total pool size, unscaled, and target_partition: Some(n)
still yields 150MB * n.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgAshYD7wVzPVdjXRuWPPs
@github-actions github-actions Bot added the bug Something isn't working label Sep 13, 2026

@lance-gatekeeper lance-gatekeeper Bot 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.

✅ Gate recommendation: approve.

The pool now follows the same effective partition count as the DataFusion session while preserving explicit memory and partition overrides, closing the observed fair-share exhaustion without changing plan parallelism.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 13, 2026
@wjones127
wjones127 merged commit aac7844 into main Sep 17, 2026
38 checks passed
@wjones127
wjones127 deleted the lu/fix-mem-pool-partition-sizing branch September 17, 2026 22:46
Xuanwo pushed a commit that referenced this pull request Sep 30, 2026
)

## Root cause

The original CI failure ran before #9183 sized the default memory pool
by the effective DataFusion partition count. New production evidence on
#9402 exposed a remaining failure: concurrent scalar BTree trainings use
spilling sorts, but `execute_plan` reused a cached `SessionContext` and
its single `FairSpillPool`. Each `ExternalSorterMerge` can need up to 40
MiB of non-spillable memory when it re-reserves after a spill. Several
trainings can therefore exhaust the shared pool even when each sort fits
by itself.

## Fix

Give each spilling caller a fresh session and bounded pool; continue
caching non-spilling sessions. Keep the named 40 MiB merge reservation
limit and the partition headroom regression guard. The new regression
holds three spilling contexts with the same 150 MiB configuration at
once and reserves 100 MiB in each; the old shared-pool behavior fails on
the second reservation.

Each spilling execution retains its configured memory-pool and
temporary-disk budgets, but concurrent executions can consume multiples
of `LANCE_MEM_POOL_SIZE` and `LANCE_MAX_TEMP_DIRECTORY_SIZE`. On
resource-constrained hosts, limit concurrent index builds or size those
per-execution budgets accordingly.

## Validation

- `cargo test -p lance-datafusion` (183 unit tests and 5 doctests
passed; 3 existing ignored doctests)
- `cargo test -p lance --test scalar_index_spill` (passed)
- `cargo test -p lance --features slow_tests --test integration_tests
query::primitives::test_query_integer` (8 passed, including
`case_7_uint32`)
- `cargo fmt --all -- --check`
- `cargo clippy --all --tests --benches -- -D warnings`
- `RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps`

Fixes #9402

<!-- lance-gatekeeper-fix:v1 agent=7ba7a17aff9f983820ad859ecc276c6e
generation=1 -->

---------

Co-authored-by: Gatefixer <312823363+lance-gatefixer[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working K-approved Latest Gatekeeper recommendation permits acceptance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants