goankur opened a new pull request, #16705:
URL: https://github.com/apache/lucene/pull/16705
# Prefetch full-precision vectors when rescoring
Fixes the case where a BBQ/scalar-quantized rerank shortlist turns into one
dependent blocking read
per candidate, and makes `KnnVectorValues#prefetch` able to express a
contiguous run.
Follow-up to the discussion in #16656, which this replaces. Thanks @jimczi
for the review that
redirected it onto the existing `IndexInput#prefetch` hook.
### The problem
`RescoreTopNQuery` scored one candidate at a time and never called
`IndexInput#prefetch`, so a
shortlist of M candidates became M dependent blocking reads. Measured over a
10,000-query run, the
JVM issued **23 `madvise` calls in total** — the rerank phase ran at queue
depth 1.
A caller that did prefetch could not reach the store either. The
`FloatVectorValues` returned by
`getFloatVectorValues()` for a quantized field
(`Lucene104ScalarQuantizedVectorsReader.ScalarQuantizedVectorValues`)
forwards `vectorValue()` and `rescorer()` but not `prefetch()`, so it
silently inherited the no-op
default. Same for `ScalarQuantizedFloat16VectorValues` and
`NormalizedFloatVectorValues`. Nothing
failed; prefetching just did nothing on the rerank path.
### Changes
- **`KnnVectorValues#prefetch(int ord, int count)`** is now the primitive.
Vectors are laid out
contiguously by ordinal, so a run of them is a single read rather than one
per ordinal. It returns
the number of vectors a prefetch was actually issued for, generalizing the
boolean returned by
`IndexInput#prefetch` ("true if prefetch actually prefetched something").
`count` is clamped to
the vectors remaining after `ord`.
- **`prefetch(int[] ords, int numOrds)`** now coalesces ordinals that are
consecutive in the array
into one call — the common shape on the HNSW neighbour path — and keeps
skipping a lone ord that is
about to be read anyway (`TestOffHeapVectorValues` pins this).
- **Forward `prefetch` from the wrappers**: `ScalarQuantizedVectorValues`,
`ScalarQuantizedFloat16VectorValues`, `NormalizedFloatVectorValues`. This
was the silent break.
- **`DoubleValues#prefetch(int doc)`** (default no-op), implemented by
`FullPrecisionFloatVectorSimilarityValuesSource` over a separate `copy()`
view, so running ahead
does not disturb the iterator that `advanceExact()`/`doubleValue()` rely
on.
- **`RescoreTopNQuery`** starts the loads for a whole segment's candidates
before scoring any of
them. Only doc ids are buffered, never `count × dim` floats. Deferral is
disabled when
`valuesSource.needsScores()`, since `DoubleValuesSource.fromScorer` reads
the scorer's current
score.
- **Sandbox dedup codecs** override `prefetch(int ord, int count)` via a
shared
`DedupUtil.prefetchRemapped`: they map field ords to group ords, so
consecutive field ords are not
necessarily contiguous on disk. Without the override they would inherit
the no-op default, which is
the same silent break as above.
- **Test**: `TestRescoreTopNQueryPrefetch` counts `IndexInput#prefetch` per
file extension through
slices and clones, and asserts `.vec` is prefetched about once per
rescored candidate. It fails
with `.vec=0` if any forwarding override is dropped.
### Measurements
g6.4xlarge, kernel 6.1, local NVMe. 25M Cohere-v3 1024-d, 1-bit BBQ + fp32
rerank,
`overSample 5 / fanout 100`, `nquery=10000`, `topK=100`. Each run in a 10 GB
systemd scope against
~101 GB of vectors, page cache dropped first. Recall **0.967 in every row**.
`madvise` and page-fault
counts via bpftrace, device via `iostat -x`. MGLRU off.
Single-stream p99:
| read path | p99 | aqu-sz | avg read |
|---|---|---|---|
| before this PR (mmap, nothing prefetches) | 252.0 ms | — | 27.8 KB |
| this PR, `MemorySegmentIndexInput#prefetch` backoff as-is | 221.0 ms | — |
19.7 KB |
| this PR, with the #16145 backoff fix | **17.09 ms** | 2.91 | 4.22 KB |
Two things worth separating out:
- The 27.8 KB average read is **not** read amplification from mmap.
`MADV_RANDOM` is applied and
active (`--enable-native-access` on), and with prefetching the reads are
4.22 KB. The inflated
figure is an artifact of nothing prefetching.
- **The power-of-two backoff in `MemorySegmentIndexInput#prefetch` (#16145)
gates most of the win
here: 221 → 17.09 ms, 13x.** It is invisible in a pure-cold microbench
(#16279 measures 3.389 vs
3.394 ops/ms with it disabled) because every miss resets the counter. It
only bites when the access
pattern mixes hits and misses, which is what rerank does. That fix is
worth landing on its own.
Reducing the number of `madvise` calls is actively harmful on this path:
prefetching every 2nd
candidate halves the calls and makes p99 **7.7x worse** (17.1 → 131.4 ms,
faults 35 → 389 per
query). Each 12.2 µs `madvise` buys away an ~88 µs fault. Burst size is flat
(64/128/512 candidates
before scoring: 16.55/16.91/16.77 ms), so in-flight depth is not issue-rate
limited here.
### Two questions for reviewers
1. **Return value of `prefetch(int ord, int count)`.** I read it as "the
number of vectors a prefetch
was actually issued for, 0 if none," to mirror `IndexInput#prefetch`'s
boolean. If it should
instead mean "how far the caller may advance" or something else, it is a
one-line change in the
two off-heap classes plus the javadoc.
2. **`DoubleValues#prefetch(int doc)`.** This widens a very general API to
serve the rerank path.
The alternative is for `RescoreTopNQuery` to special-case a vector-values
source and drive
`values.prefetch(ord)` / `values.rescorer()` directly in the leaf loop,
with no new method on
`DoubleValues`. Happy to switch if that is preferred.
### Not in this PR
Issuing the prefetches on `IndexSearcher#getTaskExecutor()` rather than all
on the calling thread is
a further ~1.6x on p99 (17.09 → 10.49 ms single-stream) and is orthogonal —
it helps every read path,
not just this one. It will follow as a separate PR on top of this one.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]