Bound CAGRA hash table sizing loops so they cannot spin forever - #2533
Open
shaunakkapur wants to merge 1 commit into
Open
Bound CAGRA hash table sizing loops so they cannot spin forever#2533shaunakkapur wants to merge 1 commit into
shaunakkapur wants to merge 1 commit into
Conversation
calc_hashmap_params() sizes its hash tables by growing a bit length until the requested table fits, checking the supported maximum only after the loop. hashmap::get_size() is `1U << bitlen`, which is undefined once bitlen reaches 32 and in practice wraps back to a small value, so a request needing more than 2^31 entries left the loop condition permanently true. The bit length grew without bound, the post-loop RAFT_EXPECTS was never reached, and the search hung on the host instead of returning an error. Bound each of the four sizing loops by the same maximum its post-loop check already uses, so an oversized request exits the loop and raises the existing error. The limits are hoisted into local constants so the loop bound and the check cannot drift apart. check_params() only caps itopk_size at 1024 for SINGLE_CTA, so MULTI_CTA and MULTI_KERNEL were the reachable paths. Both are covered by a new regression test; note that a regression there resurfaces as a test timeout rather than a failed assertion. Verified on an RTX PRO 6000: itopk_size 1,100,000,000 previously hung indefinitely and now raises the hash_bitlen error in a few seconds, while valid searches are unaffected. A standalone sweep of the old and new loop logic over all four loops agrees on every in-range input. Closes NVIDIA#2523 Signed-off-by: Shaunak Kapur <shaunakk@nvidia.com>
shaunakkapur
force-pushed
the
fix/cagra-hashmap-sizing-loop-hang
branch
from
August 31, 2026 16:44
b871fac to
6da0dda
Compare
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.
Closes #2523.
Problem
search_plan_impl::calc_hashmap_params()sizes its hash tables with loops that grow a bit length until the requested table fits, and check the supported maximum only after the loop:hashmap::get_size()is1U << bitlen, which is undefined oncebitlenreaches 32. In practice the shift count is masked to 5 bits, soget_size(32)wraps to1,get_size(33)to2, and so on.hash_bitlenisint64_t, so for any request needing a table larger than2^31entries the loop condition stays permanently true, the bit length grows without bound, and theRAFT_EXPECTSis never reached. The search hangs on the host rather than returning an error.check_params()only capsitopk_sizeat 1024 forSINGLE_CTA, soMULTI_CTAandMULTI_KERNELare the two algorithms that can reach the sizing loops with an oversized request. A normal single-queryAUTOsearch resolves toMULTI_CTA.Fix
Bound each of the four sizing loops by the same maximum its post-loop check already uses, so an oversized request exits the loop and raises the existing error instead of spinning:
Each limit is hoisted into a local constant so the loop bound and the check cannot drift apart, which is the failure mode this bug came from.
All four loops are covered (lines 270, 285, 303, 343). Only 285, 303 and 343 are reachable with user controlled sizes; 270 is bounded by
graph_degreein practice and is bounded here for consistency.Behaviour
No change for any request that was previously accepted. A request that previously hung now raises the error it should always have raised. A request that previously errored still errors.
Testing
New regression test
cpp/tests/neighbors/ann_cagra/bug_issue_2523_hashmap_bitlen.cu, registered underNEIGHBORS_ANN_CAGRA_TEST_BUGS, coveringMULTI_CTAoversized,MULTI_KERNELoversized, and validitopk_sizeon all three algorithms.Note that a regression here resurfaces as a test timeout rather than a failed assertion, since the failure mode is a hang. This is called out in the test file header.
I also checked that the test actually catches this. With the fix reverted and nothing else changed,
MultiCtaOversizedItopkThrowshangs and gets killed at the 300s timeout (exit 124) instead of failing an assertion. With the fix back in, it passes in about 1.6s.Verified on an RTX PRO 6000 Blackwell:
itopk_size = 1,100,000,000previously hung indefinitely (confirmed past a 60s timeout, with the thread stuck in native code). It now raises thehash_bitlenerror in a few seconds.NEIGHBORS_ANN_CAGRA_TEST_BUGSsuite: 10/10 passed, including the five pre-existing reproducers.pre-commithooks pass on the changed files.Notes for reviewers
hashmap::get_size()itself. Making it safe forbitlen >= 32(returninguint64_t, or asserting on its input) would remove the undefined behaviour at the source, but it is used in device code and by callers that assign touint32_t, so it is a wider change. Bounding the loops fixes the reachable defect locally. Happy to follow up onget_size()separately if you would prefer that hardening too.search_plan.cuhwas rewritten by theverify-copyrightpre-commit hook to the canonical form. That is unrelated to the fix but the hook applies it to any touched file.