Skip to content

Commit 71d06de

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

4 files changed

Lines changed: 170 additions & 7 deletions

File tree

include/knowhere/index/index_node.h

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

15+
#include <atomic>
1516
#include <functional>
17+
#include <memory>
1618
#include <mutex>
1719
#include <queue>
1820
#include <utility>
@@ -618,6 +620,22 @@ class IndexNode : public Object {
618620
SearchEmbList(const DataSetPtr dataset, std::unique_ptr<Config> cfg, const BitsetView& bitset,
619621
milvus::OpContext* op_context = nullptr) const;
620622

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

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

639657
Version version_;
658+
659+
private:
660+
struct SearchConfigCacheEntry {
661+
Json json;
662+
std::shared_ptr<const Config> config;
663+
};
664+
665+
mutable std::atomic<std::shared_ptr<const SearchConfigCacheEntry>> search_config_cache_;
666+
mutable std::mutex search_config_cache_mutex_;
667+
668+
protected:
640669
std::shared_ptr<EmbListOffset> emb_list_offset_; // emb_list group offset structure (shared with strategy)
641670
std::string el_metric_type_;
642671
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 = search_config_cache_.load(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 = search_config_cache_.load(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+
search_config_cache_.store(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)