Tweak behavior of adaptive L - #1354
Tweak behavior of adaptive L#1354Magdalen Dobson Manohar (magdalendobson) wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refines DiskANN’s inline filtered search behavior when using adaptive-L, aiming to avoid pathological over-expansion of L_search when sampling finds zero matches and to allow more gradual resizing as sampling progresses. It also updates the inline graph test baselines and expectations to reflect the adjusted adaptive-L behavior, and makes the adaptive-L unit tests more tolerant to minor floating-point variability in log10-driven calculations.
Changes:
- Update adaptive-L to treat “0 matches” as an estimated specificity of
1 / visited, avoiding immediate jumps to the max multiplier. - Allow repeated adaptive-L resize opportunities at doubling sample thresholds (N, 2N, 4N, …) instead of only once.
- Refresh inline search test expectations/baselines and add tolerance in adaptive-L unit tests to reduce flakiness from floating-point nondeterminism.
Reviewed changes
Copilot reviewed 2 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| diskann/src/graph/search/inline_filter_search.rs | Adjusts adaptive-L sampling/resizing logic; updates adaptive-L unit tests for tolerance. |
| diskann/src/graph/test/cases/inline.rs | Updates test expectations and test naming/docs to match the new adaptive-L behavior. |
| diskann/test/generated/graph/test/cases/inline/inline_search_three_level_no_adaptive_l_with_l2_finds_no_matches.json | Updates generated baseline for the renamed/adjusted three-level non-adaptive-L test (L=2). |
| diskann/test/generated/graph/test/cases/inline/inline_search_three_level_adaptive_l_with_l2_finds_matches.json | Updates generated baseline for the renamed/adjusted three-level adaptive-L test (L=2). |
| diskann/test/generated/graph/test/cases/inline/inline_adaptive_l_max.json | Updates generated baseline to reflect new incremental growth behavior (fewer results returned). |
| diskann/test/generated/graph/test/cases/inline/inline_adaptive_l_logarithmic.json | Updates generated baseline values consistent with the revised adaptive-L behavior. |
| diskann/test/generated/graph/test/cases/inline/inline_adaptive_l_linear.json | Updates generated baseline values and expected IDs consistent with the revised adaptive-L behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if new_l > scratch.best.capacity() { | ||
| scratch.resize(new_l); | ||
| } |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1354 +/- ##
=======================================
Coverage 91.55% 91.56%
=======================================
Files 521 521
Lines 100347 100390 +43
=======================================
+ Hits 91877 91922 +45
+ Misses 8470 8468 -2
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Mark Hildebrand (hildebrandmw)
left a comment
There was a problem hiding this comment.
Thanks Magdalen - I have some concerns about the increased complexity and loss of test accuracy. Please see the inline comments.
| scratch.hops += scratch.beam_nodes.len() as u32; | ||
|
|
||
| // Adaptive L: after enough samples, estimate specificity and scale L. | ||
| // Adaptive L: estimate specificity at N samples, then at 2N, 4N, and so on. |
There was a problem hiding this comment.
Silly nit: May want to add one more: 2N, 4N, 8N since this plausibly looks like the sequence could be 2N, 4N, 6N.
| adaptive_l.scale_factor, | ||
| ); | ||
| if new_l > l_search { | ||
| if new_l > scratch.best.capacity() { |
There was a problem hiding this comment.
I'll agree with the bot - we probably want scratch.best.search_l() here.
| /// This will boost `l` to 10. The additional point 44 requires this larger `l` to hit. | ||
| /// We do not expect `43` to be hit. | ||
| /// This will boost `l` to 10. Re-evaluating at each 2x sample threshold allows the | ||
| /// search to reach the additional points 44 and 43. |
There was a problem hiding this comment.
Including "43" in the test results defeats the purpose of the test. Since all matching points are now returned, we are no longer testing that the search window is upper-bounded.
While this can be fixed by changing the point to "42", I have another concern. The behavior of the algorithm becomes much harder to predict once multiple sample points are introduced. Before when there was a single sample point and change to L, it was easier to understand and test. Now that there are multiple rounds, this gets harder. For this test case, the behavior looks something like:
- Sample at 10 visited with 2 matches. Specificity = 0.2 so we double the original L-search (5) to 10.
- Sample at 20 visited with 2 matches. Specificity = 0.1, so we double the original L-search (5) to 10.
- Sample at 40 visited with 2 matches. Specificity = 0.05, so we change the original L-search (5) to 12 and thus pick up the extra point that used to be on the boundary.
I also suspect that changing L-search later into a search yields diminishing returns since nodes that have already fallen off the back of the queue cannot be recovered. This can kind of be seen in graph/test/cases/inline/inline_adaptive_l_max which (1) is no longer testing the maximum search case, so is now misnamed and (2) goes from finding 3 of the 4 matching points to just 1 (albeit, with less exploration).
Can we find an alternate way to avoid this complexity? For example, resample only when matched == 0 and then stop resampling once at least one matched point is found? Alternatively, if this is the best approach, can the tests be adjusted so corner cases and bounds are still exercised and checked?
| /// No matching items are found durihng the sample window. Adaptive will boost the | ||
| /// window size to the max. | ||
| /// No matching items are found during the initial sample window. With 2x sample | ||
| /// thresholds, adaptive L grows incrementally and reaches the nearest match. |
There was a problem hiding this comment.
See my other comment: this case is now either misnamed or is not set up incorrectly.
Adaptive L search had a bit of strange behavior at the margins with the interaction of
sample_countand resizing. If no matching samples were found when a resize was attempted, it would default to the maximum multiplier, meaning that ifsample_countis relatively low, and selectivity is low but not nearly low enough to prompt resizing to the maximum multiplier, you could see an unnecessary, very large increase inL_search. This PR fixes that issue by resizing to1/sample_countif no matching elements were found, and then allowing more than one opportunity for the queue to resize, allowing a resize attempt each time the number of comparisons doubled. This allows for more graceful behavior, and makes it harder to enact a pathological mismatch between sampling and resizing.Along the way, after Mark noticed some issues with nondeterminism in
log10, it adds some tolerance to the unit tests that depend on that function.