Fix long-context search OOMs, improve search speed with auto batching, add index auto device placement - #46
Merged
Conversation
…atch sizes; packed query tensors; manual tuning via API if needed. 1. Memory-budgeted scoring chunks. Scoring batch/chunk sizes are computed automatically for both scoring stages (approximate and exact rerank) from a per-device memory budget. The default budget (search_memory_fraction=0.5) is 50% of the free VRAM at query time (sampled with torch). On CUDA OOM the stage retries with the budget halved. Batching/chunking only kicks in when needed: a single-chunk fast path keeps short queries at full speed, since chunking may introduce extra time. Exposed as search(batch_size='auto' | <int>): 'auto' (default) plans from the budget, an int forces fixed-size chunks in both stages. 2. Packed query tensors. Queries are passed as one packed 2D tensor plus per-query lengths instead of a padded 3D tensor - no padding memory waste on GPUs and a single host-to-device transfer. query_lengths is now required at the Rust FFI and the padded-3D path is removed; the public Python API is unchanged (packing happens internally). 3. Adaptive index placement. Exposed as index_gpu_memory='auto' | 'low' | 'medium' | 'high' (replaces low_memory): 'high' keeps codes and residuals on GPU, 'medium' keeps codes on GPU and residuals (the larger tensor) on CPU, 'low' keeps both on CPU. 'auto' (default) picks the highest tier that keeps total device occupancy within index_memory_fraction (default 0.7 of total VRAM), reserving the rest for the search step. 4. In-place MaxSim masking. The mask fill in MaxSim scoring is applied in place on the freshly materialized score tensor, avoiding an extra full-size GPU allocation. 5. No GPU load in create(). FastPlaid.create() no longer loads the index onto GPU; per-device loads are deferred to the first search, since the object built during creation would be discarded anyway. 6. Per-query search errors now propagate instead of being silently returned as empty results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
paulomouraj
marked this pull request as draft
July 23, 2026 13:23
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
paulomouraj
force-pushed
the
faster-eval-long-context
branch
from
July 23, 2026 15:01
d4bc0f2 to
dd94341
Compare
paulomouraj
marked this pull request as ready for review
July 24, 2026 10:00
raphaelsty
added a commit
that referenced
this pull request
Jul 31, 2026
low_memory was replaced in #46 by index_gpu_memory ('auto'|'low'|'medium'|'high') plus index_memory_fraction and search_memory_fraction, and search(batch_size=) now defaults to 'auto'. Update the quick start, retitle the speed-tip section to GPU Memory Tuning (with the anchor it is linked by), refresh the trade-off tables, and sync the documented __init__/search signatures with the source, including the previously undocumented n_processes.
raphaelsty
added a commit
that referenced
this pull request
Jul 31, 2026
low_memory was removed from FastPlaid.__init__ in #46, replaced by index_gpu_memory. The keyword form was already absorbed by **kwargs, but the positional form FastPlaid(index, device, low_memory), valid before 1.4.7, landed the bool in index_gpu_memory and raised ValueError. Treat a bool in that slot as the legacy flag, pop low_memory from kwargs, and emit a DeprecationWarning pointing at index_gpu_memory. The flag is ignored rather than mapped to a tier, so upgrading call sites get the adaptive 'auto' placement. Add TestLegacyArguments covering the keyword and positional forms, unknown kwargs, legacy int batch_size, and that validation of real index_gpu_memory values is unchanged.
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.
Why
When evaluating popular retrieval benchmarks with long context settings (query/doc lengths of 8192 tokens), some challenges appeared:
low_memory=Truewas needed, but this makes search much slower. The solution was then to manually keep residuals on CPU and only codes on GPU.All of this tuning overhead justifies implementing auto batching/chunking based on free GPU memory, avoiding possible OOMs with long queries/documents and avoiding slowing down the search step for datasets that do not require it. The same requirements applies for index placement, which is dataset specific and requires placing codes or residuals on CPU/GPU based on available GPU memory. In addition to that, some ops were wasting gpu memory: 3D query tensors may be huge depending on the longest query on the batch, and those could be replaced by packed 2D query tensors; MaxSim scoring creating big tensor copies to mask padded positions, when in-place fill-in was possible. Inside large tensor splitting/chunking, sorting documents before chunking enables minimal padding waste and thus lower gpu memory.
Summary of all changes
search_memory_fraction=0.5) at query time (sampled with torch). On CUDA OOM the stage retries with the budget halved. Batching/chunking only kicks in when needed: a single-chunk fast path keeps short queries at full speed, since chunking ops may introduce extra time. If users still want to manually tune it, they can do it forcing a int onbatch_size='auto' | <int>that will be used as approximate score batch size and exact score chunk size. Sorting is implemented wrt document lengths when chunking is enabled so that minimal padding is used for tensor ops, reducing gpu memory waste and potentially improving speed.index_gpu_memory='auto' | 'low' | 'medium' | 'high'(replaces low_memory): 'high' keeps codes and residuals on GPU, 'medium' keeps codes on GPU and residuals (the larger tensor) on CPU, 'low' keeps both on CPU. 'auto' (default) picks the highest tier that keeps total device occupancy withinindex_memory_fraction(default 70% of total VRAM), reserving the rest for the search step.End-to-end benchmark
Environment
faster-eval-long-context@dd94341Both builds compiled from source with maturin (release,
LIBTORCH_USE_PYTORCH=1) against the sametorch; identical eval stack. Search params both sides:
top_k=1000,n_full_scores=8192,n_ivf_probe=8. Vanilla fast-plaid uses PyLate (1.5.0) defaults (+low_memory=Truefor MLDR) while PR fast-plaid uses everything on auto (PR defaults).Per-task results (PLAID search step)
Notes:
overhead on a tiny query set, but PR uses 13.9 GiB while vanilla peaked at 70.8 GiB.
low_memory=True(index kept onCPU, much slower search) and/or lowering the query batch size (PyLate's retriever passes
batches of 50 queries into
search(); smaller batches shrink every scoring tensorproportionally, at a large speed cost). Others cannot: on MLDR the OOM occurs in the
exact-scoring step with
low_memory=Truealready set, and that tensor has a per-queryfloor of ~34 GiB (
n_full_scores=8192candidates × 8192-token docs × 128 dims), no batchsize fixes it, and lowering
n_full_scoreschanges retrieval results.