perf(search): share a thread-local RNG for HNSW and drop seed (#2398)#3562
perf(search): share a thread-local RNG for HNSW and drop seed (#2398)#3562advisedy wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR optimizes redis::HnswIndex construction cost on query paths by avoiding eager construction of a large std::mt19937 member, and instead initializing the RNG only when layer randomization is actually needed during inserts.
Changes:
- Replace the eager
std::mt19937member with a lazily initializedstd::unique_ptr<std::mt19937>plus an optional stored seed. - Initialize the RNG on first
RandomizeLayer()call rather than in the constructor. - Add a new C++ gtest that asserts the RNG is not initialized until
RandomizeLayer()is invoked.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/search/hnsw_indexer.h |
Changes HnswIndex RNG-related members/signature to support lazy initialization. |
src/search/hnsw_indexer.cc |
Implements lazy RNG allocation/initialization inside RandomizeLayer(). |
tests/cppunit/hnsw_index_test.cc |
Adds a regression test to verify RNG is initialized lazily. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
|
Hi, @git-hulk , @PragmaTwice ,just friendly ping on this one too, whenever you have some time. Thanks! |
| uint16_t HnswIndex::RandomizeLayer() { | ||
| if (!generator) { | ||
| const auto actual_seed = seed ? *seed : std::random_device()(); | ||
| generator = std::make_unique<std::mt19937>(actual_seed); |
There was a problem hiding this comment.
From the original issue, it should expect to keep only one random generator for all HNSW indexes? @PragmaTwice
There was a problem hiding this comment.
Yup. maybe thread local to avoid race.
There was a problem hiding this comment.
switched to a single static thread_local generator as you suggested. please take a look.
PragmaTwice
left a comment
There was a problem hiding this comment.
Refer to the issue, I think it's better to make the RNG static.
|
I was being silly and misunderstood,Now fixed: switched to a single |
| std::optional<std::random_device::result_type> seed) | ||
| : search_key(search_key), | ||
| metadata(vector), | ||
| storage(storage), | ||
| generator(std::mt19937(seed)), | ||
| seed(seed), | ||
| m_level_normalization_factor(1.0 / std::log(metadata->m)) {} | ||
|
|
||
| uint16_t HnswIndex::RandomizeLayer() { | ||
| static thread_local std::mt19937 generator = [this] { return std::mt19937(seed.value_or(std::random_device()())); }(); |
There was a problem hiding this comment.
- Please don't make it a local static variable;
- The semantics of
seedis very weird here.
There was a problem hiding this comment.
You're right — the semantics of seed is indeed weird here. Since the
generator is now shared, only the first instance's seed would take
effect, which is misleading.
I see two possible directions:
-
Share the generator only among indexes with the same seed, and let
indexes with different seeds keep their own. I haven't fully figured
out the implementation yet, but maybe astatic thread_localmap
could work? It seems like that might be doable. -
Drop
seedentirely, seed the generator withstd::random_device,
and make it a class-levelstatic thread_local std::mt19937member
(no longer a function-local static), so its initialization no longer
depends on any specific instance.
I'd like to know which direction you think we should take. If you
prefer option 1, I'll think about how to implement it in detail. If
it's option 2, I'll go ahead and do it now.
There was a problem hiding this comment.
Could you stop copy-pasting to me what your agent replies to you? You can do your own decision. I have enough token that I don't need an agent proxy.
There was a problem hiding this comment.
Sorry, my english is not good, so I tell AI what I was thinking and let it translate. But The decision is mine. please.
There was a problem hiding this comment.
Here is my origin words: 确实感觉seed有点奇怪在这里,但是generator得根据seed去生成出来,不如说这里seed,generator,和我们期望共享的逻辑有点混乱,我的理解有两种思路,第一种思路就是我们考虑对于seed一样的进行共享,不一样的就是各自持有,这种方法用static + thread_local + map 应该能实现出来? 但我感觉有点麻烦。第二种思路就是或许可以考虑直接把seed删掉,使用std::random_device,这样的话,static thread_local std::mt19937 generator; 放到类里面,不再是局部了,这样初始化也不会依赖某个实例。 想知道一下,你觉得我们应该怎么做会比较好呢?如果你想要第一种的思路,我再去思考具体该怎么实现会比较好。如果是第二种的话,我可以现在就去实现出来代码。
There was a problem hiding this comment.
I’m very sorry that I relied too much on AI when replying, but my genuine intention was to contribute. I’m truly sorry.
There was a problem hiding this comment.
- I think we can just remove
seedhere. - After the change, don't forget to rewrite the PR title and description to match the current implementation.
|
Hi @advisedy. There is nothing wrong with using AI to translate your writing into English, but do not let it turn a simple point into something unnecessarily long and bloated. No one wants to feel as though they are talking to an AI agent. Copying and pasting an AI-generated response without editing it shows little respect for other people’s time. You need to think through what the AI gives you, incorporate your own understanding, and express it in your own words. We are not short on tokens, but we do not need robots either. We are not looking for contributors who are only here to meet a KPI. We want contributors to grow with the community and remain part of it over the long term. That requires you to spend real time and effort thinking about and understanding the project, rather than relying entirely on AI to complete your tasks. It is okay if you do not do everything well at the beginning, so there is no need to worry too much. I hope you can improve over time. |
There was a problem hiding this comment.
The code is good now. To get merged you need to check why the lint failed and fix it (just to add a const specifier).


Fixes #2398.
Problem
HnswIndexcurrently owns astd::mt19937generator directly, so everyHnswIndexconstruction also constructs the random generator.However, the generator is only needed by
RandomizeLayer()when insertingvector entries.
HnswIndexcan also be constructed on query paths such asKNN/RANGE search, where random layer generation is not used.
Fix
std::mt19937member with a lazily initializedstd::unique_ptr<std::mt19937>.RandomizeLayer()call.Tests
new C++ gtests added (hnsw_index_test.cc):
RandomGeneratorIsInitializedLazilyAI-assisted contribution
I used an LLM to help draft the new GTest case for verifying lazy generator
initialization.