Skip to content

Commit 1002fff

Browse files
committed
feat: reuse prepared search configurations
Signed-off-by: xianliang.li <xianliang.li@zilliz.com>
1 parent 6eebfe4 commit 1002fff

4 files changed

Lines changed: 172 additions & 7 deletions

File tree

include/knowhere/index/index_node.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
#ifndef INDEX_NODE_H
1313
#define INDEX_NODE_H
1414

15+
#define KNOWHERE_SEARCH_CONFIG_CACHE_VERSION 1
16+
17+
#include <atomic>
1518
#include <functional>
19+
#include <memory>
1620
#include <mutex>
1721
#include <queue>
1822
#include <utility>
@@ -618,6 +622,22 @@ class IndexNode : public Object {
618622
SearchEmbList(const DataSetPtr dataset, std::unique_ptr<Config> cfg, const BitsetView& bitset,
619623
milvus::OpContext* op_context = nullptr) const;
620624

625+
public:
626+
virtual bool
627+
SupportsSearchConfigCache() const {
628+
return false;
629+
}
630+
631+
virtual expected<DataSetPtr>
632+
SearchWithPreparedConfig(const DataSetPtr, std::shared_ptr<const Config>, const BitsetView&,
633+
milvus::OpContext* = nullptr) const {
634+
return expected<DataSetPtr>::Err(Status::not_implemented, "prepared search config is not supported");
635+
}
636+
637+
expected<std::shared_ptr<const Config>>
638+
GetOrCreateSearchConfig(const Json& json) const;
639+
640+
protected:
621641
static EmbListMetaHeader
622642
ParseEmbListMetaHeader(const uint8_t* data, int64_t size);
623643

@@ -637,6 +657,17 @@ class IndexNode : public Object {
637657
std::shared_ptr<ThreadPool> pool, milvus::OpContext* op_context = nullptr) const;
638658

639659
Version version_;
660+
661+
private:
662+
struct SearchConfigCacheEntry {
663+
Json json;
664+
std::shared_ptr<const Config> config;
665+
};
666+
667+
mutable std::shared_ptr<const SearchConfigCacheEntry> search_config_cache_;
668+
mutable std::mutex search_config_cache_mutex_;
669+
670+
protected:
640671
std::shared_ptr<EmbListOffset> emb_list_offset_; // emb_list group offset structure (shared with strategy)
641672
std::string el_metric_type_;
642673
EmbListStrategyPtr emb_list_strategy_; // emb_list encoding strategy (tokenann/muvera)

src/index/index.cc

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,23 @@ inline expected<DataSetPtr>
133133
Index<T>::Search(const DataSetPtr dataset, const Json& json, const BitsetView& bitset_,
134134
milvus::OpContext* op_context) const noexcept {
135135
return GuardedCall([&]() -> expected<DataSetPtr> {
136-
auto cfg = this->node->CreateConfig();
136+
std::unique_ptr<BaseConfig> owned_cfg;
137+
std::shared_ptr<const Config> prepared_cfg;
137138
std::string msg;
138-
const Status load_status = LoadConfig(cfg.get(), json, knowhere::SEARCH, "Search", &msg);
139-
if (load_status != Status::success) {
140-
return expected<DataSetPtr>::Err(load_status, msg);
139+
if (this->node->SupportsSearchConfigCache()) {
140+
auto result = this->node->GetOrCreateSearchConfig(json);
141+
if (!result.has_value()) {
142+
return expected<DataSetPtr>::Err(result.error(), result.what());
143+
}
144+
prepared_cfg = std::move(result.value());
145+
} else {
146+
owned_cfg = this->node->CreateConfig();
147+
const Status load_status = LoadConfig(owned_cfg.get(), json, knowhere::SEARCH, "Search", &msg);
148+
if (load_status != Status::success) {
149+
return expected<DataSetPtr>::Err(load_status, msg);
150+
}
141151
}
152+
const Config* cfg = prepared_cfg != nullptr ? prepared_cfg.get() : owned_cfg.get();
142153
// when index is immutable, bitset size should always equal to data count in index
143154
// when index is mutable, it could happen that data count larger than bitset size, see
144155
// https://github.com/zilliztech/knowhere/issues/70
@@ -177,14 +188,18 @@ Index<T>::Search(const DataSetPtr dataset, const Json& json, const BitsetView& b
177188
// LCOV_EXCL_STOP
178189

179190
TimeRecorder rc("Search");
180-
auto k = cfg->k.value();
181-
auto res = this->node->SearchEmbListIfNeed(dataset, std::move(cfg), bitset, op_context);
191+
auto k = b_cfg.k.value();
192+
auto res = prepared_cfg != nullptr
193+
? this->node->SearchWithPreparedConfig(dataset, std::move(prepared_cfg), bitset, op_context)
194+
: this->node->SearchEmbListIfNeed(dataset, std::move(owned_cfg), bitset, op_context);
182195
auto time = rc.ElapseFromBegin("done");
183196
time *= 0.001; // convert to ms
184197
this->node->GetSearchLatencyMetric().Observe(time);
185198
knowhere_search_topk.Observe(k);
186199
#else
187-
auto res = this->node->SearchEmbListIfNeed(dataset, std::move(cfg), bitset, op_context);
200+
auto res = prepared_cfg != nullptr
201+
? this->node->SearchWithPreparedConfig(dataset, std::move(prepared_cfg), bitset, op_context)
202+
: this->node->SearchEmbListIfNeed(dataset, std::move(owned_cfg), bitset, op_context);
188203
#endif
189204
return res;
190205
});

src/index/index_node.cc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,39 @@
3232

3333
namespace knowhere {
3434

35+
expected<std::shared_ptr<const Config>>
36+
IndexNode::GetOrCreateSearchConfig(const Json& json) const {
37+
auto cached = std::atomic_load_explicit(&search_config_cache_, std::memory_order_acquire);
38+
if (cached != nullptr && cached->json == json) {
39+
return cached->config;
40+
}
41+
42+
std::lock_guard lock(search_config_cache_mutex_);
43+
cached = std::atomic_load_explicit(&search_config_cache_, std::memory_order_relaxed);
44+
if (cached != nullptr && cached->json == json) {
45+
return cached->config;
46+
}
47+
48+
auto cfg = CreateConfig();
49+
Json normalized_json(json);
50+
std::string msg;
51+
auto status = Config::FormatAndCheck(*cfg, normalized_json, &msg);
52+
LOG_KNOWHERE_DEBUG_ << "Search config dump: " << normalized_json.dump();
53+
if (status != Status::success) {
54+
return expected<std::shared_ptr<const Config>>::Err(status, msg);
55+
}
56+
cfg->CaptureRawJson(normalized_json);
57+
status = Config::Load(*cfg, normalized_json, knowhere::SEARCH, &msg);
58+
if (status != Status::success) {
59+
return expected<std::shared_ptr<const Config>>::Err(status, msg);
60+
}
61+
62+
std::shared_ptr<const Config> prepared_config(std::move(cfg));
63+
auto entry = std::make_shared<const SearchConfigCacheEntry>(SearchConfigCacheEntry{json, prepared_config});
64+
std::atomic_store_explicit(&search_config_cache_, std::move(entry), std::memory_order_release);
65+
return prepared_config;
66+
}
67+
3568
// NOLINTBEGIN(google-default-arguments)
3669
expected<DataSetPtr>
3770
IndexNode::RangeSearch(const DataSetPtr dataset, std::unique_ptr<Config> cfg, const BitsetView& bitset,

tests/ut/test_index_node.cc

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
1010
// or implied. See the License for the specific language governing permissions and limitations under the License.
1111

12+
#include <atomic>
13+
#include <future>
1214
#include <unordered_set>
1315

1416
#include "catch2/catch_approx.hpp"
@@ -136,6 +138,49 @@ class BaseFlatIndexNode : public IndexNode {
136138
}
137139
};
138140

141+
template <typename DataType>
142+
class CachedSearchConfigIndexNode : public BaseFlatIndexNode<DataType> {
143+
public:
144+
CachedSearchConfigIndexNode(const int32_t& version, const Object& object)
145+
: BaseFlatIndexNode<DataType>(version, object) {
146+
}
147+
148+
bool
149+
SupportsSearchConfigCache() const override {
150+
return true;
151+
}
152+
153+
expected<DataSetPtr>
154+
SearchWithPreparedConfig(const DataSetPtr, std::shared_ptr<const Config> cfg, const BitsetView&,
155+
milvus::OpContext*) const override {
156+
const Config* expected = nullptr;
157+
first_config_.compare_exchange_strong(expected, cfg.get());
158+
reused_same_config_.store(reused_same_config_.load() && first_config_.load() == cfg.get());
159+
return std::make_shared<DataSet>();
160+
}
161+
162+
std::unique_ptr<BaseConfig>
163+
CreateConfig() const override {
164+
create_config_calls_.fetch_add(1);
165+
return std::make_unique<BaseConfig>();
166+
}
167+
168+
int
169+
CreateConfigCalls() const {
170+
return create_config_calls_.load();
171+
}
172+
173+
bool
174+
ReusedSameConfig() const {
175+
return reused_same_config_.load();
176+
}
177+
178+
private:
179+
mutable std::atomic<int> create_config_calls_{0};
180+
mutable std::atomic<const Config*> first_config_{nullptr};
181+
mutable std::atomic<bool> reused_same_config_{true};
182+
};
183+
139184
TEST_CASE("Test index node") {
140185
auto version = GenTestVersionList();
141186
DataSetPtr ds = std::make_shared<DataSet>();
@@ -208,3 +253,44 @@ TEST_CASE("Test index node") {
208253
}
209254
#pragma GCC diagnostic pop
210255
}
256+
257+
TEST_CASE("Search reuses an immutable prepared config", "[search_config_cache]") {
258+
KNOWHERE_SIMPLE_REGISTER_GLOBAL(SEARCH_CONFIG_CACHE, CachedSearchConfigIndexNode, fp32, knowhere::feature::FLOAT32);
259+
const auto version = GenTestVersionList();
260+
auto dataset = std::make_shared<DataSet>();
261+
const Json base_search_config = {{meta::METRIC_TYPE, metric::L2}, {meta::TOPK, 10}};
262+
263+
SECTION("same config reuses the prepared object") {
264+
auto index = IndexFactory::Instance().Create<fp32>("SEARCH_CONFIG_CACHE", version).value();
265+
auto* node = dynamic_cast<CachedSearchConfigIndexNode<fp32>*>(index.Node());
266+
REQUIRE(node != nullptr);
267+
268+
REQUIRE(index.Search(dataset, base_search_config, nullptr).has_value());
269+
REQUIRE(index.Search(dataset, base_search_config, nullptr).has_value());
270+
REQUIRE(node->CreateConfigCalls() == 1);
271+
REQUIRE(node->ReusedSameConfig());
272+
273+
auto changed_search_config = base_search_config;
274+
changed_search_config[meta::TOPK] = 20;
275+
REQUIRE(index.Search(dataset, changed_search_config, nullptr).has_value());
276+
REQUIRE(node->CreateConfigCalls() == 2);
277+
}
278+
279+
SECTION("concurrent searches prepare the config once") {
280+
auto index = IndexFactory::Instance().Create<fp32>("SEARCH_CONFIG_CACHE", version).value();
281+
auto* node = dynamic_cast<CachedSearchConfigIndexNode<fp32>*>(index.Node());
282+
REQUIRE(node != nullptr);
283+
284+
std::vector<std::future<expected<DataSetPtr>>> searches;
285+
for (int i = 0; i < 32; ++i) {
286+
searches.emplace_back(
287+
std::async(std::launch::async, [&] { return index.Search(dataset, base_search_config, nullptr); }));
288+
}
289+
for (auto& search : searches) {
290+
REQUIRE(search.get().has_value());
291+
}
292+
293+
REQUIRE(node->CreateConfigCalls() == 1);
294+
REQUIRE(node->ReusedSameConfig());
295+
}
296+
}

0 commit comments

Comments
 (0)