Skip to content

Commit 77d79d8

Browse files
kyleknaparanadiveovidiusm
authored
Parallelize memory query for AZURE_BLOB plugin (#1721)
* Parallelize memory query for azure blob Similar to OBJ plugin, parallelize HEAD requests when batch memory queries are provided. This specifically improves initial query times for integrations that checks for a blob existence (e.g., checking for KV cache in LMCache). For the most part the implementation, matches the OBJ plugin to make it easy to update both plugins if there are adjustments made to the approach in the future. Signed-off-by: Kyle Knapp <kyleknapp@microsoft.com> * Clean up memory query wait logic Specific changes were: * Make sure we wait for all instances where the callback started within the timeout block and not just within the error block when queueing work. * Consolidated logic to single helper function to make it easier to follow and maintain in the future. Signed-off-by: Kyle Knapp <kyleknapp@microsoft.com> * Add more robust error handling in blob client Specifically catch any non-standard exceptions to safeguard against crashes in the asio thread pool. This is primarily defensive as prior to it, non-standard exceptions had yet to be encountered. It also adds logging for exceptions in async put and get to have Azure exceptions visible in NIXL error logs. Alternative was to turn on AZURE_LOG_LEVEL=verbose to know what the exceptions were * Add assertions for null clients in tests Prevents not constructing a client and trying to use a null client when building a test blob engine for testing. Signed-off-by: Kyle Knapp <kyleknapp@microsoft.com> --------- Signed-off-by: Kyle Knapp <kyleknapp@microsoft.com> Co-authored-by: Adit Ranadive <aranadive@nvidia.com> Co-authored-by: ovidiusm <ovidium@nvidia.com>
1 parent d1749f4 commit 77d79d8

4 files changed

Lines changed: 511 additions & 31 deletions

File tree

src/plugins/azure_blob/azure_blob_backend.cpp

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <vector>
2828
#include <chrono>
2929
#include <algorithm>
30+
#include <atomic>
3031

3132
namespace {
3233

@@ -110,6 +111,43 @@ class nixlAzureBlobMetadata : public nixlBackendMD {
110111
std::string blobName;
111112
};
112113

114+
struct QueryMemDescriptorState {
115+
std::shared_ptr<std::promise<void>> promise;
116+
std::shared_ptr<std::atomic<bool>> completed;
117+
};
118+
119+
// Wait for all asynchronous memory queries to complete with a per-descriptor
120+
// wait timeout.
121+
void
122+
waitForQueryMemFutures(std::vector<std::future<void>> &futures,
123+
std::vector<QueryMemDescriptorState> &states,
124+
std::vector<nixl_query_resp_t> &resp,
125+
std::atomic<bool> &has_error,
126+
std::chrono::seconds timeout) {
127+
for (size_t i = 0; i < futures.size(); ++i) {
128+
if (futures[i].wait_for(timeout) == std::future_status::timeout) {
129+
if (!states[i].completed->exchange(true)) {
130+
// We've timed out and the callback never fired. Mark as
131+
// completed and resolve the promise ourselves.
132+
NIXL_ERROR << "checkBlobExistsAsync timed out for descriptor " << i;
133+
resp[i] = std::nullopt;
134+
has_error.store(true, std::memory_order_relaxed);
135+
try {
136+
states[i].promise->set_value();
137+
}
138+
catch (...) {
139+
}
140+
} else {
141+
// Handles case where we timed out but the callback started
142+
// at the same time as the timeout and won the exchange.
143+
// Wait for it to finish so its writes to resp[i]/has_error
144+
// complete before stack-captured references go out of scope.
145+
futures[i].wait();
146+
}
147+
}
148+
}
149+
}
150+
113151
} // namespace
114152

115153
// -----------------------------------------------------------------------------
@@ -172,19 +210,60 @@ nixlAzureBlobEngine::deregisterMem(nixlBackendMD *meta) {
172210
nixl_status_t
173211
nixlAzureBlobEngine::queryMem(const nixl_reg_dlist_t &descs,
174212
std::vector<nixl_query_resp_t> &resp) const {
175-
resp.reserve(descs.descCount());
213+
resp.assign(descs.descCount(), std::nullopt);
214+
215+
std::vector<std::future<void>> futures;
216+
futures.reserve(descs.descCount());
217+
std::vector<QueryMemDescriptorState> states;
218+
states.reserve(descs.descCount());
219+
std::atomic<bool> has_error{false};
220+
221+
constexpr auto kQueryTimeout = std::chrono::seconds(30);
176222

177223
try {
178-
for (auto &desc : descs)
179-
resp.emplace_back(blobClient_->checkBlobExists(desc.metaInfo) ?
180-
nixl_query_resp_t{nixl_b_params_t{}} :
181-
std::nullopt);
224+
// Launch async existence checks for each descriptor
225+
for (int i = 0; i < descs.descCount(); ++i) {
226+
auto &desc = descs[i];
227+
auto state = QueryMemDescriptorState{std::make_shared<std::promise<void>>(),
228+
std::make_shared<std::atomic<bool>>(false)};
229+
230+
blobClient_->checkBlobExistsAsync(
231+
desc.metaInfo,
232+
[&resp, &has_error, i, promise = state.promise, completed = state.completed](
233+
std::optional<bool> exists) {
234+
if (completed->exchange(true)) {
235+
return;
236+
}
237+
if (!exists.has_value()) {
238+
resp[i] = std::nullopt;
239+
has_error.store(true, std::memory_order_relaxed);
240+
} else {
241+
resp[i] = *exists ? nixl_query_resp_t{nixl_b_params_t{}} : std::nullopt;
242+
}
243+
try {
244+
promise->set_value();
245+
}
246+
catch (...) {
247+
}
248+
});
249+
250+
futures.push_back(state.promise->get_future());
251+
states.push_back(std::move(state));
252+
}
182253
}
183-
catch (const std::runtime_error &e) {
254+
catch (const std::exception &e) {
255+
waitForQueryMemFutures(futures, states, resp, has_error, kQueryTimeout);
184256
NIXL_ERROR << "Failed to query memory: " << e.what();
185257
return NIXL_ERR_BACKEND;
186258
}
187259

260+
waitForQueryMemFutures(futures, states, resp, has_error, kQueryTimeout);
261+
262+
if (has_error.load(std::memory_order_relaxed)) {
263+
NIXL_ERROR << "Failed to query memory: one or more existence checks failed";
264+
return NIXL_ERR_BACKEND;
265+
}
266+
188267
return NIXL_SUCCESS;
189268
}
190269

src/plugins/azure_blob/azure_blob_client.cpp

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@ azureBlobClient::putBlobAsync(std::string_view blob_name,
164164
callback(true);
165165
}
166166
catch (const std::exception &e) {
167+
NIXL_ERROR << "putBlobAsync error: " << e.what();
168+
callback(false);
169+
}
170+
catch (...) {
171+
NIXL_ERROR << "putBlobAsync: unknown exception";
167172
callback(false);
168173
}
169174
});
@@ -189,24 +194,40 @@ azureBlobClient::getBlobAsync(std::string_view blob_name,
189194
callback(true);
190195
}
191196
catch (const std::exception &e) {
197+
NIXL_ERROR << "getBlobAsync error: " << e.what();
198+
callback(false);
199+
}
200+
catch (...) {
201+
NIXL_ERROR << "getBlobAsync: unknown exception";
192202
callback(false);
193203
}
194204
});
195205
}
196206

