Skip to content

Commit 3369178

Browse files
committed
fix integrity checksum review issues
1 parent 7f8abfe commit 3369178

10 files changed

Lines changed: 245 additions & 70 deletions

File tree

kv_cache_manager/client/src/transfer_client_impl.cc

Lines changed: 130 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,77 @@
2626

2727
namespace kv_cache_manager {
2828

29+
#if defined(USING_CUDA) || defined(USING_MUSA)
30+
namespace {
31+
32+
bool IsChecksumHashableBlock(const BlockBuffer &block_buffer) {
33+
if (block_buffer.iovs.empty()) {
34+
return false;
35+
}
36+
return std::all_of(block_buffer.iovs.begin(), block_buffer.iovs.end(), [](const Iov &iov) {
37+
return !iov.ignore && iov.base != nullptr && iov.size > 0;
38+
});
39+
}
40+
41+
bool HashBlocksByIovShape(const BlockBuffers &block_buffers,
42+
SdkBufferCheckPool::CellHandle &handle,
43+
size_t max_check_iov_num,
44+
std::vector<int64_t> &out_checksums) {
45+
out_checksums.clear();
46+
out_checksums.resize(block_buffers.size());
47+
if (block_buffers.empty()) {
48+
return true;
49+
}
50+
51+
BlockBuffers chunk;
52+
std::vector<size_t> chunk_indices;
53+
size_t chunk_iov_num = 0;
54+
size_t chunk_total_iovs = 0;
55+
56+
auto flush_chunk = [&]() -> bool {
57+
if (chunk.empty()) {
58+
return true;
59+
}
60+
auto checksums = SdkBufferCheckUtil::GetBlocksHash(
61+
chunk, handle->d_iovs, handle->d_crcs, handle->h_iovs, max_check_iov_num, handle->gpu_stream);
62+
if (checksums.size() != chunk.size()) {
63+
KVCM_LOG_ERROR("checksum hash returned [%zu] entries for [%zu] blocks", checksums.size(), chunk.size());
64+
return false;
65+
}
66+
for (size_t i = 0; i < checksums.size(); ++i) {
67+
out_checksums[chunk_indices[i]] = checksums[i];
68+
}
69+
chunk.clear();
70+
chunk_indices.clear();
71+
chunk_iov_num = 0;
72+
chunk_total_iovs = 0;
73+
return true;
74+
};
75+
76+
for (size_t i = 0; i < block_buffers.size(); ++i) {
77+
const size_t iov_num = block_buffers[i].iovs.size();
78+
if (iov_num == 0 || iov_num > max_check_iov_num) {
79+
KVCM_LOG_ERROR("block [%zu] has invalid iov_num [%zu] for checksum hash", i, iov_num);
80+
return false;
81+
}
82+
if (!chunk.empty() && (iov_num != chunk_iov_num || chunk_total_iovs + iov_num > max_check_iov_num)) {
83+
if (!flush_chunk()) {
84+
return false;
85+
}
86+
}
87+
if (chunk.empty()) {
88+
chunk_iov_num = iov_num;
89+
}
90+
chunk.push_back(block_buffers[i]);
91+
chunk_indices.push_back(i);
92+
chunk_total_iovs += iov_num;
93+
}
94+
return flush_chunk();
95+
}
96+
97+
} // namespace
98+
#endif
99+
29100
TransferClientImpl::TransferClientImpl() {}
30101

31102
TransferClientImpl::~TransferClientImpl() {}
@@ -237,66 +308,66 @@ ClientErrorCode TransferClientImpl::LoadKvCaches(const UriStrVec &uri_str_vec,
237308
KVCM_LOG_WARN("expected_checksums given but sdk_buffer_check_pool is not enabled; "
238309
"skip verification");
239310
} else {
240-
std::vector<int64_t> effective_expected = *expected_checksums;
311+
BlockBuffers verifiable_block_buffers;
312+
std::vector<int64_t> verifiable_expected;
313+
std::vector<size_t> original_indices;
314+
verifiable_block_buffers.reserve(block_buffers.size());
315+
verifiable_expected.reserve(block_buffers.size());
316+
original_indices.reserve(block_buffers.size());
241317
size_t skipped_blocks = 0;
242318
for (size_t i = 0; i < block_buffers.size(); ++i) {
243-
if (effective_expected[i] == 0) {
319+
if ((*expected_checksums)[i] == 0) {
244320
continue; // already sentinel
245321
}
246-
const auto &iovs = block_buffers[i].iovs;
247-
bool unverifiable = iovs.empty();
248-
if (!unverifiable) {
249-
for (const auto &iov : iovs) {
250-
if (iov.ignore) {
251-
unverifiable = true;
252-
break;
253-
}
254-
}
255-
}
256-
if (unverifiable) {
257-
effective_expected[i] = 0;
322+
if (!IsChecksumHashableBlock(block_buffers[i])) {
258323
++skipped_blocks;
324+
continue;
259325
}
326+
verifiable_block_buffers.push_back(block_buffers[i]);
327+
verifiable_expected.push_back((*expected_checksums)[i]);
328+
original_indices.push_back(i);
260329
}
261330
if (skipped_blocks > 0) {
262-
KVCM_LOG_DEBUG("checksum verification skipped %zu block(s) due to ignored or empty iovs",
331+
KVCM_LOG_DEBUG("checksum verification skipped %zu block(s) due to ignored, empty, or invalid iovs",
263332
skipped_blocks);
264333
}
265334
// Short-circuit when nothing is left to verify (all-legacy batch or every
266335
// block downgraded to sentinel by the partial-read / empty-iov handling
267336
// above). Avoids one wasted GPU hash compute and — more importantly —
268337
// avoids GetBlocksHash dereferencing block_buffers on shapes it cannot
269338
// hash (empty iovs, etc.) after we already declared them unverifiable.
270-
const bool has_any_expected =
271-
std::any_of(effective_expected.begin(), effective_expected.end(), [](int64_t v) { return v != 0; });
272-
if (!has_any_expected) {
339+
if (verifiable_expected.empty()) {
273340
KVCM_LOG_DEBUG("checksum verification: no non-sentinel expected entries; skip");
274341
return ec;
275342
}
276343
auto handle = sdk_buffer_check_pool_->GetCell();
277-
auto actual = SdkBufferCheckUtil::GetBlocksHash(
278-
block_buffers, handle->d_iovs, handle->d_crcs, handle->h_iovs, max_check_iov_num_, handle->gpu_stream);
344+
std::vector<int64_t> actual;
345+
if (!HashBlocksByIovShape(verifiable_block_buffers, handle, max_check_iov_num_, actual)) {
346+
KVCM_LOG_ERROR("checksum verification failed to hash verifiable blocks safely");
347+
return ER_CHECKSUM_MISMATCH;
348+
}
279349
const bool strict_mode = EnvUtil::GetEnv("KVCM_CHECKSUM_STRICT_MODE", false);
280-
const auto verify_result = VerifyBatchChecksums(effective_expected, actual, strict_mode);
350+
const auto verify_result = VerifyBatchChecksums(verifiable_expected, actual, strict_mode);
281351
if (verify_result.mismatch) {
282352
if (verify_result.faulty_indices.empty()) {
283353
// Size mismatch surfaced from the helper (we already pre-checked
284354
// expected vs block_buffers above, so this is paranoid coverage).
285355
KVCM_LOG_ERROR(
286-
"actual checksums size [%zu] != expected [%zu]", actual.size(), effective_expected.size());
356+
"actual checksums size [%zu] != expected [%zu]", actual.size(), verifiable_expected.size());
287357
} else {
288358
// Structured per-block log; field set mirrors ChecksumMismatchEvent
289359
// so a log scraper can build the same observability surface until
290360
// the client SDK grows a real EventManager hook (follow-up).
291-
for (auto idx : verify_result.faulty_indices) {
361+
for (auto compact_idx : verify_result.faulty_indices) {
362+
const size_t idx = original_indices[compact_idx];
292363
const char *block_id_str = (trace_info != nullptr && idx < trace_info->block_ids.size())
293364
? trace_info->block_ids[idx].c_str()
294365
: "<unknown>";
295366
KVCM_LOG_ERROR("ChecksumMismatchEvent {block_index=%zu, expected_checksum=0x%lx, "
296367
"actual_checksum=0x%lx, storage_uri=\"%s\", block_id=\"%s\"}",
297368
idx,
298-
static_cast<unsigned long>(effective_expected[idx]),
299-
static_cast<unsigned long>(actual[idx]),
369+
static_cast<unsigned long>(verifiable_expected[compact_idx]),
370+
static_cast<unsigned long>(actual[compact_idx]),
300371
idx < uri_str_vec.size() ? uri_str_vec[idx].c_str() : "<oob>",
301372
block_id_str);
302373
}
@@ -326,21 +397,13 @@ std::pair<ClientErrorCode, UriStrVec> TransferClientImpl::SaveKvCaches(const Uri
326397
// request or the legacy KVCM_SDK_CHECK print-only fallback. The two paths share
327398
// the same computation so the checksum is computed at most once per call.
328399
//
329-
// SdkBufferCheckUtil::GetBlocksHash dereferences block_buffers.front(), so we
330-
// must guard against empty / malformed input here. The full URI / Iov validity
331-
// check still happens inside sdk_wrapper_->Put() below; we only short-circuit
332-
// the obviously invalid shapes that would crash the hashing kernel.
400+
// SdkBufferCheckUtil::GetBlocksHash dereferences block_buffers.front() and
401+
// assumes uniform iov shapes within one call. Validate every block first and
402+
// then hash by compatible iov-shape chunks.
333403
std::vector<int64_t> block_checksums;
334404
bool block_checksums_computed = false;
335-
const bool checksum_input_usable = !block_buffers.empty() && !block_buffers.front().iovs.empty();
336405
if (out_checksums != nullptr || is_check_buffer_) {
337-
if (!checksum_input_usable) {
338-
if (out_checksums != nullptr) {
339-
KVCM_LOG_WARN("block_buffers empty or first block has no iovs; "
340-
"skip checksum compute and return empty out_checksums");
341-
out_checksums->clear();
342-
}
343-
} else if (!sdk_buffer_check_pool_) {
406+
if (!sdk_buffer_check_pool_) {
344407
if (out_checksums != nullptr) {
345408
KVCM_LOG_WARN("out_checksums requested but sdk_buffer_check_pool is not enabled; "
346409
"return empty vector (caller should treat as 'checksum not available')");
@@ -349,30 +412,40 @@ std::pair<ClientErrorCode, UriStrVec> TransferClientImpl::SaveKvCaches(const Uri
349412
} else {
350413
bool need_print = (trace_info == nullptr) ? true : trace_info->need_print;
351414
if (out_checksums != nullptr || need_print) {
352-
auto handle = sdk_buffer_check_pool_->GetCell();
353-
block_checksums = SdkBufferCheckUtil::GetBlocksHash(block_buffers,
354-
handle->d_iovs,
355-
handle->d_crcs,
356-
handle->h_iovs,
357-
max_check_iov_num_,
358-
handle->gpu_stream);
359-
block_checksums_computed = true;
360-
// GetBlocksHash caps at max_check_iov_num_ and silently returns a
361-
// shorter vector; if the caller wants checksums we cannot let the
362-
// write proceed with a length that will fail FinishWrite's per-
363-
// location check after the data has already been persisted.
364-
if (out_checksums != nullptr && block_checksums.size() != block_buffers.size()) {
365-
KVCM_LOG_ERROR("block_checksums size [%zu] != block_buffers size [%zu]; iov count likely "
366-
"exceeded max_check_iov_num_ [%d]; reject write before it commits",
367-
block_checksums.size(),
368-
block_buffers.size(),
369-
max_check_iov_num_);
415+
auto invalid_it =
416+
std::find_if(block_buffers.begin(), block_buffers.end(), [](const BlockBuffer &block_buffer) {
417+
return !IsChecksumHashableBlock(block_buffer);
418+
});
419+
if (invalid_it != block_buffers.end()) {
420+
const size_t idx = std::distance(block_buffers.begin(), invalid_it);
421+
KVCM_LOG_WARN("block [%zu] has ignored, empty, null, or zero-size iovs; skip checksum hash", idx);
422+
if (out_checksums != nullptr) {
423+
out_checksums->clear();
424+
return {ER_INVALID_PARAMS, {}};
425+
}
426+
} else {
427+
auto handle = sdk_buffer_check_pool_->GetCell();
428+
if (!HashBlocksByIovShape(block_buffers, handle, max_check_iov_num_, block_checksums)) {
429+
if (out_checksums != nullptr) {
430+
out_checksums->clear();
431+
return {ER_INVALID_PARAMS, {}};
432+
}
433+
} else {
434+
block_checksums_computed = true;
435+
}
436+
}
437+
if (out_checksums != nullptr && block_checksums_computed &&
438+
block_checksums.size() != block_buffers.size()) {
439+
KVCM_LOG_ERROR(
440+
"block_checksums size [%zu] != block_buffers size [%zu]; reject write before it commits",
441+
block_checksums.size(),
442+
block_buffers.size());
370443
out_checksums->clear();
371444
return {ER_INVALID_PARAMS, {}};
372445
}
373446
}
374447
}
375-
if (is_check_buffer_ && checksum_input_usable) {
448+
if (is_check_buffer_ && block_checksums_computed) {
376449
PrintBlockChecksumAndUri("put_", uri_str_vec, block_checksums, trace_info);
377450
}
378451
// Deliberately DO NOT assign to *out_checksums here — see below. A prior
@@ -451,4 +524,4 @@ std::unique_ptr<TransferClient> TransferClient::Create(const std::string &client
451524
#undef DEFER
452525
#undef CHECK_SDK_BASE
453526
#undef CHECK_SDK
454-
#undef CHECK_SDK_WITH_TYPE
527+
#undef CHECK_SDK_WITH_TYPE

kv_cache_manager/data_storage/event_reporting_backend.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ class EventReportingBackend {
2121
const std::string &host_ip_port,
2222
const std::vector<std::string> &mediums) = 0;
2323
virtual ErrorCode UnregisterNode(const std::string &instance_id, const std::string &host_ip_port) = 0;
24+
virtual ErrorCode UnregisterNodeIfGenerationMatches(const std::string &instance_id,
25+
const std::string &host_ip_port,
26+
uint64_t expected_generation) = 0;
2427
virtual ErrorCode OnHeartbeat(const std::string &instance_id,
2528
const std::string &host_ip_port,
2629
const std::map<std::string, std::string> &system_status) = 0;
2730
virtual void SetNodeUnavailable(const std::string &instance_id, const std::string &host_ip_port) = 0;
31+
virtual bool IsNodeAvailable(const std::string &instance_id, const std::string &host_ip_port) const = 0;
2832
virtual uint64_t GetNodeGeneration(const std::string &instance_id, const std::string &host_ip_port) const = 0;
2933

3034
virtual void SetCleanupCallback(CleanupCallback cb) = 0;

kv_cache_manager/data_storage/storage_config.cc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,11 @@ bool StorageConfig::FromRapidValue(const rapidjson::Value &rapid_value) {
429429
// integrity 字段是后加的,老配置不带该字段时保留默认值 (全部关闭)。
430430
// 存在但字段类型错乱 (例如 "enable_meta_checksum": "true" 用了字符串)
431431
// 必须让整个 StorageConfig 解析失败,否则会静默降级为「全关」。
432-
if (rapid_value.HasMember("integrity") && rapid_value["integrity"].IsObject()) {
432+
if (rapid_value.HasMember("integrity")) {
433+
if (!rapid_value["integrity"].IsObject()) {
434+
KVCM_LOG_ERROR("integrity must be a json object");
435+
return false;
436+
}
433437
if (!integrity_.FromRapidValue(rapid_value["integrity"])) {
434438
return false;
435439
}

kv_cache_manager/data_storage/test/storage_config_test.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,14 @@ TEST_F(StorageConfigTest, TestStorageConfigRejectsMalformedIntegrity) {
207207
StorageConfig config;
208208
EXPECT_FALSE(config.FromJsonString(malformed_json));
209209
}
210+
211+
TEST_F(StorageConfigTest, TestStorageConfigRejectsNonObjectIntegrity) {
212+
const std::string malformed_json = R"({
213+
"type": "file",
214+
"global_unique_name": "bad_integrity",
215+
"storage_spec": {"root_path": "/tmp/x", "key_count_per_file": 1},
216+
"integrity": "bad"
217+
})";
218+
StorageConfig config;
219+
EXPECT_FALSE(config.FromJsonString(malformed_json));
220+
}

kv_cache_manager/data_storage/test/vineyard_backend_test.cc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,23 @@ TEST_F(VineyardBackendTest, GenerationBumpsOnReRegistration) {
267267
ASSERT_EQ(EC_OK, backend.Close());
268268
}
269269

270+
TEST_F(VineyardBackendTest, GuardedUnregisterSkipsStaleGeneration) {
271+
VineyardBackend backend(metrics_registry_);
272+
ASSERT_EQ(EC_OK, backend.Open(MakeConfig(/*hb*/ 200, /*grace*/ 5000, /*tick*/ 50), "trace"));
273+
274+
const std::string host = "10.0.0.8:8080";
275+
ASSERT_EQ(EC_OK, backend.RegisterNode("test_inst", host, {"mem"}));
276+
uint64_t stale_gen = backend.GetNodeGeneration("test_inst", host);
277+
278+
ASSERT_EQ(EC_OK, backend.RegisterNode("test_inst", host, {"mem", "disk"}));
279+
ASSERT_NE(stale_gen, backend.GetNodeGeneration("test_inst", host));
280+
281+
ASSERT_EQ(EC_OK, backend.UnregisterNodeIfGenerationMatches("test_inst", host, stale_gen));
282+
EXPECT_TRUE(backend.IsNodeAvailable("test_inst", host));
283+
284+
ASSERT_EQ(EC_OK, backend.Close());
285+
}
286+
270287
// (10) Cleanup callback receives correct generation
271288
TEST_F(VineyardBackendTest, LivenessLoopPassesGenerationToCallback) {
272289
VineyardBackend backend(metrics_registry_);

0 commit comments

Comments
 (0)