Fix IVF extend list resize bug#2297
Conversation
|
old_used_size is the old logical size. old real size is checked via extends. |
in ivf-flat extend, resize_list() receives |
|
here is poc after base extend after grow extend BUG: index-visible size is 120 but internal list::size stayed 100 |
|
Ok, sorry for closing this prematurely, this does seem legit. I'm not sure though if this internal get_size() is used where it could cause any problems. I'll look into this in the following days. |
|
Thank you very much! I forgot to mention how i even stumbled upon this - i got very strange Illegal memory access errors (i am using python wrapper for multiple extends + search + torch for calculating the vectors) and believe that this bug is related (IMA was in calc_chunk_indices_kernel) |
|
off-topic, figured out why i got IMA, NVIDIA/raft#3041, different issue. |
|
Everything that gets merged in the main branch before next burndown, gets automatically in the next release - the new release branch is forked from the main when burndown starts. Here's our schedule https://docs.rapids.ai/maintainers/ |
huuanhhuyn
left a comment
There was a problem hiding this comment.
Thank you for your contribution! I'd have 1 doubt.
| auto shared_list_size = old_used_size; | ||
| if (new_used_size <= old_used_size || | ||
| auto shared_list_size = old_logical_size; | ||
| if (new_used_size <= old_logical_size || |
There was a problem hiding this comment.
I guess there are 3 cases:
- new_used_size <= old_logical_size -> no size update, no reallocation
- old_logical_size < new_used_size <= old_used_size -> size update, no reallocation
- new_used_size > old_used_size -> size update, reallocation
If I understand it correctly, your current version will do both size update and reallocation for the 2nd case.
There was a problem hiding this comment.
If I understand it correctly, your current version will do both size update and reallocation for the 2nd case.
Only if orig_list->size.compare_exchange_strong(shared_list_size, new_used_size) returns false
it returns false if orig_list->size was already mutated before this resize_list call ( if the current shared list size is not old_logical_size anymore)
There was a problem hiding this comment.
That's right. I misunderstood the logics.
| std::make_optional<raft::device_vector_view<const int64_t, int64_t>>(grow_indices_view), | ||
| base_index); | ||
| raft::resource::sync_stream(handle); | ||
|
|
There was a problem hiding this comment.
| ASSERT_EQ(first_grown_index.lists()[0].get(), base_index.lists()[0].get()); |
I'd suggest to test that the first grown list reuses the base list without reallocation.
| raft::resource::sync_stream(handle); | ||
|
|
||
| ASSERT_EQ(base_index.lists()[0]->get_size(), base_rows); | ||
| ASSERT_GE(base_index.lists()[0]->indices_capacity(), rows); |
There was a problem hiding this comment.
| ASSERT_GE(base_index.lists()[0]->indices_capacity(), rows); | |
| ASSERT_EQ(base_index.lists()[0]->indices_capacity(), raft::Pow2<kIndexGroupSize>::roundUp(rows)); |
I think this makes the test more expressive
| std::vector<int64_t> host_indices(grow_rows); | ||
| std::iota(host_indices.begin(), host_indices.end(), base_rows); | ||
| auto indices = raft::make_device_vector<int64_t, int64_t>(handle, grow_rows); | ||
| raft::copy(indices.data_handle(), host_indices.data(), host_indices.size(), stream); |
There was a problem hiding this comment.
| std::vector<int64_t> host_indices(grow_rows); | |
| std::iota(host_indices.begin(), host_indices.end(), base_rows); | |
| auto indices = raft::make_device_vector<int64_t, int64_t>(handle, grow_rows); | |
| raft::copy(indices.data_handle(), host_indices.data(), host_indices.size(), stream); | |
| auto indices = raft::make_device_vector<int64_t, int64_t>(handle, grow_rows); | |
| raft::linalg::range(indices.data_handle(), base_rows, rows, stream); |
raft has a device built-in method for it.
We should #include <raft/linalg/init.cuh> and remove the include <numeric>
| auto shared_list_size = old_used_size; | ||
| if (new_used_size <= old_used_size || | ||
| auto shared_list_size = old_logical_size; | ||
| if (new_used_size <= old_logical_size || |
There was a problem hiding this comment.
That's right. I misunderstood the logics.
|
|
||
| INSTANTIATE_TEST_CASE_P(AnnIVFSQTest, AnnIVFSQTestF_float, ::testing::ValuesIn(inputs)); | ||
|
|
||
| TEST(AnnIVFSQTest, ExtendInPlaceUpdatesListSizeWithinCapacity) |
There was a problem hiding this comment.
I'd have the same reviews for this test.
| std::vector<float> host_data(rows * dim); | ||
| for (int64_t row = 0; row < rows; row++) { | ||
| for (int64_t col = 0; col < dim; col++) { | ||
| host_data[row * dim + col] = static_cast<float>(row + col); | ||
| } | ||
| } | ||
|
|
||
| auto data = raft::make_device_matrix<float, int64_t>(handle, rows, dim); | ||
| raft::copy(data.data_handle(), host_data.data(), host_data.size(), stream); |
There was a problem hiding this comment.
| std::vector<float> host_data(rows * dim); | |
| for (int64_t row = 0; row < rows; row++) { | |
| for (int64_t col = 0; col < dim; col++) { | |
| host_data[row * dim + col] = static_cast<float>(row + col); | |
| } | |
| } | |
| auto data = raft::make_device_matrix<float, int64_t>(handle, rows, dim); | |
| raft::copy(data.data_handle(), host_data.data(), host_data.size(), stream); | |
| auto data = raft::make_device_matrix<float, int64_t>(handle, rows, dim); | |
| raft::matrix::fill(handle, data.view(), 0.0f); |
I think we could initialize the data directly on device with zeros. We test the sizes instead of filled values. I assume the build() works fine with zeros.
After the changes, we should #include <raft/matrix/init.cuh> and remove #include <vector>
|
Some suggestions to improve the tests. Otherwise LGTM! Thank you for the contribution! |
|
/ok to test f4cee4f |
|
fixed the tests |
|
@achirkin I can't trigger the full 85 CI checks. Can you trigger it or it is because of missing commit signing? |
| ASSERT_EQ(index.lists()[0]->get_size(), base_rows); | ||
| ASSERT_GE(index.lists()[0]->indices_capacity(), rows); | ||
| ASSERT_EQ(index.lists()[0]->indices_capacity(), raft::Pow2<kIndexGroupSize>::roundUp(rows)); | ||
| auto* base_list = index.lists()[0].get(); |
There was a problem hiding this comment.
Why do we need to get the base_list pointer here? Can't we later get it at line 69?
Description
Fixes a bug in IVF-Flat and IVF-SQ
extend()where a list could receive new vectors but keep its old logicallist::size.The issue happens when extending a list that already has enough allocated capacity. Interleaved IVF list storage may be padded, so the old copy extent can be larger than the old logical size.
Example:
Previously,
resize_list()received only the padded value,128, and used it both for copying old storage and for deciding whetherlist::sizeneeded to be updated. It checked120 <= 128, skipped the update, and leftlist::size == 100even though new vectors were inserted up to120.This stale size can break the shared-list / copy-on-write logic in later extends and corrupt search results.
Fix
Pass separate old sizes to
ivf::resize_list():The existing overload is kept for call sites where both sizes are the same.
Tests
Added regression tests for: