fix(index): serialize spill-sort tests and skip NaN centroid initialization - #8819
Closed
u70b3 wants to merge 2 commits into
Closed
fix(index): serialize spill-sort tests and skip NaN centroid initialization#8819u70b3 wants to merge 2 commits into
u70b3 wants to merge 2 commits into
Conversation
Contributor
Author
|
@Xuanwo could you take a look when you have a moment? This PR fixes the two most frequent flake families from #8789:
Worth noting the second one is also blocking lance-gatefixer's #8767, so landing this unblocks that pipeline too. CI is fully green, including linux-arm and the Python jobs that exercise both original failure environments. |
u70b3
force-pushed
the
fix/json-sort-mem-pressure
branch
from
August 28, 2026 09:06
d30d02d to
b2298ac
Compare
Contributor
There was a problem hiding this comment.
✅ Gate recommendation: approve.
The rebase leaves both fixes unchanged at their intended boundaries: spill-enabled tests are serialized around the process-wide pool, while accelerated IVF initialization excludes non-finite candidates and reports insufficient finite data clearly.
Xuanwo
added a commit
that referenced
this pull request
Aug 31, 2026
## Problem Plain `cargo test` runs `lance-index` tests in a single process. Spill-enabled index builds therefore share the cached 150 MiB DataFusion memory pool, and concurrent 40 MiB `ExternalSorterMerge` reservations can exhaust it. This caused the [Linux ARM main job](https://github.com/lance-format/lance/actions/runs/33294579023/job/99211936332) to fail in an otherwise unrelated JSON index test. ## Change Put all 15 spill-sort tests in the named `LANCE_DF_SPILL_POOL` `serial_test` resource group. This preserves the production memory-pool configuration while keeping unrelated tests parallel, and replaces the JSON-only mutex with one shared test resource. This supersedes the Rust test-isolation portion of #8819. That PR currently conflicts and also bundles the already-landed Python NaN fix. ## Validation - `cargo test -p lance-index --lib --features geo -- --test-threads=16` (3 consecutive runs, 1191 passed / 0 failed each) - `cargo clippy --all --tests --benches -- -D warnings` - `cargo fmt --all -- --check`
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two CI flake fixes from the consolidated tracking in #8789:
test_json_btree_index_null_at_pathfailing with "Not enough memory to continue external sort" — 3 occurrences on 2026-08-26 across independent PRs, all in the linux-arm job).test_torch_index_with_nansfailing withStopIteration— 4+ occurrences across PRs and Python versions; confirmed in fix(index): make IVF-PQ test training deterministic #8767 to be a separate, uncovered root cause).Fix 1: serialize spill-sort tests against the shared memory pool
Root cause
SortExecreserves a non-spillable 40MB merge buffer on its first input batch (sort_spill_reservation_bytes = min(pool/3, 40MB)inrust/lance-datafusion/src/exec.rs), so the default 150MBFairSpillPoolfits exactly three concurrent spill sorts; a fourth always fails withResourcesExhausted.get_session_context), so the collision domain is the process. The error text confirms it: three foreignExternalSorterMergereservations of 40MB each + the victim's own 4-row sorter, 30MB remaining, 40MB requested.cargo test→ the wholelance-indexlib binary shares one process, one cached session, one 150MB pool. Any four of the crate's 15 spill-sort tests in flight can exhaust it — which is why all three observed failures were linux-arm only, and intermittent (the reservations are held for ~ms, so collisions need contended runners).FLOAT_INDEX_CASE_GUARDalready serialized two json tests for exactly this reason (fix(index): sort JSON-path values once after extraction, not the raw column #7835), but the rtree tests (8), the btree-update tests (2), and json's own update tests (3) were unguarded.Fix
Promote the guard to a crate-wide
SPILL_POOL_TEST_GUARD(documented at the definition) and hold it in every test that drives a spill-enabled execution: json (5), rtree (8), btree update (2). Undercargo testthis serializes them — one 40MB reservation against a 150MB pool, exhaustion impossible by construction. Under nextest it is a no-op. No production code touched.Fix 2: skip non-finite vectors in torch IVF centroid initialization
Root cause
test_torch_index_with_nanswrites 320 vectors, 8 of them float-NaN (not Arrow nulls, so thevector is not nullfilter doesn't remove them). Intrain_ivf_centroids_on_acceleratorthe initial centroids are drawn from a random sample (python/python/lance/vector.py):num_partitions=1, the init draw is a single row — P(NaN row) = 8/320 = 2.5% per run, matching the observed intermittency._l2_distancemaps NaN → partition id -1), so kmeans reportstotal_dist == 0and "converges" on the first epoch without ever updating the centroids.compute_partitionsthen drops every row (partitions >= 0mask), the residuals dataset ends up empty, and the PQ init sample yields zero batches —next(iter(ds_init))raisesStopIteration.This is a production-path bug, not just a test issue: any NaN-containing vector column + accelerator one-pass IVF-PQ could produce an empty index or the cryptic
StopIteration.Fix
New
_sample_finite_vectorshelper (python/python/lance/vector.py): draws init vectors from the sample stream, skipping rows with any NaN/inf values, untilkfinite vectors are collected; raises a descriptiveValueError(with scanned/available counts) when the data can't provide them. Applied intrain_ivf_centroids_on_acceleratorwhenfilter_nanis set — extending that parameter's existing intent (it already filtered Arrow nulls) to float-NaN values.Verification
cargo test -p lance-index --lib --features geo×3: 1145 passed each; targeted spill tests ×5 (--test-threads=16): 28 passed each; linux-arm CI job on this PR green (the original failure environment).python/tests/test_vector_index.py— deterministic pre-fix failure / post-fix pass (verified by reverting the change): init batch is all-NaN in storage order; all-NaN column raises the descriptive error; helper unit test covers tensor and dict batch forms. The originaltest_torch_index_with_nans[V3/Legacy]passes locally.uv run make lint·cargo clippy -p lance-index --all-features --tests -- -D warnings·cargo fmt --allAlternatives considered
LANCE_MEM_POOL_SIZEonly on the linux-arm job: works, but hides the hazard in CI config and doesn't tell future test authors why their new spill test flakes. Switch linux-arm to nextest: process isolation eliminates this whole class, but that's a CI-policy call for maintainers; this fix is correct under either runner.Partially addresses #8789 (families 1 and 2 of the consolidated list).