Skip to content

Commit 9d3d1ef

Browse files
westonpaceclaude
andauthored
fix: scale default memory pool size by partition count (#6562)
## Summary - The `FairSpillPool` divides memory evenly across spillable consumers. With up to 8 partitions, each sort consumer was limited to ~12.5MB from a flat 100MB pool, causing merge_insert operations with large payloads to fail with "not enough memory to continue external sort" at very small batch sizes (e.g. 5 rows with 1MB payloads). - Scale the default pool size to 100MB **per partition** so each consumer gets a reasonable allocation. Explicit `LANCE_MEM_POOL_SIZE` or `mem_pool_size` settings are respected as-is. - This is a partial fix — very large batches can still exhaust the per-partition budget. A more complete fix may involve revisiting the pool type or spilling behavior for merge_insert. ## Test plan - [x] Added unit test `test_mem_pool_size_scales_with_partitions` verifying pool size scales correctly - [x] Verified with a Python repro script that merge_insert with 1MB-per-row payloads no longer fails at 5 rows (now succeeds up to ~50 rows) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 682cdd4 commit 9d3d1ef

1 file changed

Lines changed: 35 additions & 3 deletions

File tree

rust/lance-datafusion/src/exec.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,21 +313,22 @@ impl std::fmt::Debug for LanceExecutionOptions {
313313
}
314314
}
315315

316-
const DEFAULT_LANCE_MEM_POOL_SIZE: u64 = 100 * 1024 * 1024;
316+
const DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION: u64 = 100 * 1024 * 1024;
317317
const DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE: u64 = 100 * 1024 * 1024 * 1024; // 100GB
318318

319319
impl LanceExecutionOptions {
320320
pub fn mem_pool_size(&self) -> u64 {
321+
let num_partitions = self.target_partition.unwrap_or(1) as u64;
321322
self.mem_pool_size.unwrap_or_else(|| {
322323
std::env::var("LANCE_MEM_POOL_SIZE")
323324
.map(|s| match s.parse::<u64>() {
324325
Ok(v) => v,
325326
Err(e) => {
326327
warn!("Failed to parse LANCE_MEM_POOL_SIZE: {}, using default", e);
327-
DEFAULT_LANCE_MEM_POOL_SIZE
328+
DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION * num_partitions
328329
}
329330
})
330-
.unwrap_or(DEFAULT_LANCE_MEM_POOL_SIZE)
331+
.unwrap_or(DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION * num_partitions)
331332
})
332333
}
333334

@@ -1051,4 +1052,35 @@ mod tests {
10511052
);
10521053
}
10531054
}
1055+
1056+
#[test]
1057+
fn test_mem_pool_size_scales_with_partitions() {
1058+
let default_per_partition = DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION;
1059+
1060+
// No partitions specified → defaults to 1 partition
1061+
let opts = LanceExecutionOptions::default();
1062+
assert_eq!(opts.mem_pool_size(), default_per_partition);
1063+
1064+
// 4 partitions → 4x the per-partition size
1065+
let opts = LanceExecutionOptions {
1066+
target_partition: Some(4),
1067+
..Default::default()
1068+
};
1069+
assert_eq!(opts.mem_pool_size(), default_per_partition * 4);
1070+
1071+
// 8 partitions → 8x the per-partition size
1072+
let opts = LanceExecutionOptions {
1073+
target_partition: Some(8),
1074+
..Default::default()
1075+
};
1076+
assert_eq!(opts.mem_pool_size(), default_per_partition * 8);
1077+
1078+
// Explicit mem_pool_size is not scaled
1079+
let opts = LanceExecutionOptions {
1080+
mem_pool_size: Some(50 * 1024 * 1024),
1081+
target_partition: Some(8),
1082+
..Default::default()
1083+
};
1084+
assert_eq!(opts.mem_pool_size(), 50 * 1024 * 1024);
1085+
}
10541086
}

0 commit comments

Comments
 (0)