Skip to content

Reduce disk reads on the tiered Vector Set search path - #2007

Open
badrishc wants to merge 2 commits into
mainfrom
badrishc/vector-tiered-read-efficiency
Open

Reduce disk reads on the tiered Vector Set search path#2007
badrishc wants to merge 2 commits into
mainfrom
badrishc/vector-tiered-read-efficiency

Conversation

@badrishc

@badrishc badrishc commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

What

Two changes to the storage-tiered (disk-backed) DiskANN Vector Set read path:

  1. Cache the DiskANN Metadata term (copy-to-tail). It previously fell into the default read-copy branch (CopyTo=None) and was re-read from disk on every rerank candidate; it is now copied back to memory after first touch like the other per-node graph records (NeighborList, QuantizedVector, id maps). DiskANNService.Metadata is made internal so the policy can name it.
  2. Size the FullVector's initial disk read for a single IO. Widen VectorRecordReadOverheadBytes 64 → 128 so the whole record (RecordInfo + RecordDataHeader + namespace + key + value) lands in one sector-aligned IO. Over-sizing is free here (it rounds to the same sectors as the value alone); under-sizing forces a second IO.

Why

On the disk tier, even though the quantized vectors and graph adjacency are already memory-resident, two records were read from disk once per rerank candidate: the raw FullVector (expected — used for the exact-distance rerank) and the DiskANN Metadata term. The Metadata term is a small, shared structure (~154 records for a 10M graph) but at ~8 KB each and read ~193×/query it accounted for ~73% of disk bytes/query — more bytes than the vectors themselves — purely because it was never cached. Caching it costs ~1.3 MB of memory and removes it from the steady-state disk path entirely.

Impact

Cohere-10M (768-dim, COSINE), Q8, M=16 / l_build=300 / l_search=192 / k=100, warm disk-tiered serial (--storage-tier, Native/O_DIRECT):

metric before after
disk reads / query 386 193
disk bytes / query 2,176 KB 594 KB (−73%)
warm serial latency (avg) 86 ms 46.5 ms
disk peak QPS ~223 ~1,162
recall@100 0.8408 0.8408 (unchanged)

Tests

Garnet.test.vectorset — the VectorSetRecallSmokeTests (which assert recall survives eviction / copy-to-tail / save+recover across NOQUANT/Q8/BIN) pass on both net8.0 and net10.0; the full vectorset suite is green.

Notes / follow-up (not in this PR)

After this change the sole remaining steady-state disk cost is the FullVector rerank reads. These are currently issued by DiskANN one key at a time (the read callback is invoked with numKeys=1, ~193 serial calls/query), which caps the device queue depth (~30) well below the drive's IOPS ceiling. Garnet's batched-read path (ReadWithPrefetch) already issues reads in parallel for numKeys>1 (it is used for graph traversal today), so batching the rerank GET would be a small, self-contained DiskANN-side change with no Garnet change required — a promising follow-up for a further QPS lift.

On the storage-tiered (disk-backed) DiskANN search path, per-rerank-candidate
disk reads dominated query cost even though the quantized vectors and graph
adjacency were already memory-resident:

- The DiskANN Metadata term is a small, shared record read once per rerank
  candidate, but it fell into the default read-copy branch (CopyTo=None) and was
  re-read from disk on every query. Add it to the copy-to-tail branch alongside
  the other per-node graph records (NeighborList, QuantizedVector, id maps) so it
  is served from memory after first touch, bounding disk traffic to the visited
  working set. Making DiskANNService.Metadata internal so the policy can name it.

- The FullVector's initial disk read could require a second IO when the record
  overhead exceeded the 64-byte budget. Widen VectorRecordReadOverheadBytes to
  128 so the whole record (RecordInfo + RecordDataHeader + namespace + key +
  value) lands in a single sector-aligned IO. Over-sizing is free here (it rounds
  to the same sectors as the value alone); under-sizing forces a second IO.

Measured on Cohere-10M (768-dim, Q8, l_search=192), warm disk-tiered serial:
disk bytes/query 2,176 -> 594 KB (-73%), warm serial latency 86 -> 46.5 ms,
disk peak QPS ~223 -> ~1,162, recall unchanged (0.8408). Vector Set recall smoke
tests pass across NOQUANT/Q8/BIN on the evict / copy-to-tail / recover paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81de0152-2aa0-47b5-bf27-f0358619d578
Copilot AI review requested due to automatic review settings August 1, 2026 20:44

Copilot AI 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.

Pull request overview

This PR optimizes the disk-backed DiskANN Vector Set read path by reducing repeated storage-tier disk reads and improving initial disk read sizing for full-vector records.

Changes:

  • Cache DiskANN “Metadata” term records via read-copy (copy-to-tail/read-cache) so they are served from memory after first touch.
  • Increase VectorRecordReadOverheadBytes (64 → 128) to better ensure a single sector-aligned IO fetches the whole FullVector record.
  • Make DiskANNService.Metadata internal so the read-copy policy can reference it.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
libs/server/Resp/Vector/VectorManager.Callbacks.cs Adds Metadata to the read-copy policy and widens the initial disk-read sizing overhead used for vector-record reads.
libs/server/Resp/Vector/DiskANNService.cs Exposes the Metadata term constant at internal visibility for use by the vector read-copy policy.

Comment on lines +301 to +303
/// id key are 4 bytes each; 128 leaves generous slack for alignment and any per-value prefix. Over-sizing is
/// free here (the read is sector-aligned downstream, so this rounds to the same sectors as the value alone);
/// under-sizing would force a second IO.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — corrected in 2283af6. The comment now states the over-size is free only within the sector-alignment slack (while value+overhead rounds up to the same sectors the full record already occupies); a bump crossing a sector boundary would read an extra sector.

…lignment slack

Address PR review feedback: the initial-read-size overhead is not unconditionally
free. Downstream Tsavorite rounds the requested length up to the device sector
size, so the extra bytes add no IO only while value+overhead rounds up to the same
sectors the full record occupies; a bump that crossed a sector boundary would read
an additional sector. Doc-comment only; no behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81de0152-2aa0-47b5-bf27-f0358619d578
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants