项目文件夹

文件
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

197 行
8.0 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.
// End-to-end lifecycle tests for the flat KV-cache FSM path
// (TOKENSPEED_FLAT_KVCACHE=ON; the whole file compiles to nothing otherwise).
// Config: two paged-cache groups (full + sliding-window), no L2/L3.
#if TOKENSPEED_FLAT_KVCACHE
#include <algorithm>
#include <optional>
#include "integration_test_helper.h"
namespace tokenspeed::test {
class FlatKvCacheLifecycleTestSuite : public SchedulerTestSuite {
protected:
SchedulerConfig MakeConfig() override {
SchedulerConfig cfg{};
cfg.block_size = 2;
cfg.device_allocator.total_pages = 32;
cfg.host_allocator.total_pages = 32;
cfg.max_scheduled_tokens = 64;
cfg.max_batch_size = 8;
cfg.enable_l3_storage = false;
cfg.disable_l2_cache = true;
cfg.disable_prefix_cache = true;
PagedCacheGroupConfig full_grp;
full_grp.group_id = "full";
full_grp.rows_per_page = cfg.block_size;
full_grp.entry_stride_tokens = 1;
full_grp.total_pages = cfg.device_allocator.total_pages;
full_grp.retention = PagedCacheGroupConfig::Retention::FullHistory;
full_grp.family = PagedCacheGroupFamily::History;
PagedCacheGroupConfig swa_grp;
swa_grp.group_id = "swa";
swa_grp.rows_per_page = cfg.block_size;
swa_grp.entry_stride_tokens = 1;
swa_grp.total_pages = cfg.device_allocator.total_pages;
swa_grp.retention = PagedCacheGroupConfig::Retention::SlidingWindow;
swa_grp.sliding_window_tokens = 4;
swa_grp.family = PagedCacheGroupFamily::State;
cfg.paged_cache_groups = {full_grp, swa_grp};
return cfg;
}
static const FlatForwardOperation* FindFlatOp(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (const auto* f = std::get_if<FlatForwardOperation>(&op)) return f;
}
return nullptr;
}
};
TEST_F(FlatKvCacheLifecycleTestSuite, Construct_AndSubmit_Waiting) {
Submit(MakeRequestSpec("r1", /*num_pages=*/2));
EXPECT_EQ(scheduler_->WaitingSize(), 1u);
EXPECT_EQ(scheduler_->DecodingSize(), 0u);
}
TEST_F(FlatKvCacheLifecycleTestSuite, SingleRequest_PrefillDecodeFinish) {
const std::int32_t free_at_start = scheduler_->FlatPoolFreeBlocks();
Submit(MakeRequestSpec("r1", /*num_pages=*/2));
ExecutionPlan prefill_plan = PlanOnce();
EXPECT_EQ(scheduler_->WaitingSize(), 0u);
const FlatForwardOperation* prefill = FindFlatOp(prefill_plan);
ASSERT_NE(prefill, nullptr);
ASSERT_EQ(prefill->flat_block_tables.count("full"), 1u);
ASSERT_EQ(prefill->flat_block_tables.count("swa"), 1u);
EXPECT_EQ(prefill->flat_block_tables.at("full").size(), 1u);
EXPECT_EQ(prefill->flat_block_tables.at("swa").size(), 1u);
SendForwardDone("r1", {42});
EXPECT_EQ(scheduler_->PrefillSize(), 1u);
// Swa null hole first appears at decode step 1 (window=4 tokens = 2 pages).
// last_plan must outlive the loop: the FlatForwardOperation is owned by its plan.
std::optional<ExecutionPlan> last_plan;
int tok = 43;
for (int step = 0; step < 4; ++step) {
last_plan = PlanOnce();
ASSERT_NE(FindFlatOp(*last_plan), nullptr);
EXPECT_EQ(scheduler_->DecodingSize(), 1u);
SendForwardDone("r1", {tok++});
}
const FlatForwardOperation* last_decode = FindFlatOp(*last_plan);
ASSERT_NE(last_decode, nullptr);
const auto& full_row = last_decode->flat_block_tables.at("full").at(0);
for (std::int32_t id : full_row) {
EXPECT_GT(id, 0) << "full row should keep history with no null/padding hole";
}
const auto& swa_row = last_decode->flat_block_tables.at("swa").at(0);
EXPECT_NE(std::find(swa_row.begin(), swa_row.end(), 0), swa_row.end())
<< "swa row should contain a null hole after the sliding window slides";
SendFinish("r1");
PlanOnce();
EXPECT_EQ(scheduler_->DecodingSize(), 0u);
EXPECT_EQ(scheduler_->FlatPoolFreeBlocks(), free_at_start);
}
// AvailableKvPages() must report the flat shared BlockPool, not the radix
// device_allocator_. TODO(radix-removal): collapses to the only accessor.
TEST_F(FlatKvCacheLifecycleTestSuite, AvailableKvPagesReportsFlatPool) {
const std::size_t idle = scheduler_->AvailableKvPages();
EXPECT_EQ(idle, static_cast<std::size_t>(scheduler_->FlatPoolFreeBlocks()));
// 32 total pages, block 0 is the never-allocated null placeholder.
EXPECT_EQ(idle, 31u);
Submit(MakeRequestSpec("r1", /*num_pages=*/2));
PlanOnce();
EXPECT_EQ(scheduler_->AvailableKvPages(), static_cast<std::size_t>(scheduler_->FlatPoolFreeBlocks()));
EXPECT_LT(scheduler_->AvailableKvPages(), idle)
<< "prefill draws from the flat pool and the bound accessor must see it";
SendForwardDone("r1", {42});
SendFinish("r1");
PlanOnce();
EXPECT_EQ(scheduler_->AvailableKvPages(), idle);
}
TEST_F(FlatKvCacheLifecycleTestSuite, TwoRequests_BatchedFlatBlockTables) {
const std::int32_t free_at_start = scheduler_->FlatPoolFreeBlocks();
Submit(MakeRequestSpec("r1", /*num_pages=*/2));
Submit(MakeRequestSpec("r2", /*num_pages=*/3, /*start=*/101));
ExecutionPlan prefill_plan = PlanOnce();
EXPECT_EQ(scheduler_->WaitingSize(), 0u);
const FlatForwardOperation* prefill = FindFlatOp(prefill_plan);
ASSERT_NE(prefill, nullptr);
ASSERT_EQ(prefill->request_ids.size(), 2u);
ASSERT_EQ(prefill->flat_block_tables.count("full"), 1u);
ASSERT_EQ(prefill->flat_block_tables.count("swa"), 1u);
const auto& full = prefill->flat_block_tables.at("full");
const auto& swa = prefill->flat_block_tables.at("swa");
ASSERT_EQ(full.size(), 2u);
ASSERT_EQ(swa.size(), 2u);
EXPECT_EQ(full.at(0).size(), full.at(1).size());
EXPECT_EQ(swa.at(0).size(), swa.at(1).size());
const bool any_pad = std::any_of(full.at(0).begin(), full.at(0).end(), [](std::int32_t id) { return id == -1; }) ||
std::any_of(full.at(1).begin(), full.at(1).end(), [](std::int32_t id) { return id == -1; });
EXPECT_TRUE(any_pad) << "unequal prompt lengths should force -1 padding in one full row";
auto assert_no_page_collision = [](const std::vector<std::vector<std::int32_t>>& group) {
std::vector<std::int32_t> real;
for (const auto& row : group) {
for (std::int32_t id : row) {
if (id > 0) real.push_back(id);
}
}
std::vector<std::int32_t> sorted = real;
std::sort(sorted.begin(), sorted.end());
EXPECT_EQ(std::adjacent_find(sorted.begin(), sorted.end()), sorted.end())
<< "two requests must not be handed the same physical page";
};
assert_no_page_collision(full);
assert_no_page_collision(swa);
SendForwardDone("r1", {42});
SendForwardDone("r2", {142});
SendFinish("r1");
SendFinish("r2");
PlanOnce();
EXPECT_EQ(scheduler_->DecodingSize(), 0u);
EXPECT_EQ(scheduler_->FlatPoolFreeBlocks(), free_at_start);
}
} // namespace tokenspeed::test
#endif // TOKENSPEED_FLAT_KVCACHE