Repository navigation
Fix CAGRA dataset descriptor race across threads sharing a handle #2807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,8 @@ | |
|
|
||
| #include <cuvs/neighbors/common.hpp> | ||
|
|
||
| #include <mutex> | ||
|
|
||
| namespace cuvs::neighbors::cagra::detail { | ||
|
|
||
| template <typename DataT, | ||
|
|
@@ -147,6 +149,8 @@ struct store { | |
| raft::cache:: | ||
| lru<key, key_hash, std::equal_to<>, dataset_descriptor_host<DataT, IndexT, DistanceT>> | ||
| value{kDefaultSize}; | ||
| /** Serializes lookup-or-insert, so that concurrent searches share a single descriptor. */ | ||
| std::mutex mutex; | ||
|
Comment on lines
+152
to
+153
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The mutex lives in the per- Question for reviewers: One lifetime hazard is not changed here. When the last copy of a descriptor is dropped (for example after LRU eviction), |
||
| }; | ||
|
|
||
| } // namespace descriptor_cache | ||
|
|
@@ -170,9 +174,17 @@ auto dataset_descriptor_init_with_cache(const raft::resources& res, | |
| -> dataset_descriptor_host<DataT, IndexT, DistanceT> | ||
| { | ||
| auto key = descriptor_cache::make_key(params, dataset, metric, dataset_norms); | ||
| auto& cache = | ||
| raft::resource::get_custom_resource<descriptor_cache::store<DataT, IndexT, DistanceT>>(res) | ||
| ->value; | ||
| auto* store = | ||
| raft::resource::get_custom_resource<descriptor_cache::store<DataT, IndexT, DistanceT>>(res); | ||
| auto& cache = store->value; | ||
| // Hold the lock across the lookup and the insertion. Otherwise threads that miss concurrently | ||
| // each create a descriptor and insert it under the same key (the LRU cache does not deduplicate | ||
| // keys), so concurrent searches can end up with different descriptors. When the last owner of | ||
| // one releases it, its device memory is freed with `cudaFreeAsync` on the stream that | ||
| // initialized it, which is not ordered after a kernel on another thread's stream that can still | ||
| // be reading it. Only host-side work happens under the lock (the device-side initialization is | ||
| // deferred to the first `dev_ptr()` call). | ||
| std::lock_guard<std::mutex> lock(store->mutex); | ||
|
Comment on lines
+180
to
+187
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Commit 2, a second and narrower hazard. Question for reviewers: Reading |
||
| dataset_descriptor_host<DataT, IndexT, DistanceT> desc; | ||
| if (!cache.get(key, &desc)) { | ||
| desc = | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the fix (commit 1).
ready_tstores thecuda::stream_refthat ran the init kernel (eval). The old code compared it with the caller's stream and skipped the wait when they were equal. That comparison is on handle values, and with raft's defaultcudaStreamPerThreadevery thread has the same handle value but its own stream. The wait is now unconditional. When the caller really is on the init stream, the event has already been recorded on it, so the extracudaStreamWaitEventadds no ordering. The filter payload cache already does it this way (cagra_filter_payload.hpp).I checked this commit alone before CI: I patched the nightly 26.10
libcuvs.sosostate::getalways waits. On a local GPU,AnnMG/5:/8went from 4/6 processes aborting to 0/6, andAnnMG/14(ROUND_ROBIN) went from 34/200 failing tests to 0/200 (#2783 comment).