Skip to content

Vector Set recall smoke tests - #1990

Draft
badrishc wants to merge 4 commits into
mainfrom
badrishc/vector-set-recall-smoke-tests
Draft

Vector Set recall smoke tests#1990
badrishc wants to merge 4 commits into
mainfrom
badrishc/vector-set-recall-smoke-tests

Conversation

@badrishc

@badrishc badrishc commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What

Recall-oriented smoke tests for Garnet Vector Sets that stress a DiskANN graph across the physical storage configurations that matter for disk-tiered operation. (A CopyUpdater sizing change was briefly included and has since been reverted — see the last section.)

Smoke test coverage

  • quantization mode — NOQUANT / Q8 / BIN
  • larger-than-memory — records spill to the storage tier during load, or are evicted to disk after an in-memory build
  • read-cache vs copy-reads-to-tail vs neither
  • save → restart → recover, optionally recovering into a much smaller log

Invariant

Physical robustness: a graph that answers queries well while resident in memory must keep answering them well once the same records are served from disk or recovered from a checkpoint.

Each test builds a small, well-clustered, deterministic graph (Dim=32, 16 clusters, M=16, EF=64, fixed seed, single connection, sequential VADD), measures recall@10 against a brute-force ground truth, applies a physical stressor, and asserts recall does not collapse (MaxRecallDrop = 0.20). Spill is forced and verified with DEBUG FLUSHANDEVICT + the store head/tail addresses. Everything is single-threaded and the graphs are intentionally tiny, so the whole suite runs in ~30s.

Tests (18 cases)

Test Coverage
RecallSurvivesFlushAndEvict(quant, {none/readcache/copytotail}) larger-than-memory served from disk × read-cache combinations
RecallSurvivesLargerThanMemoryLoad(quant) graph built while spilling to disk (256k log / 32k pages)
RecallSurvivesSaveAndRecover(quant, recoverIntoSmallerLog) save → restart → recover, optionally into a much smaller (disk-tiered) log

Known-failing cases (deliberate regression guards)

The Q8 cases that read from disk (FlushAndEvict, larger-than-memory load, and recover) currently fail against diskann-garnet 4.0.0. The per-dimension Q8 quantization table is native in-memory-only state that is lost when the index is recreated after eviction/recovery and cannot be rebuilt, so stored codes are decoded with a missing table and recall collapses (~1.0 to ~0.03-0.33). NOQUANT and BIN (table-free) are robust across every configuration.

Current result against main (diskann 4.0.0): 12 pass / 6 fail, where the 6 failures are exactly the Q8 disk cases:

evict         Q8 [none/readcache]              1.000 -> 0.092
evict         Q8 [copytotail]                  1.000 -> 0.045
largerthanmem Q8                               0.333
recover       Q8 [recoverIntoSmallerLog=False] 1.000 -> 0.195
recover       Q8 [recoverIntoSmallerLog=True]  1.000 -> 0.073

These are intentional regression guards for the Q8 disk-recall bug - they should start passing once the quantization table is persisted with (or rebuildable for) a recreated/recovered index. Opening as draft since these Q8 cases are expected red until that fix lands.

CopyUpdater "Destination is too short" — investigated, fix reverted

An earlier commit sized the CopyUpdater destination to max(WriteDesiredSize + ValueAlignmentBytes, existingValue.Length) to stop a rare ArgumentException ("Destination is too short") seen during a BIN larger-than-memory build at 4 KB pages. That commit has been reverted after root-cause investigation showed it guards a condition that does not occur.

Instrumented runs (per-(namespace,key) write-size tracking with liveness confirmation, plus a disk-chain-walk namespace/key match check, run isolated and across the full smoke suite at 4 KB pages under CPU stress) established:

  • DiskANN RMWs never change a record's size. Exactly two copy/inplace shapes are produced and each is perfectly constant across a build (a 68 B inline NeighborList and an 8192 B overflow record). Zero size changes were ever observed for any key.
  • The pending (disk) CopyUpdater always matches the correct key and namespace for DiskANN records (zero mismatches). GarnetKeyComparer's generic path compares NamespaceBytes + KeyBytes, so a small-record RMW never copy-updates a larger record.

Because the source value equals WriteDesiredSize + ValueAlignmentBytes for a correctly-matched, size-stable record, oldValueAligned.Length always equals newValueAligned.Length and the CopyTo cannot overflow in normal operation. The max(...) sizing therefore protects against nothing that happens; worse, if ValueSpan.Length were ever transiently corrupt (large) it would attempt a huge allocation (OOM) rather than fail fast. The original crash was most plausibly the separate index-record recreate path (already correctly sized in HEAD via VectorManager.IndexSize) or an extremely rare eviction/pending-IO race; either way it must be root-caused as a race/corruption, not sized around.

Notes

  • Follows the existing Garnet.test.vectorset conventions (TestBase, TestUtils.CreateGarnetServer, VADD/VSIM via StackExchange.Redis, binary int element ids).

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

