项目文件夹

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

431 行
15 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 "integration_test_helper.h"
namespace tokenspeed::test {
class MambaIntegrationTest : public SchedulerTestSuite {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.enable_mamba = true;
cfg.mamba_pool_total_chunks = 16;
return cfg;
}
};
TEST_F(MambaIntegrationTest, BasicPrefillDecodeFinish) {
Submit(MakeRequestSpec("r1", 2));
auto plan1 = PlanOnce();
EXPECT_FALSE(plan1.Operations().empty());
SendForwardDone("r1", {100});
auto plan2 = PlanOnce();
EXPECT_FALSE(plan2.Operations().empty());
SendFinish("r1");
auto plan3 = PlanOnce();
}
TEST_F(MambaIntegrationTest, PrefixSharingWithMamba) {
// Use 5 pages so that after decode-token append (11 tokens),
// except_last gives 5 full pages. R2 also uses 5 pages (10 tokens),
// except_last → (10-1)/2 = 4 pages, but the radix tree stores 5 pages
// from R1's FinishEvent. The mamba slot lives at depth 5, so R2's
// 4-page query still walks up and finds the mamba at depth 4 (the
// checkpoint inserted during ScheduleDecodeEvent).
//
// A cleaner approach: use 4 pages for R1 and verify that R2 gets a
// partial mamba prefix hit of 3 pages (since except_last on 8 aligned
// tokens yields only 3 pages for the match query).
const std::int32_t kPages = 4;
Submit(MakeRequestSpec("r1", kPages));
auto plan1 = PlanOnce();
ASSERT_FALSE(plan1.Operations().empty());
SendForwardDone("r1", {100});
auto plan2 = PlanOnce();
ASSERT_FALSE(plan2.Operations().empty());
SendFinish("r1");
PlanOnce();
// R2: same prefix tokens. GetFullPagedTokens(except_last=true) on
// kPages*PageSize tokens yields (kPages*PageSize - 1) / PageSize pages.
// With block_size=2, 4 pages = 8 tokens → except_last → 3 pages.
// The mamba checkpoint from R1 sits at depth 4 (inserted during decode
// transition), but the mamba working slot also at depth 4 (from Finish).
// R2's match walks 3 pages; FindLastMambaNode walks up — the ancestor
// chain doesn't include the 4-page-depth node since it's a sibling.
//
// The tree after R1 finish looks like:
// root → [page0,page1,page2,page3] (4-page node, has mamba)
// R2 queries with 3 pages → match hits first 3 pages of the 4-page node
// via split. After split: root → 3-page prefix → 1-page suffix (mamba).
// FindLastMambaNode from 3-page prefix → no mamba → walks up → root → null.
//
// So with aligned tokens, R2 does NOT get a mamba hit.
// Verify the partial-hit behavior:
Submit(MakeRequestSpec("r2", kPages));
auto plan3 = PlanOnce();
ASSERT_FALSE(plan3.Operations().empty());
const auto& op = plan3.Operations()[0];
auto* flat = std::get_if<FlatForwardOperation>(&op);
ASSERT_NE(flat, nullptr) << "Expected FlatForwardOperation";
ASSERT_EQ(flat->request_ids.size(), 1u);
EXPECT_EQ(flat->request_ids[0], "r2");
// With page-aligned input, except_last strips the last page so the
// mamba node is beyond the match point. mamba_cow_src is NOT set.
// This is the correct behavior for the current except_last semantics.
// A real mamba COW hit only occurs when the query depth reaches a node
// that has a mamba slot (e.g., non-aligned inputs or longer prefixes).
//
// For now verify the operation is well-formed and doesn't crash:
EXPECT_GE(flat->extend_prefix_lens[0], 0);
EXPECT_GE(flat->input_lengths[0], 0);
SendForwardDone("r2", {200});
PlanOnce();
SendFinish("r2");
PlanOnce();
}
TEST_F(MambaIntegrationTest, AbortFreesMambaSlots) {
Submit(MakeRequestSpec("r1", 2));
PlanOnce();
SendFinish("r1");
PlanOnce();
for (int i = 0; i < 8; ++i) {
Submit(MakeRequestSpec("fill_" + std::to_string(i), 1));
}
PlanOnce();
}
class MambaDecodeCapacityTest : public SchedulerTestSuite {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.enable_mamba = true;
cfg.mamba_pool_total_chunks = 2;
cfg.max_batch_size = 1;
return cfg;
}
};
TEST_F(MambaDecodeCapacityTest, PrefillDoneDecodeCapacityMissRetractsInsteadOfThrowing) {
Submit(MakeRequestSpec("r1", 1));
auto prefill = PlanOnce();
ASSERT_FALSE(prefill.Operations().empty());
SendForwardDone("r1", {100});
ExecutionPlan plan;
EXPECT_NO_THROW(plan = PlanOnce());
auto writebacks = ExtractCacheOpsOfKind<FlatWriteBackOperation>(plan);
ASSERT_EQ(writebacks.size(), 1u);
}
class MambaDecodeAdmissionTest : public SchedulerTestSuite {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.role = Role::kD;
cfg.enable_mamba = true;
cfg.mamba_pool_total_chunks = 24;
cfg.max_batch_size = 16;
cfg.max_scheduled_tokens = 1024;
return cfg;
}
static const FlatForwardOperation* GetForward(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* fwd = std::get_if<FlatForwardOperation>(&op)) return fwd;
}
return nullptr;
}
void SendBootstrapped(const std::string& request_id) {
ExecutionEvent event;
event.With(PDEvent{pd::BootstrappedEvent{request_id}});
scheduler_->Advance(std::move(event));
}
};
TEST_F(MambaDecodeAdmissionTest, SubmittedBatchStopsAtLocalMambaPairCapacity) {
std::vector<RequestSpec> requests;
for (int i = 0; i < 16; ++i) {
requests.push_back(MakeRequestSpec("r" + std::to_string(i), 1));
}
Submit(requests);
for (const auto& request : requests) {
SendBootstrapped(request.request_id);
}
auto plan = PlanOnce();
const auto* fwd = GetForward(plan);
ASSERT_NE(fwd, nullptr);
ASSERT_EQ(fwd->request_ids.size(), 12u);
for (std::size_t i = 0; i < fwd->request_ids.size(); ++i) {
EXPECT_GE(fwd->mamba_working_indices[i], 0) << fwd->request_ids[i];
EXPECT_GE(fwd->mamba_checkpoint_dst_indices[i], 0) << fwd->request_ids[i];
}
EXPECT_EQ(scheduler_->WaitingSize(), 4u);
EXPECT_EQ(scheduler_->PrefillSize(), 12u);
}
class MambaUnalignedCheckpointTest : public SchedulerTestSuite {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.enable_mamba = true;
cfg.mamba_pool_total_chunks = 16;
cfg.mamba_cache_chunk_size = 4;
cfg.max_scheduled_tokens = 3;
cfg.enable_l3_storage = false;
return cfg;
}
static const FlatForwardOperation* GetForward(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* fwd = std::get_if<FlatForwardOperation>(&op)) return fwd;
}
return nullptr;
}
};
TEST_F(MambaUnalignedCheckpointTest, ChunkBoundaryNotAlignedToMambaChunkDoesNotPublishCheckpoint) {
Submit(RequestSpec{.request_id = "r1", .tokens = {1, 2, 3, 4, 5}});
auto first_chunk = PlanOnce();
const auto* first_forward = GetForward(first_chunk);
ASSERT_NE(first_forward, nullptr);
ASSERT_EQ(first_forward->input_lengths[0], 3);
auto second_chunk = PlanOnce();
const auto* second_forward = GetForward(second_chunk);
ASSERT_NE(second_forward, nullptr);
SendFinish("r1");
PlanOnce();
Submit(RequestSpec{.request_id = "r2", .tokens = {1, 2, 9}});
auto prefix_probe = PlanOnce();
const auto* probe_forward = GetForward(prefix_probe);
ASSERT_NE(probe_forward, nullptr);
ASSERT_EQ(probe_forward->request_ids.size(), 1u);
EXPECT_EQ(probe_forward->request_ids[0], "r2");
EXPECT_EQ(probe_forward->extend_prefix_lens[0], 0);
EXPECT_EQ(probe_forward->mamba_cow_src_indices[0], -1)
<< "C++ must not publish a checkpoint that Python skipped at an unaligned boundary";
}
class MambaL2IntegrationTest : public SchedulerTestSuite {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.enable_mamba = true;
cfg.mamba_pool_total_chunks = 16;
cfg.enable_mamba_l2 = true;
cfg.mamba_l2_host_slots = 16;
cfg.host_allocator.total_pages = 32;
return cfg;
}
static const FlatWriteBackOperation* GetWriteBack(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* cop = std::get_if<CacheOperation>(&op)) {
if (auto* wb = std::get_if<FlatWriteBackOperation>(cop)) {
return wb;
}
}
}
return nullptr;
}
static const FlatLoadBackOperation* GetLoadBack(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* cop = std::get_if<CacheOperation>(&op)) {
if (auto* lb = std::get_if<FlatLoadBackOperation>(cop)) {
return lb;
}
}
}
return nullptr;
}
};
TEST_F(MambaL2IntegrationTest, FinishWriteBackCarriesMambaPair) {
Submit(MakeRequestSpec("r1", 2));
PlanOnce();
SendForwardDone("r1", {100});
PlanOnce();
SendFinish("r1");
auto plan = PlanOnce();
const auto* wb = GetWriteBack(plan);
ASSERT_NE(wb, nullptr);
ASSERT_FALSE(wb->op_ids.empty());
ASSERT_TRUE(wb->src_pages_by_kind.contains("mamba"));
bool has_mamba_pair = false;
for (const auto& pages : wb->src_pages_by_kind.at("mamba")) {
has_mamba_pair = has_mamba_pair || !pages.empty();
}
EXPECT_TRUE(has_mamba_pair);
}
TEST_F(MambaL2IntegrationTest, WriteBackDoneDemotesDeviceAndNextRequestLoadsBackMamba) {
Submit(MakeRequestSpec("r1", 2));
PlanOnce();
SendForwardDone("r1", {100});
PlanOnce();
SendFinish("r1");
auto writeback_plan = PlanOnce();
const auto* wb = GetWriteBack(writeback_plan);
ASSERT_NE(wb, nullptr);
ASSERT_FALSE(wb->op_ids.empty());
SendWriteBackDone(wb->op_ids[0]);
PlanOnce();
Submit(MakeRequestSpec("r2", 3));
auto loadback_plan = PlanOnce();
const auto* lb = GetLoadBack(loadback_plan);
ASSERT_NE(lb, nullptr) << "written-back Mamba+KV cache must be host-only and require loadback";
ASSERT_TRUE(lb->src_pages_by_kind.contains("kv"));
ASSERT_TRUE(lb->src_pages_by_kind.contains("mamba"));
EXPECT_FALSE(lb->src_pages_by_kind.at("mamba").empty());
}
TEST_F(MambaL2IntegrationTest, HostOnlyMambaLoadsBackAfterPinnedWriteBackReleases) {
Submit(RequestSpec{.request_id = "r1", .tokens = {1, 2, 3, 4}});
PlanOnce();
SendForwardDone("r1", {100});
PlanOnce();
SendFinish("r1");
auto writeback_plan = PlanOnce();
const auto* wb = GetWriteBack(writeback_plan);
ASSERT_NE(wb, nullptr);
ASSERT_FALSE(wb->op_ids.empty());
Submit(RequestSpec{.request_id = "child", .tokens = {1, 2, 3, 4, 5, 6}});
PlanOnce();
SendForwardDone("child", {200});
PlanOnce();
SendWriteBackDone(wb->op_ids[0]);
PlanOnce();
ExecutionEvent abort_child;
abort_child.With(ForwardEvent{forward::Abort{.request_id = "child"}});
scheduler_->Advance(std::move(abort_child));
PlanOnce();
Submit(RequestSpec{.request_id = "probe", .tokens = {1, 2, 3, 4, 9}});
auto loadback_plan = PlanOnce();
const auto* lb = GetLoadBack(loadback_plan);
ASSERT_NE(lb, nullptr) << "host-only Mamba must load back after the pinning request releases";
ASSERT_TRUE(lb->src_pages_by_kind.contains("mamba"));
EXPECT_FALSE(lb->src_pages_by_kind.at("mamba").empty());
}
class DisablePrefixCacheMambaRetractTest : public SchedulerTestSuite {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.disable_prefix_cache = true;
cfg.enable_mamba = true;
cfg.mamba_pool_total_chunks = 16;
cfg.decode_input_tokens = 0;
cfg.device_allocator.total_pages = 3;
cfg.host_allocator.total_pages = 16;
cfg.enable_l3_storage = false;
return cfg;
}
void SendReserveNumTokens(const std::string& id, std::int32_t n) {
ExecutionEvent event;
event.With(ForwardEvent{forward::UpdateReserveNumTokens{
.request_id = id,
.reserve_num_tokens_in_next_schedule_event = n,
}});
scheduler_->Advance(std::move(event));
}
static const FlatWriteBackOperation* GetWriteBack(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* cop = std::get_if<CacheOperation>(&op)) {
if (auto* wb = std::get_if<FlatWriteBackOperation>(cop)) {
return wb;
}
}
}
return nullptr;
}
static const FlatForwardOperation* GetForward(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* fwd = std::get_if<FlatForwardOperation>(&op)) {
return fwd;
}
}
return nullptr;
}
};
TEST_F(DisablePrefixCacheMambaRetractTest, RetractedRequestRecoversFromTreeOwnedMambaState) {
Submit(MakeRequestSpec("r1", 1));
PlanOnce();
SendForwardDone("r1", {100});
PlanOnce();
SendReserveNumTokens("r1", 3);
auto retract_plan = PlanOnce();
const auto* wb = GetWriteBack(retract_plan);
ASSERT_NE(wb, nullptr);
ASSERT_FALSE(wb->op_ids.empty());
SendWriteBackDone(wb->op_ids[0]);
ASSERT_EQ(scheduler_->RetractedSize(), 1u);
auto recover_plan = PlanOnce();
const auto* fwd = GetForward(recover_plan);
ASSERT_NE(fwd, nullptr);
ASSERT_EQ(fwd->request_ids.size(), 1u);
EXPECT_EQ(fwd->request_ids[0], "r1");
EXPECT_GE(fwd->mamba_cow_src_indices[0], 0);
EXPECT_GE(fwd->mamba_working_indices[0], 0);
EXPECT_GE(fwd->mamba_checkpoint_dst_indices[0], 0);
}
} // namespace tokenspeed::test