Skip to content

Commit 8c275ee

Browse files
committed
fix checksum verification review gaps
1 parent e4645fe commit 8c275ee

4 files changed

Lines changed: 66 additions & 65 deletions

File tree

kv_cache_manager/client/src/internal/util/checksum_verify_util.h

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,15 @@
66

77
namespace kv_cache_manager {
88

9-
// Two-stage block checksum verification for the read path.
10-
//
11-
// Stage 1 (fast): XOR-aggregate expected and actual into one uint64 each and
12-
// compare once. Each block's contribution is multiplied by a position-dependent
13-
// odd constant before being XORed in, so a plain block swap (expected=[A,B] vs
14-
// actual=[B,A]) changes the aggregate — the fast path is order-sensitive.
15-
// Common case: a Load batch that is fully consistent returns in O(1) compares
16-
// regardless of batch size, with no per-block branching.
17-
//
18-
// Stage 2 (slow, only on fast-path mismatch or when strict_mode=true): walk the
19-
// batch and report the indices of every block whose checksum disagrees. The
20-
// caller uses these indices to log per-block diagnostics or publish a per-block
21-
// ChecksumMismatchEvent.
9+
// Walk the batch and report the indices of every block whose checksum disagrees.
10+
// The caller uses these indices to log per-block diagnostics or publish a
11+
// per-block ChecksumMismatchEvent.
2212
//
2313
// Sentinels: expected[i] == 0 means "no checksum was stored for this block"
2414
// (legacy data or legacy client). Such entries are skipped in both stages.
2515
//
26-
// strict_mode (typically driven by KVCM_CHECKSUM_STRICT_MODE) bypasses the fast
27-
// aggregate and always compares per block. Kept as a diagnostic knob for
28-
// on-call: when triaging a suspected data-integrity issue you may want per-block
29-
// index output without relying on the fast-path fallback triggering.
16+
// strict_mode is kept for API compatibility with earlier revisions; verification
17+
// is always per-block so the function cannot accept XOR-cancelled batches.
3018
struct ChecksumVerifyResult {
3119
bool mismatch = false;
3220
std::vector<std::size_t> faulty_indices; // populated only when mismatch == true
@@ -35,46 +23,12 @@ struct ChecksumVerifyResult {
3523
inline ChecksumVerifyResult VerifyBatchChecksums(const std::vector<std::int64_t> &expected,
3624
const std::vector<std::int64_t> &actual,
3725
bool strict_mode) {
26+
(void)strict_mode;
3827
ChecksumVerifyResult result;
3928
if (expected.size() != actual.size()) {
4029
result.mismatch = true;
4130
return result;
4231
}
43-
if (strict_mode) {
44-
for (std::size_t i = 0; i < actual.size(); ++i) {
45-
if (expected[i] == 0) {
46-
continue;
47-
}
48-
if (expected[i] != actual[i]) {
49-
result.faulty_indices.push_back(i);
50-
}
51-
}
52-
result.mismatch = !result.faulty_indices.empty();
53-
return result;
54-
}
55-
// fast path: xor aggregate with a position-dependent odd multiplier so a
56-
// plain block swap changes the aggregate (integer multiplication is not
57-
// GF(2)-linear, so (A*m0)^(B*m1) != (B*m0)^(A*m1) in general). Multiplying
58-
// by an odd constant is bijective mod 2^64, preserving per-element entropy.
59-
// kIndexSalt is the 2^64/phi constant used by splitmix64/xxhash; any well-
60-
// dispersed odd constant works.
61-
constexpr std::uint64_t kIndexSalt = 0x9E3779B97F4A7C15ULL;
62-
std::uint64_t expected_xor = 0;
63-
std::uint64_t actual_xor = 0;
64-
bool any_compared = false;
65-
for (std::size_t i = 0; i < actual.size(); ++i) {
66-
if (expected[i] == 0) {
67-
continue;
68-
}
69-
const std::uint64_t multiplier = (2ULL * i + 1ULL) * kIndexSalt;
70-
expected_xor ^= static_cast<std::uint64_t>(expected[i]) * multiplier;
71-
actual_xor ^= static_cast<std::uint64_t>(actual[i]) * multiplier;
72-
any_compared = true;
73-
}
74-
if (!any_compared || expected_xor == actual_xor) {
75-
return result; // all match (or nothing to check)
76-
}
77-
// fast path detected a mismatch; locate the offending block(s)
7832
for (std::size_t i = 0; i < actual.size(); ++i) {
7933
if (expected[i] == 0) {
8034
continue;

kv_cache_manager/client/src/internal/util/test/checksum_verify_util_test.cc

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ using namespace kv_cache_manager;
77

88
class ChecksumVerifyUtilTest : public TESTBASE {};
99

10-
// Fast path:所有 block 一致 -> mismatch=false, faulty_indices 空。
10+
// 所有 block 一致 -> mismatch=false, faulty_indices 空。
1111
TEST_F(ChecksumVerifyUtilTest, FastPathAllMatch) {
1212
std::vector<int64_t> expected = {0x1111, 0x2222, 0x3333};
1313
std::vector<int64_t> actual = expected;
@@ -16,7 +16,7 @@ TEST_F(ChecksumVerifyUtilTest, FastPathAllMatch) {
1616
EXPECT_TRUE(r.faulty_indices.empty());
1717
}
1818

19-
// Fast path:检测到不匹配后应该回填 faulty_indices 让上层逐块打日志。
19+
// 检测到不匹配后应该回填 faulty_indices 让上层逐块打日志。
2020
TEST_F(ChecksumVerifyUtilTest, FastPathDetectsMismatchAndLocatesIndex) {
2121
std::vector<int64_t> expected = {0x1111, 0x2222, 0x3333};
2222
std::vector<int64_t> actual = {0x1111, 0xFFFF, 0x3333}; // block #1 错
@@ -26,7 +26,7 @@ TEST_F(ChecksumVerifyUtilTest, FastPathDetectsMismatchAndLocatesIndex) {
2626
EXPECT_EQ(r.faulty_indices[0], 1u);
2727
}
2828

29-
// 多个错位 block:fallback 阶段把所有错的都列出来
29+
// 多个错位 block:把所有错的都列出来
3030
TEST_F(ChecksumVerifyUtilTest, FastPathListsAllFaultyBlocks) {
3131
std::vector<int64_t> expected = {0x1111, 0x2222, 0x3333, 0x4444};
3232
std::vector<int64_t> actual = {0xAAAA, 0x2222, 0xBBBB, 0x4444}; // #0, #2 错
@@ -45,7 +45,7 @@ TEST_F(ChecksumVerifyUtilTest, FastPathSentinelZeroIsSkipped) {
4545
EXPECT_FALSE(r.mismatch);
4646
}
4747

48-
// 全 sentinel:fast 路径没有可比较的项,等同于全 match。
48+
// 全 sentinel:没有可比较的项,等同于全 match。
4949
TEST_F(ChecksumVerifyUtilTest, FastPathAllSentinelsTreatedAsMatch) {
5050
std::vector<int64_t> expected = {0, 0, 0};
5151
std::vector<int64_t> actual = {0xAA, 0xBB, 0xCC};
@@ -62,7 +62,7 @@ TEST_F(ChecksumVerifyUtilTest, SizeMismatchReturnsMismatchWithoutIndices) {
6262
EXPECT_TRUE(r.faulty_indices.empty());
6363
}
6464

65-
// Strict mode:行为跟 fast fallback 一致 (per-block 比对),但跳过 fast 聚合阶段
65+
// strict_mode 参数保留兼容;当前实现始终逐块比对
6666
TEST_F(ChecksumVerifyUtilTest, StrictModeMatchesFastFallback) {
6767
std::vector<int64_t> expected = {0x1111, 0, 0x3333, 0x4444};
6868
std::vector<int64_t> actual = {0xAAAA, 0x2222, 0x3333, 0xBBBB}; // #0, #3 错;#1 是 sentinel
@@ -81,8 +81,7 @@ TEST_F(ChecksumVerifyUtilTest, StrictModeAllMatch) {
8181
EXPECT_FALSE(r.mismatch);
8282
}
8383

84-
// Block swap (读串): expected=[A,B], actual=[B,A]. 老 XOR 聚合无序会漏,
85-
// 加了 position-dependent 奇数乘子后 fast path 也能识别并回填两个 faulty index。
84+
// Block swap (读串): expected=[A,B], actual=[B,A]. 必须识别并回填两个 faulty index。
8685
TEST_F(ChecksumVerifyUtilTest, FastPathCatchesBlockSwap) {
8786
std::vector<int64_t> expected = {0xAAAA, 0xBBBB};
8887
std::vector<int64_t> actual = {0xBBBB, 0xAAAA};
@@ -98,8 +97,7 @@ TEST_F(ChecksumVerifyUtilTest, FastPathCatchesBlockSwap) {
9897
}
9998

10099
// Same-delta 成对突变:每个 block 都被同一 delta 改写 (expected=[A,B],
101-
// actual=[A^X, B^X])。老 XOR fast 会 delta 对消而漏;新的乘法聚合不再有 GF(2)-
102-
// 线性,所以两条路径都能识别。这里同时断言,防止将来实现回退成纯 XOR 时漏检回归。
100+
// actual=[A^X, B^X])。老 XOR fast 会 delta 对消而漏;逐块比对必须识别。
103101
TEST_F(ChecksumVerifyUtilTest, DetectsSameDeltaPairedMutation) {
104102
constexpr int64_t kDelta = 0x0F0F0F0F0F0F0F0FLL;
105103
std::vector<int64_t> expected = {0xAAAA, 0xBBBB};
@@ -111,3 +109,17 @@ TEST_F(ChecksumVerifyUtilTest, DetectsSameDeltaPairedMutation) {
111109
ASSERT_TRUE(r_strict.mismatch);
112110
EXPECT_EQ(r_strict.faulty_indices.size(), 2u);
113111
}
112+
113+
// High-bit 成对突变:奇数乘法聚合也会让最高位 delta 在偶数个 block 中抵消。
114+
// 逐块比对不能接受这种 batch。
115+
TEST_F(ChecksumVerifyUtilTest, DetectsHighBitPairedMutation) {
116+
constexpr int64_t kHighBit = static_cast<int64_t>(0x8000000000000000ULL);
117+
std::vector<int64_t> expected = {0x1111, 0x2222, 0x3333};
118+
std::vector<int64_t> actual = {0x1111 ^ kHighBit, 0x2222 ^ kHighBit, 0x3333};
119+
120+
auto r = VerifyBatchChecksums(expected, actual, /*strict_mode=*/false);
121+
ASSERT_TRUE(r.mismatch);
122+
ASSERT_EQ(r.faulty_indices.size(), 2u);
123+
EXPECT_EQ(r.faulty_indices[0], 0u);
124+
EXPECT_EQ(r.faulty_indices[1], 1u);
125+
}

kv_cache_manager/client/src/transfer_client_impl.cc

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ bool IsChecksumHashableBlock(const BlockBuffer &block_buffer) {
3838
});
3939
}
4040

41+
std::vector<size_t> GetIovSizeShape(const BlockBuffer &block_buffer) {
42+
std::vector<size_t> shape;
43+
shape.reserve(block_buffer.iovs.size());
44+
for (const auto &iov : block_buffer.iovs) {
45+
shape.push_back(iov.size);
46+
}
47+
return shape;
48+
}
49+
4150
bool HashBlocksByIovShape(const BlockBuffers &block_buffers,
4251
SdkBufferCheckPool::CellHandle &handle,
4352
size_t max_check_iov_num,
@@ -50,7 +59,7 @@ bool HashBlocksByIovShape(const BlockBuffers &block_buffers,
5059

5160
BlockBuffers chunk;
5261
std::vector<size_t> chunk_indices;
53-
size_t chunk_iov_num = 0;
62+
std::vector<size_t> chunk_iov_shape;
5463
size_t chunk_total_iovs = 0;
5564

5665
auto flush_chunk = [&]() -> bool {
@@ -68,7 +77,7 @@ bool HashBlocksByIovShape(const BlockBuffers &block_buffers,
6877
}
6978
chunk.clear();
7079
chunk_indices.clear();
71-
chunk_iov_num = 0;
80+
chunk_iov_shape.clear();
7281
chunk_total_iovs = 0;
7382
return true;
7483
};
@@ -79,13 +88,14 @@ bool HashBlocksByIovShape(const BlockBuffers &block_buffers,
7988
KVCM_LOG_ERROR("block [%zu] has invalid iov_num [%zu] for checksum hash", i, iov_num);
8089
return false;
8190
}
82-
if (!chunk.empty() && (iov_num != chunk_iov_num || chunk_total_iovs + iov_num > max_check_iov_num)) {
91+
auto iov_shape = GetIovSizeShape(block_buffers[i]);
92+
if (!chunk.empty() && (iov_shape != chunk_iov_shape || chunk_total_iovs + iov_num > max_check_iov_num)) {
8393
if (!flush_chunk()) {
8494
return false;
8595
}
8696
}
8797
if (chunk.empty()) {
88-
chunk_iov_num = iov_num;
98+
chunk_iov_shape = std::move(iov_shape);
8999
}
90100
chunk.push_back(block_buffers[i]);
91101
chunk_indices.push_back(i);

kv_cache_manager/client/test/transfer_client_test.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,31 @@ TEST_F(TransferClientTest, TestCreateAcceptsMetaChecksumSpec) {
305305
delete init_params.regist_span;
306306
}
307307

308+
TEST_F(TransferClientTest, TestCreateRejectsUnsupportedChecksumAlgo) {
309+
auto init_params = init_params_;
310+
init_params.regist_span = new RegistSpan();
311+
init_params.regist_span->base = malloc(1024 * 1024);
312+
init_params.regist_span->size = 1024 * 1024;
313+
init_params.storage_configs = R"([
314+
{
315+
"type": "file",
316+
"global_unique_name": "test_nfs",
317+
"storage_spec": {
318+
"root_path": "/tmp/test/",
319+
"key_count_per_file": 5
320+
},
321+
"integrity": {
322+
"enable_meta_checksum": true,
323+
"algo": "unknown_algo"
324+
}
325+
}
326+
])";
327+
auto client = TransferClient::Create(client_config_, init_params);
328+
EXPECT_EQ(client, nullptr);
329+
free(init_params.regist_span->base);
330+
delete init_params.regist_span;
331+
}
332+
308333
// expected_checksums 全 0 -> sentinel 跳过校验,行为 == 老路径,不会因 checksum 不匹配返回错误。
309334
TEST_F(TransferClientTest, TestLoadKvCachesExpectedHashesAllZeroSkipsCheck) {
310335
auto client = TransferClient::Create(client_config_, init_params_);

0 commit comments

Comments
 (0)