197-
bool
198-
azureBlobClient::checkBlobExists(std::string_view blob_name) {
199-
auto blobClient = blobContainerClient_->GetBlockBlobClient(std::string(blob_name));
200-
Azure::Storage::Blobs::GetBlobPropertiesOptions options;
201-
try {
202-
blobClient.GetProperties(options);
203-
}
204-
catch (const Azure::Core::RequestFailedException &e) {
205-
if (e.StatusCode == Azure::Core::Http::HttpStatusCode::NotFound) {
206-
return false;
207-
} else {
208-
throw std::runtime_error("Failed to check if blob exists: " + std::string(e.what()));
207+
void
208+
azureBlobClient::checkBlobExistsAsync(std::string_view blob_name, check_blob_callback_t callback) {
209+
std::string blob_name_str(blob_name);
210+
asio::post(*executor_, [this, blob_name_str, callback]() {
211+
try {
212+
auto blobClient = blobContainerClient_->GetBlockBlobClient(blob_name_str);
213+
blobClient.GetProperties();
214+
callback(true);
209215
}
210-
}
211-
return true;
216+
catch (const Azure::Core::RequestFailedException &e) {
217+
if (e.StatusCode == Azure::Core::Http::HttpStatusCode::NotFound) {
218+
callback(false);
219+
return;
220+
}
221+
NIXL_ERROR << "checkBlobExistsAsync error: " << e.what();
222+
callback(std::nullopt);
223+
}
224+
catch (const std::exception &e) {
225+
NIXL_ERROR << "checkBlobExistsAsync error: " << e.what();
226+
callback(std::nullopt);
227+
}
228+
catch (...) {
229+
NIXL_ERROR << "checkBlobExistsAsync: unknown exception";
230+
callback(std::nullopt);
231+
}
232+
});
212233
}

src/plugins/azure_blob/azure_blob_client.h

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include <functional>
2323
#include <memory>
24+
#include <optional>
2425
#include <string_view>
2526
#include <cstdint>
2627
#include <azure/storage/blobs.hpp>
@@ -29,6 +30,7 @@
2930

3031
using put_blob_callback_t = std::function<void(bool success)>;
3132
using get_blob_callback_t = std::function<void(bool success)>;
33+
using check_blob_callback_t = std::function<void(std::optional<bool> exists)>;
3234

3335
/**
3436
* Abstract interface for Azure Blob client operations.
@@ -76,12 +78,15 @@ class iBlobClient {
7678
get_blob_callback_t callback) = 0;
7779

7880
/**
79-
* Check if the blob exists.
81+
* Asynchronously check if the blob exists.
82+
* Uses check_blob_callback_t: the callback receives a std::optional<bool>
83+
* that is true if the blob exists, false if it does not, or std::nullopt
84+
* if the request failed (indicating an error).
8085
* @param blob_name The blob name
81-
* @return true if the blob exists, false otherwise
86+
* @param callback Callback function invoked with the existence check result
8287
*/
83-
virtual bool
84-
checkBlobExists(std::string_view blob_name) = 0;
88+
virtual void
89+
checkBlobExistsAsync(std::string_view blob_name, check_blob_callback_t callback) = 0;
8590
};
8691

8792
/**
@@ -114,8 +119,8 @@ class azureBlobClient : public iBlobClient {
114119
size_t offset,
115120
get_blob_callback_t callback) override;
116121

117-
bool
118-
checkBlobExists(std::string_view blob_name) override;
122+
void
123+
checkBlobExistsAsync(std::string_view blob_name, check_blob_callback_t callback) override;
119124

120125
private:
121126
std::shared_ptr<asio::thread_pool> executor_;

0 commit comments

Comments
 (0)