|
9 | 9 | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
10 | 10 | // or implied. See the License for the specific language governing permissions and limitations under the License. |
11 | 11 |
|
| 12 | +#include <atomic> |
| 13 | +#include <future> |
12 | 14 | #include <unordered_set> |
13 | 15 |
|
14 | 16 | #include "catch2/catch_approx.hpp" |
@@ -136,6 +138,49 @@ class BaseFlatIndexNode : public IndexNode { |
136 | 138 | } |
137 | 139 | }; |
138 | 140 |
|
| 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 | + |
139 | 184 | TEST_CASE("Test index node") { |
140 | 185 | auto version = GenTestVersionList(); |
141 | 186 | DataSetPtr ds = std::make_shared<DataSet>(); |
@@ -208,3 +253,44 @@ TEST_CASE("Test index node") { |
208 | 253 | } |
209 | 254 | #pragma GCC diagnostic pop |
210 | 255 | } |
| 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