项目文件夹

文件
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

370 行
14 KiB
C++

// Copyright (c) 2026 LightSeek Foundation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#include <gtest/gtest.h>
#include <cstdint>
#include <span>
#include <string>
#include <vector>
#include "scheduler/page_hasher.h"
namespace tokenspeed::test {
namespace {
using token_span = std::span<const std::int32_t>;
using key_span = std::span<const std::string>;
// HashPage frames the input as [prior_len][prior][token_count][tokens][extra...];
// all-empty input is two zero u32s (8 zero bytes), whose SHA-256 is below.
constexpr const char* kEmptyFramedSha256 = "af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc";
token_span Tokens(const std::vector<std::int32_t>& v) {
return token_span(v.data(), v.size());
}
key_span Keys(const std::vector<std::string>& v) {
return key_span(v.data(), v.size());
}
// ---- hex helpers --------------------------------------------------------
TEST(PageHasherHexTest, AppendHexBytesIsLowercaseTwoCharsPerByte) {
std::string out;
const uint8_t bytes[] = {0x00, 0x0f, 0xa3, 0xff};
AppendHexBytes(out, bytes, 4);
EXPECT_EQ(out, "000fa3ff");
}
TEST(PageHasherHexTest, HexToBytesIsInverseOfAppend) {
const uint8_t bytes[] = {0x00, 0x01, 0x7e, 0x80, 0xab, 0xff};
std::string hex;
AppendHexBytes(hex, bytes, sizeof(bytes));
std::vector<uint8_t> decoded = HexToBytes(hex);
ASSERT_EQ(decoded.size(), sizeof(bytes));
for (std::size_t i = 0; i < sizeof(bytes); ++i) {
EXPECT_EQ(decoded[i], bytes[i]) << "byte " << i;
}
}
TEST(PageHasherHexTest, HexToBytesAcceptsUppercase) {
EXPECT_EQ(HexToBytes("ABCDEF"), HexToBytes("abcdef"));
}
// ---- HashPage -----------------------------------------------------------
TEST(HashPageTest, EmptyPageMatchesKnownSha256) {
std::vector<std::int32_t> none;
EXPECT_EQ(HashPage(Tokens(none), ""), kEmptyFramedSha256);
}
TEST(HashPageTest, OutputIs64HexChars) {
std::vector<std::int32_t> toks = {1, 2, 3};
std::string h = HashPage(Tokens(toks), "");
EXPECT_EQ(h.size(), 64u);
EXPECT_EQ(h.find_first_not_of("0123456789abcdef"), std::string::npos);
}
TEST(HashPageTest, Deterministic) {
std::vector<std::int32_t> toks = {7, 8, 9};
EXPECT_EQ(HashPage(Tokens(toks), "seed"), HashPage(Tokens(toks), "seed"));
}
TEST(HashPageTest, DifferentTokensDifferentHash) {
std::vector<std::int32_t> a = {1, 2, 3};
std::vector<std::int32_t> b = {1, 2, 4};
EXPECT_NE(HashPage(Tokens(a), ""), HashPage(Tokens(b), ""));
}
TEST(HashPageTest, TokenOrderMatters) {
std::vector<std::int32_t> a = {1, 2};
std::vector<std::int32_t> b = {2, 1};
EXPECT_NE(HashPage(Tokens(a), ""), HashPage(Tokens(b), ""));
}
TEST(HashPageTest, PriorHashChangesOutput) {
std::vector<std::int32_t> toks = {5, 6};
std::string no_prior = HashPage(Tokens(toks), "");
std::string with_prior = HashPage(Tokens(toks), no_prior);
EXPECT_NE(no_prior, with_prior);
}
TEST(HashPageTest, EmptyExtraKeysEqualsTwoArgForm) {
std::vector<std::int32_t> toks = {1, 2, 3};
std::vector<std::string> empty;
EXPECT_EQ(HashPage(Tokens(toks), "p"), HashPage(Tokens(toks), "p", Keys(empty)));
}
TEST(HashPageTest, ExtraKeysChangeOutput) {
std::vector<std::int32_t> toks = {1, 2, 3};
std::vector<std::string> keys = {"lora-A"};
EXPECT_NE(HashPage(Tokens(toks), "p"), HashPage(Tokens(toks), "p", Keys(keys)));
}
TEST(HashPageTest, FramingDisambiguatesKeySplits) {
std::vector<std::int32_t> toks = {1};
std::vector<std::string> split_a = {"ab", "c"};
std::vector<std::string> split_b = {"a", "bc"};
EXPECT_NE(HashPage(Tokens(toks), "", Keys(split_a)), HashPage(Tokens(toks), "", Keys(split_b)));
}
TEST(HashPageTest, FramingDisambiguatesKeyCount) {
std::vector<std::int32_t> toks = {1};
std::vector<std::string> one = {"abc"};
std::vector<std::string> two = {"a", "bc"};
EXPECT_NE(HashPage(Tokens(toks), "", Keys(one)), HashPage(Tokens(toks), "", Keys(two)));
}
// A 32-byte prior reinterpreted as 8 LE tokens in page 0 must not produce the
// same stream as a chained page carrying that digest as prior.
TEST(HashPageTest, FramingDisambiguatesEmptyPriorFromChainedPage) {
const std::string prior = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
std::vector<uint8_t> pb = HexToBytes(prior);
ASSERT_EQ(pb.size(), 32u);
std::vector<std::int32_t> toks(8);
for (std::size_t i = 0; i < 8; ++i) {
toks[i] = static_cast<std::int32_t>(
static_cast<uint32_t>(pb[4 * i]) | (static_cast<uint32_t>(pb[4 * i + 1]) << 8) |
(static_cast<uint32_t>(pb[4 * i + 2]) << 16) | (static_cast<uint32_t>(pb[4 * i + 3]) << 24));
}
std::vector<std::int32_t> none;
std::string as_page0 = HashPage(Tokens(toks), "");
std::string as_chained = HashPage(Tokens(none), prior);
EXPECT_NE(as_page0, as_chained);
}
// A 4-byte extra key must not produce the same stream as count(1) + len(4) +
// the key's LE int32 folded back into the token list.
TEST(HashPageTest, FramingDisambiguatesTokensFromExtraKeys) {
std::vector<std::int32_t> short_toks = {9, 8};
std::vector<std::string> one_key = {"wxyz"};
// count=1, len=4, then "wxyz" (0x7a797877 little-endian) as a trailing token.
std::vector<std::int32_t> long_toks = {9, 8, 1, 4, 0x7a797877};
std::vector<std::string> no_keys;
EXPECT_NE(HashPage(Tokens(short_toks), "", Keys(one_key)), HashPage(Tokens(long_toks), "", Keys(no_keys)));
}
// ---- ComputePagedHashes (chaining) -------------------------------------
TEST(ComputePagedHashesTest, MatchesManualRollingChain) {
std::vector<std::int32_t> p0 = {1, 2};
std::vector<std::int32_t> p1 = {3, 4};
std::vector<std::int32_t> p2 = {5, 6};
std::vector<token_span> pages = {Tokens(p0), Tokens(p1), Tokens(p2)};
std::vector<std::string> got = ComputePagedHashes(pages, "root");
std::string h0 = HashPage(Tokens(p0), "root");
std::string h1 = HashPage(Tokens(p1), h0);
std::string h2 = HashPage(Tokens(p2), h1);
ASSERT_EQ(got.size(), 3u);
EXPECT_EQ(got[0], h0);
EXPECT_EQ(got[1], h1);
EXPECT_EQ(got[2], h2);
}
TEST(ComputePagedHashesTest, SamePageDifferentPrefixDiffers) {
std::vector<std::int32_t> same = {9, 9};
std::vector<std::int32_t> other = {1, 1};
std::vector<token_span> a = {Tokens(same), Tokens(same)};
std::vector<token_span> b = {Tokens(other), Tokens(same)};
std::vector<std::string> ha = ComputePagedHashes(a, "");
std::vector<std::string> hb = ComputePagedHashes(b, "");
EXPECT_NE(ha[0], hb[0]);
EXPECT_NE(ha[1], hb[1]);
}
TEST(ComputePagedHashesTest, MissingExtraKeysPerPageTreatedAsEmpty) {
std::vector<std::int32_t> p0 = {1};
std::vector<std::int32_t> p1 = {2};
std::vector<token_span> pages = {Tokens(p0), Tokens(p1)};
std::vector<std::string> k0 = {"salt"};
std::vector<key_span> extra = {Keys(k0)};
std::vector<std::string> got = ComputePagedHashes(pages, "", extra);
std::string h0 = HashPage(Tokens(p0), "", Keys(k0));
std::string h1 = HashPage(Tokens(p1), h0);
EXPECT_EQ(got[0], h0);
EXPECT_EQ(got[1], h1);
}
TEST(ComputePagedHashesTest, IncrementalChainEqualsOneShot) {
// 12-token stream, page_size 2 -> 6 pages.
std::vector<std::int32_t> tokens(12);
for (std::int32_t i = 0; i < 12; ++i) {
tokens[i] = 100 + i;
}
std::vector<token_span> pages;
for (std::size_t start = 0; start < tokens.size(); start += 2) {
pages.push_back(token_span(tokens.data() + start, 2));
}
const std::vector<std::string> one_shot = ComputePagedHashes(pages, "");
const std::vector<token_span> head(pages.begin(), pages.begin() + 3);
const std::vector<token_span> tail(pages.begin() + 3, pages.end());
std::vector<std::string> incremental = ComputePagedHashes(head, "");
const std::vector<std::string> rest = ComputePagedHashes(tail, incremental.back());
incremental.insert(incremental.end(), rest.begin(), rest.end());
EXPECT_EQ(incremental, one_shot);
}
// ---- group_id pack / unpack --------------------------------------------
TEST(GroupIdTest, KeyIsContentHashPlusEightHex) {
std::string content(64, 'a');
std::string key = MakeKeyWithGroupId(content, 7);
EXPECT_EQ(key.size(), 72u);
EXPECT_EQ(key.substr(0, 64), content);
EXPECT_EQ(key.substr(64), "00000007"); // big-endian
}
TEST(GroupIdTest, BigEndianByteOrder) {
std::string key = MakeKeyWithGroupId(std::string(64, 'a'), 0x01020304u);
EXPECT_EQ(key.substr(64), "01020304");
}
TEST(GroupIdTest, RoundTrip) {
std::string content(64, 'd');
for (uint32_t gid : {0u, 1u, 7u, 255u, 256u, 0xdeadbeefu, 0xffffffffu}) {
std::string key = MakeKeyWithGroupId(content, gid);
EXPECT_EQ(GetBlockHashFromKey(key), content) << "gid " << gid;
EXPECT_EQ(GetGroupIdFromHashKey(key), gid) << "gid " << gid;
}
}
TEST(GroupIdTest, ContentHashIndependentOfGroup) {
std::string content(64, 'c');
std::string k0 = MakeKeyWithGroupId(content, 0);
std::string k1 = MakeKeyWithGroupId(content, 1);
EXPECT_EQ(GetBlockHashFromKey(k0), GetBlockHashFromKey(k1));
EXPECT_NE(k0, k1);
}
TEST(GroupIdTest, ShortKeyDecodesDefensively) {
EXPECT_EQ(GetBlockHashFromKey("abc"), "");
EXPECT_EQ(GetGroupIdFromHashKey("abc"), 0u);
}
// ---- ComputePagedHashesWithGroup ---------------------------------------
TEST(ComputePagedHashesWithGroupTest, EqualsBareHashesWrappedWithGroup) {
std::vector<std::int32_t> p0 = {1, 2};
std::vector<std::int32_t> p1 = {3, 4};
std::vector<token_span> pages = {Tokens(p0), Tokens(p1)};
std::vector<std::string> bare = ComputePagedHashes(pages, "r");
std::vector<std::string> grouped = ComputePagedHashesWithGroup(pages, "r", 42);
ASSERT_EQ(grouped.size(), bare.size());
for (std::size_t i = 0; i < bare.size(); ++i) {
EXPECT_EQ(grouped[i], MakeKeyWithGroupId(bare[i], 42)) << "page " << i;
// group_id rides outside the chain: stripping it recovers the bare hash.
EXPECT_EQ(GetBlockHashFromKey(grouped[i]), bare[i]) << "page " << i;
}
}
TEST(ComputePagedHashesWithGroupTest, GroupDoesNotLeakIntoPrefixChain) {
std::vector<std::int32_t> p0 = {1, 2};
std::vector<std::int32_t> p1 = {3, 4};
std::vector<token_span> pages = {Tokens(p0), Tokens(p1)};
std::vector<std::string> g0 = ComputePagedHashesWithGroup(pages, "r", 0);
std::vector<std::string> g9 = ComputePagedHashesWithGroup(pages, "r", 9);
for (std::size_t i = 0; i < g0.size(); ++i) {
EXPECT_EQ(GetBlockHashFromKey(g0[i]), GetBlockHashFromKey(g9[i])) << "page " << i;
}
}
// ---- FoldBaseHashes ----------------------------------------------------
TEST(FoldBaseHashesTest, IdentityWhenGroupEqualsBase) {
std::vector<std::string> base = {"aa", "bb", "cc"};
auto folded = FoldBaseHashes(base, /*first_base=*/0, /*m=*/1);
ASSERT_EQ(folded.size(), 3u);
EXPECT_EQ(folded[0], HashPage(std::span<const std::int32_t>{}, "", std::vector<std::string>{"aa"}));
}
TEST(FoldBaseHashesTest, FoldsMConsecutiveIntoOneOrderSensitive) {
std::vector<std::string> base = {"a0", "a1", "a2", "a3", "a4", "a5"};
auto folded = FoldBaseHashes(base, 0, 2);
ASSERT_EQ(folded.size(), 3u);
std::vector<std::string> swapped = {"a1", "a0", "a2", "a3", "a4", "a5"};
auto folded2 = FoldBaseHashes(swapped, 0, 2);
EXPECT_NE(folded[0], folded2[0]);
EXPECT_EQ(folded[1], folded2[1]);
}
TEST(FoldBaseHashesTest, DropsIncompleteTrailingGroupBlock) {
std::vector<std::string> base = {"a0", "a1", "a2", "a3", "a4"};
auto folded = FoldBaseHashes(base, 0, 2);
EXPECT_EQ(folded.size(), 2u);
}
TEST(FoldBaseHashesTest, FirstBaseOffsetShiftsFoldWindow) {
// first_base=1, m=2: first_base%m==1 -> drop 1 leading page, [a2,a3] folds into 1 block
std::vector<std::string> base = {"a1", "a2", "a3"};
auto folded = FoldBaseHashes(base, /*first_base=*/1, /*m=*/2);
ASSERT_EQ(folded.size(), 1u);
}
// ---- MakeFoldedGroupKeys ----------------------------------------------
TEST(MakeFoldedGroupKeysTest, MEqualsOneIsByteIdenticalToRawPerBaseKey) {
std::vector<std::string> base = {"aa", "bb", "cc"};
auto keys = MakeFoldedGroupKeys(base, /*group_id=*/7, /*m=*/1);
ASSERT_EQ(keys.size(), 3u);
for (std::size_t i = 0; i < base.size(); ++i) {
EXPECT_EQ(keys[i], MakeKeyWithGroupId(base[i], 7)) << "page " << i;
}
}
TEST(MakeFoldedGroupKeysTest, MTwoFoldsThenWrapsGroupId) {
std::vector<std::string> base = {"a0", "a1", "a2", "a3"};
auto keys = MakeFoldedGroupKeys(base, /*group_id=*/3, /*m=*/2);
auto folded = FoldBaseHashes(base, /*first_base=*/0, /*m=*/2);
ASSERT_EQ(keys.size(), 2u);
ASSERT_EQ(folded.size(), 2u);
for (std::size_t i = 0; i < folded.size(); ++i) {
EXPECT_EQ(keys[i], MakeKeyWithGroupId(folded[i], 3)) << "coarse block " << i;
}
}
TEST(MakeFoldedGroupKeysTest, FirstBaseOffsetShiftsFoldedKeys) {
// first_base=1, m=2 -> drop 1 leading base page, [a2,a3] fold into 1 coarse key.
std::vector<std::string> base = {"a1", "a2", "a3"};
auto keys = MakeFoldedGroupKeys(base, /*group_id=*/0, /*m=*/2, /*first_base=*/1);
auto folded = FoldBaseHashes(base, /*first_base=*/1, /*m=*/2);
ASSERT_EQ(keys.size(), 1u);
EXPECT_EQ(keys[0], MakeKeyWithGroupId(folded[0], 0));
}
} // namespace
} // namespace tokenspeed::test