badrishc and others added 2 commits July 28, 2026 16:49
Adds VectorSetRecallSmokeTests: recall-oriented smoke tests that stress a
DiskANN Vector Set graph across the physical storage configurations that
matter for disk-tiered operation:

  * quantization mode (NOQUANT / Q8 / BIN)
  * larger-than-memory: records spill to the storage tier during load, or
    are evicted to disk after an in-memory build
  * read-cache vs copy-reads-to-tail vs neither
  * save -> restart -> recover, optionally re-evicting the recovered graph

The invariant under test is physical robustness: a graph that answers
queries well in memory must keep answering them well once the same records
are served from disk or recovered from a checkpoint. Each test builds a
small, well-clustered, deterministic graph (Dim=32, fixed seed, single
connection, sequential VADD), measures recall@10 against a brute-force
ground truth, applies a physical stressor, and asserts recall does not
collapse. Spill is forced and verified with DEBUG FLUSHANDEVICT plus the
store head/tail addresses. The graphs are intentionally tiny so the whole
suite runs in ~30s.

Known-failing cases: the Q8 cases that read from disk (FlushAndEvict,
larger-than-memory load, and recover) currently fail against
diskann-garnet 4.0.0. The per-dimension Q8 quantization table is native
in-memory-only state that is lost when the index is recreated after
eviction/recovery and cannot be rebuilt, so stored codes are decoded with a
missing table and recall collapses (~1.0 -> ~0.03-0.33). NOQUANT and BIN
(table-free) are robust across every configuration. These failures are
deliberate regression guards for that bug, not flaky tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Recovering a checkpoint onto a "smaller box" (a much smaller log) must keep
the Vector Set working correctly. Reworks RecallSurvivesSaveAndRecover so the
`recoverIntoSmallerLog` case recovers into a 64k log (down from 8m) while
holding the page size fixed at a valid 16k, so the recovered graph no longer
fits in memory and is served from disk. Asserts the records actually spilled
and that recall is preserved.

Verified: NOQUANT and BIN preserve recall (1.0) when recovered into the
smaller log; only Q8 collapses, which is the known quantization-table bug.
(An earlier iteration recovered with the lowMemory helper, which forces a 4k
page size below the 16k minimum the server enforces for Vector Sets; that is
an unsupported geometry and is not what "recover to a smaller log" means.)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
return total / data.Queries.Length;
}

private static int[] BruteForceTopK(float[][] vectors, float[] query)

@tiagonapoli tiagonapoli Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: we have DiskANNSyntheticRecallTests which has methods duplicated with some here, maybe worth evaluating if it's possible to reuse? Or updating DiskANNSyntheticRecallTests to add fixtures that will also add quantization and spilling to disk?

VectorSessionFunctions.CopyUpdater copies the full existing value into the
new record (so the DiskANN read-modify-write callback can read it back)
before the callback runs. The new record was sized from
GetRMWModifiedFieldInfo to only WriteDesiredSize + ValueAlignmentBytes, which
is smaller than the existing value when a record is rewritten to a smaller
size. In that case oldValueAligned.CopyTo(newValueAligned) overflows the
destination and throws ArgumentException ("Destination is too short"),
crashing the server. This only occurs on the pending (disk) CopyUpdater path
— in-memory updates use InPlaceUpdater, which does not copy — so it surfaces
under heavy eviction (e.g. a small page size / large graph).

Size the destination to max(WriteDesiredSize + ValueAlignmentBytes,
existingValue.Length) so the copy can never overrun the destination,
regardless of whether the update grows or shrinks the record. The common
(growing / same-size) case is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@badrishc badrishc changed the title Add recall smoke tests for Vector Set physical configurations Vector Set recall smoke tests + CopyUpdater shrink-sizing fix Jul 29, 2026
…ecords"

This reverts commit 9a71407.

The fix guarded a condition that does not occur. Instrumented investigation
(per-(namespace,key) write-size tracking with liveness confirmation, plus a
disk-chain-walk namespace/key match check, run isolated and across the full
smoke suite at 4 KB pages under CPU stress) established:

- DiskANN neighbor/quant/FSM RMWs never change a record's size. Exactly two
  copy/inplace shapes are produced and each is perfectly constant across a
  build (NeighborList 68 B inline; the 8192 B overflow record). Zero size
  changes were ever observed for any key.
- The pending (disk) CopyUpdater always matches the correct key AND namespace
  for DiskANN records (zero mismatches). GarnetKeyComparer's generic path
  compares NamespaceBytes + KeyBytes and correctly distinguishes the two
  namespaces, so a small-record RMW never copy-updates a larger record.

Given the source value equals WriteDesiredSize + ValueAlignmentBytes for a
correctly-matched, size-stable record, oldValueAligned.Length always equals
newValueAligned.Length, so the CopyTo cannot overflow in normal operation.
max(WriteDesiredSize + ValueAlignmentBytes, value.Length) therefore protects
against nothing that happens; worse, if ValueSpan.Length were ever transiently
corrupt (large), it would attempt a huge allocation (OOM) instead of failing
fast. Any future "Destination is too short" here must be root-caused as a
race/corruption, not sized around.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@badrishc badrishc changed the title Vector Set recall smoke tests + CopyUpdater shrink-sizing fix Vector Set recall smoke tests Jul 29, 2026
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