项目文件夹

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

938 行
44 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.
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <tuple>
#include <unordered_map>
#include <variant>
#include <vector>
#include "integration_test_helper.h"
#include "paged_cache_test_fixture.h"
namespace tokenspeed::test {
namespace {
class PagedCacheTerminalSchedulerTest : public SchedulerTestSuite {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.block_size = 2;
cfg.device_allocator.total_pages = 64;
cfg.host_allocator.total_pages = 64;
cfg.max_scheduled_tokens = 64;
cfg.max_batch_size = 8;
cfg.enable_l3_storage = false;
PagedCacheGroupConfig fh{};
fh.group_id = "fh";
fh.rows_per_page = 4;
fh.entry_stride_tokens = 1;
fh.total_pages = 32;
fh.retention = PagedCacheGroupConfig::Retention::FullHistory;
fh.family = PagedCacheGroupFamily::History;
cfg.paged_cache_groups.push_back(fh);
PagedCacheGroupConfig swa{};
swa.group_id = "swa";
swa.rows_per_page = 2;
swa.entry_stride_tokens = 1;
swa.total_pages = 32;
swa.retention = PagedCacheGroupConfig::Retention::SlidingWindow;
swa.sliding_window_tokens = 8;
swa.family = PagedCacheGroupFamily::State;
cfg.paged_cache_groups.push_back(swa);
PrefixCacheAdjunctSpec spec{};
spec.required_groups = {"fh"};
cfg.prefix_cache_adjunct = spec;
return cfg;
}
static const FlatForwardOperation* GetForwardOp(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* f = std::get_if<FlatForwardOperation>(&op)) return f;
}
return nullptr;
}
};
class PagedCacheTerminalMixedSchedulerTest : public PagedCacheTerminalSchedulerTest {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = PagedCacheTerminalSchedulerTest::MakeConfig();
cfg.device_allocator.total_pages = 256;
cfg.max_scheduled_tokens = 128;
cfg.enable_mixed_prefill_decode = true;
for (auto& group : cfg.paged_cache_groups) {
group.total_pages = 256;
}
return cfg;
}
};
class PagedCacheDecodePublishTest : public SchedulerTestSuite {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.block_size = 1;
cfg.device_allocator.total_pages = 64;
cfg.host_allocator.total_pages = 64;
cfg.max_scheduled_tokens = 64;
cfg.max_batch_size = 8;
cfg.decode_input_tokens = 4;
cfg.enable_l3_storage = false;
PagedCacheGroupConfig history{};
history.group_id = "fh";
history.rows_per_page = 2;
history.entry_stride_tokens = 1;
history.total_pages = 64;
history.retention = PagedCacheGroupConfig::Retention::FullHistory;
history.family = PagedCacheGroupFamily::History;
cfg.paged_cache_groups.push_back(history);
PrefixCacheAdjunctSpec spec{};
spec.required_groups = {"fh"};
cfg.prefix_cache_adjunct = spec;
return cfg;
}
static const FlatForwardOperation* GetForwardOp(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* f = std::get_if<FlatForwardOperation>(&op)) return f;
}
return nullptr;
}
};
class PagedCacheOverlapSchedulerTest
: public SchedulerTestSuite,
public ::testing::WithParamInterface<std::tuple<std::int32_t, std::int32_t, std::int32_t>> {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.block_size = 64;
cfg.device_allocator.total_pages = 256;
cfg.host_allocator.total_pages = 256;
cfg.max_scheduled_tokens = 256;
cfg.max_batch_size = 4;
cfg.decode_input_tokens = std::get<0>(GetParam());
cfg.overlap_schedule_depth = std::get<2>(GetParam());
cfg.disable_l2_cache = true;
PagedCacheGroupConfig history{};
history.group_id = "overlap.history";
history.rows_per_page = 1;
history.entry_stride_tokens = 1;
history.total_pages = 256;
history.retention = PagedCacheGroupConfig::Retention::FullHistory;
history.family = PagedCacheGroupFamily::History;
cfg.paged_cache_groups.push_back(history);
PagedCacheGroupConfig state{};
state.group_id = "overlap.state";
state.rows_per_page = 1;
state.entry_stride_tokens = 1;
state.total_pages = 64;
state.retention = PagedCacheGroupConfig::Retention::SlidingWindow;
state.sliding_window_tokens = 8;
state.family = PagedCacheGroupFamily::State;
cfg.paged_cache_groups.push_back(state);
return cfg;
}
static const FlatForwardOperation* GetForwardOp(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* f = std::get_if<FlatForwardOperation>(&op)) return f;
}
return nullptr;
}
};
class PagedCacheOverlapRetractTest
: public SchedulerTestSuite,
public ::testing::WithParamInterface<std::tuple<std::int32_t, std::int32_t, bool>> {
protected:
SchedulerConfig MakeConfig() override {
auto cfg = SchedulerTestSuite::MakeConfig();
cfg.block_size = 2;
cfg.device_allocator.total_pages = 10;
cfg.host_allocator.total_pages = 64;
cfg.max_scheduled_tokens = 64;
cfg.max_batch_size = 4;
cfg.decode_input_tokens = 4;
cfg.overlap_schedule_depth = std::get<0>(GetParam());
cfg.enable_l3_storage = false;
PagedCacheGroupConfig history{};
history.group_id = "retract.history";
history.rows_per_page = 2;
history.entry_stride_tokens = 1;
history.total_pages = 128;
history.retention = PagedCacheGroupConfig::Retention::FullHistory;
history.family = PagedCacheGroupFamily::History;
cfg.paged_cache_groups.push_back(history);
PagedCacheGroupConfig state{};
state.group_id = "retract.state";
state.rows_per_page = 1;
state.entry_stride_tokens = 1;
state.total_pages = 64;
state.retention = PagedCacheGroupConfig::Retention::SlidingWindow;
state.sliding_window_tokens = 8;
state.family = PagedCacheGroupFamily::State;
cfg.paged_cache_groups.push_back(state);
PrefixCacheAdjunctSpec adjunct{};
adjunct.required_groups = {"retract.history"};
cfg.prefix_cache_adjunct = adjunct;
return cfg;
}
void SendReserveNumTokens(std::int32_t value) {
ExecutionEvent event;
event.With(ForwardEvent{forward::UpdateReserveNumTokens{
.request_id = "r",
.reserve_num_tokens_in_next_schedule_event = value,
}});
scheduler_->Advance(std::move(event));
}
static const FlatForwardOperation* GetForwardOp(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* f = std::get_if<FlatForwardOperation>(&op)) return f;
}
return nullptr;
}
static const FlatWriteBackOperation* GetWriteBack(const ExecutionPlan& plan) {
for (const auto& op : plan.Operations()) {
if (auto* cache_op = std::get_if<CacheOperation>(&op)) {
if (auto* writeback = std::get_if<FlatWriteBackOperation>(cache_op)) return writeback;
}
}
return nullptr;
}
};
class PagedCacheTerminalContinuationTest : public ::testing::Test {
protected:
static constexpr std::int32_t kPageSize = 64;
static constexpr std::int32_t kDevicePages = 64;
static constexpr std::int32_t kRequiredStateRows = 4;
static constexpr std::int32_t kRequiredStateWindow = 8;
static constexpr std::int32_t kWindowStateTokens = 128;
static constexpr const char* kHistoryGroup = "history";
static constexpr const char* kRequiredStateGroup = "required_state";
static constexpr const char* kWindowStateGroup = "window_state";
void SetUp() override {
device_alloc_ = std::make_unique<PageAllocator>(kPageSize, kDevicePages);
kv_cache_ = std::make_unique<KVPrefixCache>(device_alloc_.get(), /*host=*/nullptr);
auto history_owner = std::make_unique<PagedCacheGroupAllocator>(
MakeGroup(kHistoryGroup, /*rows_per_page=*/64, /*stride=*/1, PagedCacheGroupConfig::Retention::FullHistory,
std::nullopt, PagedCacheGroupFamily::History,
/*total_pages=*/64));
auto required_state_owner = std::make_unique<PagedCacheGroupAllocator>(MakeGroup(
kRequiredStateGroup, kRequiredStateRows, /*stride=*/1, PagedCacheGroupConfig::Retention::SlidingWindow,
kRequiredStateWindow, PagedCacheGroupFamily::State,
/*total_pages=*/128));
// Page 0 is reserved; the longest borrower/HostRef chain holds eight
// transport-only window pages concurrently.
auto window_owner = std::make_unique<PagedCacheGroupAllocator>(MakeGroup(
kWindowStateGroup, /*rows_per_page=*/64, /*stride=*/1, PagedCacheGroupConfig::Retention::SlidingWindow,
kWindowStateTokens, PagedCacheGroupFamily::State, /*total_pages=*/9));
hybrid_ = std::make_unique<HybridPrefixCache>(*kv_cache_, /*mamba=*/nullptr,
/*mamba_chunk_size=*/0);
hybrid_->RegisterPagedCacheGroup(std::move(history_owner));
hybrid_->RegisterPagedCacheGroup(std::move(required_state_owner));
hybrid_->RegisterPagedCacheGroup(std::move(window_owner));
hybrid_->EnablePagedCacheAdjunct({kHistoryGroup, kRequiredStateGroup},
{{kRequiredStateGroup, kRequiredStateWindow}});
kv_cache_->GetDeviceManager().SetEvictionCallback([this](TreeNode* node) { hybrid_->OnKVEvict(node); });
}
TreeNode* InsertDeviceTokens(std::int32_t raw_tokens, token_t token_start = 1) {
const std::int32_t num_pages = raw_tokens / kPageSize;
auto tokens = MakeAlignedTokens(num_pages, kPageSize, token_start);
OwnedPages pages = device_alloc_->Allocate(num_pages);
auto res = kv_cache_->Insert<ResourceType::Device>(tokens, /*prefix_pages=*/{}, std::move(pages),
/*page_hashes=*/{}, /*start_node=*/nullptr);
return res.last_node;
}
MatchResult MatchTokens(std::int32_t raw_tokens, token_t token_start = 1) {
return hybrid_->Match(MakeAlignedTokens(raw_tokens / kPageSize, kPageSize, token_start));
}
void CommitRequest(const std::string& request_id, std::int32_t first_token, std::int32_t target, TreeNode* terminal,
const MatchResult::PagedCache& hit = {}) {
hybrid_->AcquireForRequest(request_id, first_token, target, hit);
hybrid_->CommitChunk(request_id, terminal);
}
static PagedCacheGroupConfig MakeGroup(std::string group_id, std::int32_t rows_per_page, std::int32_t stride,
PagedCacheGroupConfig::Retention retention,
std::optional<std::int32_t> window, PagedCacheGroupFamily family,
std::int32_t total_pages) {
PagedCacheGroupConfig cfg{};
cfg.group_id = std::move(group_id);
cfg.rows_per_page = rows_per_page;
cfg.entry_stride_tokens = stride;
cfg.total_pages = total_pages;
cfg.retention = retention;
cfg.sliding_window_tokens = window;
cfg.family = family;
return cfg;
}
std::unique_ptr<PageAllocator> device_alloc_;
std::unique_ptr<KVPrefixCache> kv_cache_;
std::unique_ptr<HybridPrefixCache> hybrid_;
};
const PagedCacheGroupConfig& FindGroupConfig(const SchedulerConfig& cfg, const std::string& gid) {
for (const auto& group : cfg.paged_cache_groups) {
if (group.group_id == gid) return group;
}
throw std::logic_error("test group config missing: " + gid);
}
void ExpectPagedGroupCoversRange(const FlatForwardOperation& fwd, const SchedulerConfig& cfg, const std::string& gid,
std::size_t row, std::int32_t first_token, std::int32_t token_count) {
const auto& group_cfg = FindGroupConfig(cfg, gid);
const std::int32_t raw_per_page = group_cfg.RawTokensPerPage();
ASSERT_GT(raw_per_page, 0);
auto table_it = fwd.paged_cache_block_tables.find(gid);
ASSERT_NE(table_it, fwd.paged_cache_block_tables.end()) << "group=" << gid;
ASSERT_LT(row, table_it->second.size()) << "group=" << gid;
const auto& pages = table_it->second[row];
std::int32_t base_logical_page = 0;
auto base_map_it = fwd.paged_cache_block_table_base_offsets.find(gid);
if (base_map_it != fwd.paged_cache_block_table_base_offsets.end()) {
ASSERT_LT(row, base_map_it->second.size()) << "group=" << gid;
base_logical_page = base_map_it->second[row];
}
for (std::int32_t pos = first_token; pos < first_token + token_count; ++pos) {
const std::int32_t logical_page = pos / raw_per_page;
const std::int32_t table_page = logical_page - base_logical_page;
ASSERT_GE(table_page, 0) << "group=" << gid << " row=" << row << " pos=" << pos;
ASSERT_LT(table_page, static_cast<std::int32_t>(pages.size()))
<< "group=" << gid << " row=" << row << " pos=" << pos;
const std::int32_t physical_page = pages[static_cast<std::size_t>(table_page)];
EXPECT_GT(physical_page, 0) << "group=" << gid << " row=" << row << " pos=" << pos;
EXPECT_LT(physical_page, group_cfg.total_pages) << "group=" << gid << " row=" << row << " pos=" << pos;
}
}
} // namespace
TEST_F(PagedCacheTerminalContinuationTest, ExactTerminalHitUsesContinuationStateWithoutReplay) {
TreeNode* n256 = InsertDeviceTokens(256);
ASSERT_NE(n256, nullptr);
CommitRequest("r1", /*first_token=*/0, /*target=*/256, n256);
hybrid_->ReleaseRequest("r1");
ASSERT_TRUE(n256->HasPagedCacheSnapshot());
ASSERT_TRUE(n256->GetPagedCacheSnapshot()->continuation_state_complete);
auto first_match = MatchTokens(256);
EXPECT_EQ(first_match.paged_cache.history_hit_tokens, 256);
EXPECT_EQ(first_match.paged_cache.prefix_len_tokens, 256);
EXPECT_EQ(first_match.paged_cache.last_node, n256);
EXPECT_EQ(first_match.paged_cache.per_group_base_logical_page.at(kWindowStateGroup), 2);
EXPECT_EQ(first_match.paged_cache.per_group_page_ids.at(kWindowStateGroup).size(), 2u);
EXPECT_EQ(first_match.paged_cache.per_group_base_logical_page.at(kRequiredStateGroup), 62);
EXPECT_EQ(first_match.paged_cache.per_group_page_ids.at(kRequiredStateGroup).size(), 2u);
TreeNode* n320 = InsertDeviceTokens(320);
ASSERT_NE(n320, nullptr);
CommitRequest("r2", /*first_token=*/256, /*target=*/320, n320, first_match.paged_cache);
ASSERT_TRUE(n320->HasPagedCacheSnapshot());
ASSERT_TRUE(n320->GetPagedCacheSnapshot()->continuation_state_complete);
const auto& n320_window = n320->GetPagedCacheSnapshot()->groups.at(kWindowStateGroup);
EXPECT_EQ(n320_window.base_logical_page, 4);
EXPECT_EQ(n320_window.pages.Size(), 1);
auto second_match = MatchTokens(320);
EXPECT_EQ(second_match.paged_cache.history_hit_tokens, 320);
EXPECT_EQ(second_match.paged_cache.prefix_len_tokens, 320);
EXPECT_EQ(second_match.paged_cache.last_node, n320);
EXPECT_EQ(second_match.paged_cache.per_group_base_logical_page.at(kWindowStateGroup), 3);
EXPECT_EQ(second_match.paged_cache.per_group_page_ids.at(kWindowStateGroup).size(), 2u);
EXPECT_EQ(second_match.paged_cache.per_group_base_logical_page.at(kRequiredStateGroup), 78);
EXPECT_EQ(second_match.paged_cache.per_group_page_ids.at(kRequiredStateGroup).size(), 2u);
}
TEST_F(PagedCacheTerminalContinuationTest, SnapshotCursorStopsAtCheckpointBeforeReservedTail) {
constexpr std::int32_t kCheckpoint = 256;
constexpr std::int32_t kVerifyWidth = 4;
TreeNode* terminal = InsertDeviceTokens(kCheckpoint);
ASSERT_NE(terminal, nullptr);
hybrid_->AcquireForRequest("r", /*first_raw_position_of_op=*/0,
/*target_raw_tokens_exclusive=*/kCheckpoint + 2 * kVerifyWidth);
hybrid_->CommitChunk("r", terminal);
ASSERT_TRUE(terminal->HasPagedCacheSnapshot());
const auto* snapshot = terminal->GetPagedCacheSnapshot();
ASSERT_NE(snapshot, nullptr);
EXPECT_EQ(snapshot->prefix_len_tokens, kCheckpoint);
ASSERT_TRUE(snapshot->continuation_state_complete);
for (const auto& [group_id, group] : snapshot->groups) {
EXPECT_EQ(group.raw_token_cursor, kCheckpoint) << "group=" << group_id;
}
// The request still owns the physical lookahead page, but the snapshot
// advertises only the accepted checkpoint.
EXPECT_EQ(hybrid_->GetRequestPagedCachePageIds("r", kHistoryGroup).size(), 5u);
}
TEST_F(PagedCacheTerminalContinuationTest, RewindRejectsInvalidProtectedTail) {
EXPECT_THROW(hybrid_->RewindRequest("missing", /*accepted_raw_tokens=*/0,
/*protected_tail_tokens=*/-1),
std::invalid_argument);
EXPECT_THROW(hybrid_->RewindRequest("missing", std::numeric_limits<std::int32_t>::max(),
/*protected_tail_tokens=*/1),
std::overflow_error);
}
TEST_F(PagedCacheTerminalContinuationTest, TableBorrowPinsStateButHostRefDoesNot) {
TreeNode* n256 = InsertDeviceTokens(256);
ASSERT_NE(n256, nullptr);
CommitRequest("seed", /*first_token=*/0, /*target=*/256, n256);
hybrid_->ReleaseRequest("seed");
auto hit256 = MatchTokens(256);
ASSERT_EQ(hit256.paged_cache.prefix_len_tokens, 256);
// A retracted request can keep borrowing n256 after its DeviceNodeRef is
// gone. That table, not a coarse host-ref count, must pin the State pages.
hybrid_->AcquireForRequest("borrower", /*first_raw_position_of_op=*/0,
/*target_raw_tokens_exclusive=*/256, hit256.paged_cache);
TreeNode* n512 = InsertDeviceTokens(512);
ASSERT_NE(n512, nullptr);
{
DeviceNodeRef writer_ref{n512};
CommitRequest("writer", /*first_token=*/256, /*target=*/512, n512, hit256.paged_cache);
}
ASSERT_TRUE(n256->HasPagedCacheSnapshot());
EXPECT_TRUE(n256->GetPagedCacheSnapshot()->continuation_state_complete);
hybrid_->ReleaseRequest("borrower");
// Model the writer's own recovery HostNodeRef. The former HostRef-based
// heuristic treated this as another sharer and leaked old State snapshots.
PageAllocator host_alloc{kPageSize, /*total_pages=*/64};
std::vector<TreeNode*> attached_host_nodes;
for (TreeNode* cur = n256; cur != nullptr && !cur->IsRoot(); cur = cur->Parent()) {
if (cur->OnHost()) continue;
const auto host_pages = static_cast<std::int32_t>(cur->Tokens().size()) / kPageSize;
cur->AttachResource(std::make_unique<NodeResource<ResourceType::Host>>(host_alloc.Allocate(host_pages)));
attached_host_nodes.push_back(cur);
}
auto writer_host_ref = std::make_unique<HostNodeRef>(n256);
TreeNode* n768 = InsertDeviceTokens(768);
ASSERT_NE(n768, nullptr);
{
DeviceNodeRef writer_ref{n768};
CommitRequest("writer", /*first_token=*/512, /*target=*/768, n768);
}
ASSERT_TRUE(n256->HasPagedCacheSnapshot());
EXPECT_FALSE(n256->GetPagedCacheSnapshot()->continuation_state_complete);
EXPECT_EQ(n256->GetPagedCacheSnapshot()->groups.find(kRequiredStateGroup),
n256->GetPagedCacheSnapshot()->groups.end());
EXPECT_EQ(n256->GetPagedCacheSnapshot()->groups.find(kWindowStateGroup),
n256->GetPagedCacheSnapshot()->groups.end());
hybrid_->ReleaseRequest("writer");
writer_host_ref.reset();
for (TreeNode* node : attached_host_nodes) {
auto host_resource = node->DetachResource<ResourceType::Host>();
ASSERT_NE(host_resource, nullptr);
}
}
TEST_F(PagedCacheTerminalContinuationTest, CurrentBorrowPinsAncestorWhenTerminalSnapshotAlreadyExists) {
TreeNode* n64 = InsertDeviceTokens(64);
ASSERT_NE(n64, nullptr);
CommitRequest("seed", /*first_token=*/0, /*target=*/64, n64);
hybrid_->ReleaseRequest("seed");
auto hit64 = MatchTokens(64);
ASSERT_EQ(hit64.paged_cache.prefix_len_tokens, 64);
// Keep n64's transport-only State page borrowed by the request that will
// later adopt an already-existing terminal snapshot at n192.
hybrid_->AcquireForRequest("current", /*first_raw_position_of_op=*/64,
/*target_raw_tokens_exclusive=*/192, hit64.paged_cache);
TreeNode* n192 = InsertDeviceTokens(192);
ASSERT_NE(n192, nullptr);
{
DeviceNodeRef writer_ref{n192};
CommitRequest("writer", /*first_token=*/64, /*target=*/192, n192, hit64.paged_cache);
}
hybrid_->ReleaseRequest("writer");
ASSERT_TRUE(n64->HasPagedCacheSnapshot());
ASSERT_TRUE(n192->HasPagedCacheSnapshot());
ASSERT_TRUE(n192->GetPagedCacheSnapshot()->continuation_state_complete);
const auto& ancestor_state_pages = n64->GetPagedCacheSnapshot()->groups.at(kWindowStateGroup).pages.Ids();
const auto& current_state_pages = hybrid_->GetRequestPagedCachePageIds("current", kWindowStateGroup);
ASSERT_TRUE(std::any_of(ancestor_state_pages.begin(), ancestor_state_pages.end(), [&](std::int32_t page_id) {
return std::find(current_state_pages.begin(), current_state_pages.end(), page_id) != current_state_pages.end();
}));
// Required groups adopt n192's existing snapshot, while the
// transport-only group is already present and therefore is not adopted.
// The current request still borrows n64, so ancestor cleanup must retain
// that State snapshot.
{
DeviceNodeRef current_ref{n192};
hybrid_->CommitChunk("current", n192);
}
ASSERT_TRUE(n64->HasPagedCacheSnapshot());
EXPECT_TRUE(n64->GetPagedCacheSnapshot()->continuation_state_complete);
EXPECT_NE(n64->GetPagedCacheSnapshot()->groups.find(kWindowStateGroup), n64->GetPagedCacheSnapshot()->groups.end());
hybrid_->ReleaseRequest("current");
}
TEST_F(PagedCacheTerminalContinuationTest, StatePruneDropsContinuationAndFallsBackToColdPrefill) {
TreeNode* n256 = InsertDeviceTokens(256);
ASSERT_NE(n256, nullptr);
CommitRequest("r1", /*first_token=*/0, /*target=*/256, n256);
hybrid_->ReleaseRequest("r1");
ASSERT_TRUE(n256->HasPagedCacheSnapshot());
ASSERT_TRUE(n256->GetPagedCacheSnapshot()->continuation_state_complete);
auto simulated_free = hybrid_->InitialSimulatedFree();
simulated_free[kWindowStateGroup] = 0;
ASSERT_TRUE(hybrid_->AdmitChunk("pressure", /*first_raw_position_of_op=*/0,
/*target_raw_tokens_exclusive=*/64, simulated_free));
ASSERT_TRUE(n256->HasPagedCacheSnapshot());
ASSERT_FALSE(n256->GetPagedCacheSnapshot()->continuation_state_complete);
EXPECT_EQ(n256->GetPagedCacheSnapshot()->groups.find(kWindowStateGroup),
n256->GetPagedCacheSnapshot()->groups.end());
auto match = MatchTokens(256);
EXPECT_EQ(match.paged_cache.history_hit_tokens, 0);
EXPECT_EQ(match.paged_cache.prefix_len_tokens, 0);
EXPECT_TRUE(match.paged_cache.per_group_page_ids.empty());
}
TEST_F(PagedCacheTerminalContinuationTest, StateOnlyPruneIgnoresHistoryOnlySideTableBorrow) {
TreeNode* n64 = InsertDeviceTokens(64);
ASSERT_NE(n64, nullptr);
CommitRequest("seed", /*first_token=*/0, /*target=*/64, n64);
hybrid_->ReleaseRequest("seed");
auto hit = MatchTokens(64);
ASSERT_EQ(hit.paged_cache.prefix_len_tokens, 64);
hybrid_->AcquireForRequest("borrower", /*first_raw_position_of_op=*/192,
/*target_raw_tokens_exclusive=*/192, hit.paged_cache);
auto shares_snapshot_pages = [&](const std::string& group_id) {
const auto& snapshot_pages = n64->GetPagedCacheSnapshot()->groups.at(group_id).pages.Ids();
const auto& request_pages = hybrid_->GetRequestPagedCachePageIds("borrower", group_id);
return std::any_of(snapshot_pages.begin(), snapshot_pages.end(), [&](std::int32_t page_id) {
return std::find(request_pages.begin(), request_pages.end(), page_id) != request_pages.end();
});
};
ASSERT_TRUE(n64->HasPagedCacheSnapshot());
EXPECT_TRUE(shares_snapshot_pages(kHistoryGroup));
EXPECT_FALSE(shares_snapshot_pages(kRequiredStateGroup));
EXPECT_FALSE(shares_snapshot_pages(kWindowStateGroup));
const auto history_pages_before = hybrid_->GetRequestPagedCachePageIds("borrower", kHistoryGroup);
auto simulated_free = hybrid_->InitialSimulatedFree();
simulated_free[kWindowStateGroup] = 0;
ASSERT_TRUE(hybrid_->AdmitChunk("pressure", /*first_raw_position_of_op=*/0,
/*target_raw_tokens_exclusive=*/64, simulated_free));
ASSERT_TRUE(n64->HasPagedCacheSnapshot());
const auto* snapshot = n64->GetPagedCacheSnapshot();
ASSERT_NE(snapshot, nullptr);
EXPECT_NE(snapshot->groups.find(kHistoryGroup), snapshot->groups.end());
EXPECT_EQ(snapshot->groups.find(kRequiredStateGroup), snapshot->groups.end());
EXPECT_EQ(snapshot->groups.find(kWindowStateGroup), snapshot->groups.end());
EXPECT_FALSE(snapshot->continuation_state_complete);
EXPECT_EQ(hybrid_->GetRequestPagedCachePageIds("borrower", kHistoryGroup), history_pages_before);
hybrid_->ReleaseRequest("borrower");
}
TEST(PagedCacheHistoryOnlyTest, HistoryOnlyPrefixHitRemainsUsable) {
auto device_alloc = std::make_unique<PageAllocator>(64, 64);
auto kv_cache = std::make_unique<KVPrefixCache>(device_alloc.get(), /*host_allocator=*/nullptr);
PagedCacheGroupConfig history{};
history.group_id = "fh";
history.rows_per_page = 64;
history.entry_stride_tokens = 1;
history.total_pages = 16;
history.retention = PagedCacheGroupConfig::Retention::FullHistory;
history.family = PagedCacheGroupFamily::History;
auto history_owner = std::make_unique<PagedCacheGroupAllocator>(history);
HybridPrefixCache hybrid(*kv_cache, /*mamba=*/nullptr, /*mamba_chunk_size=*/0);
hybrid.RegisterPagedCacheGroup(std::move(history_owner));
hybrid.EnablePagedCacheAdjunct({"fh"}, {});
auto tokens = MakeAlignedTokens(/*num_pages=*/4, /*page_size=*/64, /*start=*/1);
OwnedPages pages = device_alloc->Allocate(4);
auto inserted = kv_cache->Insert<ResourceType::Device>(tokens, /*prefix_pages=*/{}, std::move(pages),
/*page_hashes=*/{}, /*start_node=*/nullptr);
ASSERT_NE(inserted.last_node, nullptr);
hybrid.AcquireForRequest("r1", /*first_raw_position_of_op=*/0, /*target_raw_tokens_exclusive=*/256);
hybrid.CommitChunk("r1", inserted.last_node);
hybrid.ReleaseRequest("r1");
auto match = hybrid.Match(MakeAlignedTokens(/*num_pages=*/4, /*page_size=*/64, /*start=*/1));
EXPECT_EQ(match.paged_cache.history_hit_tokens, 256);
EXPECT_EQ(match.paged_cache.prefix_len_tokens, 256);
ASSERT_NE(match.paged_cache.last_node, nullptr);
ASSERT_EQ(match.paged_cache.per_group_page_ids.at("fh").size(), 4u);
}
TEST(PagedCacheAdmissionTest, ExistingTransportStateGroupUsesSlidingWindowCredit) {
auto device_alloc = std::make_unique<PageAllocator>(64, 128);
auto kv_cache = std::make_unique<KVPrefixCache>(device_alloc.get(), /*host_allocator=*/nullptr);
PagedCacheGroupConfig history{};
history.group_id = "fh";
history.rows_per_page = 8;
history.entry_stride_tokens = 4;
history.total_pages = 32;
history.retention = PagedCacheGroupConfig::Retention::FullHistory;
history.family = PagedCacheGroupFamily::History;
PagedCacheGroupConfig swa{};
swa.group_id = "swa";
swa.rows_per_page = 4;
swa.entry_stride_tokens = 1;
swa.total_pages = 10;
swa.retention = PagedCacheGroupConfig::Retention::SlidingWindow;
swa.sliding_window_tokens = 16;
swa.family = PagedCacheGroupFamily::State;
auto history_owner = std::make_unique<PagedCacheGroupAllocator>(history);
auto swa_owner = std::make_unique<PagedCacheGroupAllocator>(swa);
HybridPrefixCache hybrid(*kv_cache, /*mamba=*/nullptr, /*mamba_chunk_size=*/0);
hybrid.RegisterPagedCacheGroup(std::move(history_owner));
hybrid.RegisterPagedCacheGroup(std::move(swa_owner));
hybrid.EnablePagedCacheAdjunct({"fh"}, {});
hybrid.AcquireForRequest("r", /*first_raw_position_of_op=*/0, /*target_raw_tokens_exclusive=*/32);
auto simulated_free = hybrid.InitialSimulatedFree();
ASSERT_EQ(simulated_free.at("swa"), 1);
EXPECT_FALSE(hybrid.AdmitChunk("r", /*first_raw_position_of_op=*/32,
/*target_raw_tokens_exclusive=*/64, simulated_free));
EXPECT_THROW(hybrid.AcquireForRequest("r", /*first_raw_position_of_op=*/32,
/*target_raw_tokens_exclusive=*/64),
std::runtime_error);
}
TEST(PagedCacheRewindTest, RewindRequestReleasesRejectedTailAndKeepsCommittedPrefix) {
PageAllocator device_alloc(/*page_size=*/2, /*total_pages=*/16);
KVPrefixCache kv_cache(&device_alloc, /*host=*/nullptr);
PagedCacheGroupConfig history{};
history.group_id = "fh";
history.rows_per_page = 2;
history.entry_stride_tokens = 1;
history.total_pages = 8;
history.retention = PagedCacheGroupConfig::Retention::FullHistory;
history.family = PagedCacheGroupFamily::History;
auto history_owner = std::make_unique<PagedCacheGroupAllocator>(history);
HybridPrefixCache hybrid(kv_cache, /*mamba=*/nullptr, /*mamba_chunk_size=*/0);
hybrid.RegisterPagedCacheGroup(std::move(history_owner));
hybrid.EnablePagedCacheAdjunct({"fh"}, {});
ASSERT_EQ(hybrid.PagedCacheGroupAvailablePages("fh"), 7);
hybrid.AcquireForRequest("r", /*first_raw_position_of_op=*/0, /*target_raw_tokens_exclusive=*/8);
ASSERT_EQ(hybrid.GetRequestPagedCachePageIds("r", "fh").size(), 4u);
ASSERT_EQ(hybrid.PagedCacheGroupAvailablePages("fh"), 3);
auto tokens = MakeAlignedTokens(/*num_pages=*/2, /*page_size=*/2, /*start=*/1);
OwnedPages pages = device_alloc.Allocate(/*num_pages=*/2);
auto inserted =
kv_cache.Insert<ResourceType::Device>(tokens, /*prefix_pages=*/{}, std::move(pages), /*page_hashes=*/{});
ASSERT_NE(inserted.last_node, nullptr);
hybrid.CommitChunk("r", inserted.last_node);
ASSERT_TRUE(inserted.last_node->HasPagedCacheSnapshot());
hybrid.RewindRequest("r", /*accepted_raw_tokens=*/5);
EXPECT_EQ(hybrid.GetRequestPagedCachePageIds("r", "fh").size(), 3u);
EXPECT_EQ(hybrid.PagedCacheGroupAvailablePages("fh"), 4);
auto match = hybrid.Match(MakeAlignedTokens(/*num_pages=*/2, /*page_size=*/2, /*start=*/1));
EXPECT_EQ(match.paged_cache.history_hit_tokens, 4);
}
TEST_F(PagedCacheDecodePublishTest, ContinuingDecodePublishesAcceptedPagesOnly) {
Submit(RequestSpec{.request_id = "r1", .tokens = {1, 2}});
ASSERT_NE(GetForwardOp(PlanOnce()), nullptr);
SendForwardDone("r1", {3});
ASSERT_NE(GetForwardOp(PlanOnce()), nullptr);
// Accepted 2 tokens from a 4-token speculative reserve. The accepted truth is
// {1,2,3,4,5}; with except-last KV semantics, only prefix {1,2,3,4}
// can be published. Reserved/draft tail slots beyond that must stay local.
SendForwardDone("r1", {4, 5});
EXPECT_EQ(scheduler_->GetRequestPagedCachePageIds("r1", "fh").size(), 3u);
Submit({
RequestSpec{.request_id = "hit4", .tokens = {1, 2, 3, 4, 5}},
RequestSpec{.request_id = "probe_tail", .tokens = {1, 2, 3, 4, 5, 6}},
});
auto plan = PlanOnce();
auto* fwd = GetForwardOp(plan);
ASSERT_NE(fwd, nullptr);
ASSERT_GE(fwd->extend_prefix_lens.size(), 2u);
std::unordered_map<std::string, std::int32_t> prefix_by_request;
for (std::size_t row = 0; row < fwd->extend_prefix_lens.size(); ++row) {
ASSERT_LT(row, fwd->request_ids.size());
prefix_by_request.emplace(fwd->request_ids[row], fwd->extend_prefix_lens[row]);
}
ASSERT_TRUE(prefix_by_request.contains("hit4"));
ASSERT_TRUE(prefix_by_request.contains("probe_tail"));
EXPECT_EQ(prefix_by_request.at("hit4"), 4);
EXPECT_EQ(prefix_by_request.at("probe_tail"), 4);
}
TEST_P(PagedCacheOverlapSchedulerTest, DynamicVerifyWidthRetainsAlreadyDispatchedRange) {
const auto [verify_width, accepted_length, overlap_depth] = GetParam();
Submit(RequestSpec{.request_id = "r", .tokens = {1, 2}});
ASSERT_NE(GetForwardOp(PlanOnce()), nullptr);
// Commit the prefill result so the request can enter decode at C=3.
SendForwardDone("r", {3});
const std::int32_t committed_before = scheduler_->GetRequestTokenSize("r");
ASSERT_EQ(committed_before, 3);
auto current_plan = PlanOnce();
auto* current = GetForwardOp(current_plan);
ASSERT_NE(current, nullptr);
ASSERT_EQ(current->request_ids.size(), 1u);
EXPECT_EQ(current->input_lengths.at(0), verify_width);
ExpectPagedGroupCoversRange(*current, Config(), "overlap.history", /*row=*/0, committed_before,
/*token_count=*/(overlap_depth + 1) * verify_width);
ExpectPagedGroupCoversRange(*current, Config(), "overlap.state", /*row=*/0, committed_before,
/*token_count=*/(overlap_depth + 1) * verify_width);
// The overlapped event loop dispatches the next plan before committing
// this result. Its CPU token length is still C, while the GPU will use the
// accepted length A after valid_cache_lengths is updated.
if (overlap_depth == 1) {
auto stale_plan = PlanOnce();
auto* stale = GetForwardOp(stale_plan);
ASSERT_NE(stale, nullptr);
ExpectPagedGroupCoversRange(*stale, Config(), "overlap.history", /*row=*/0, committed_before,
/*token_count=*/2 * verify_width);
}
const auto history_pages_before = scheduler_->GetRequestPagedCachePageIds("r", "overlap.history");
const auto state_pages_before = scheduler_->GetRequestPagedCachePageIds("r", "overlap.state");
const auto reserved_end = committed_before + (overlap_depth + 1) * verify_width;
ASSERT_EQ(history_pages_before.size(), static_cast<std::size_t>(reserved_end));
ASSERT_EQ(state_pages_before.size(), static_cast<std::size_t>(reserved_end));
std::vector<std::int32_t> accepted(static_cast<std::size_t>(accepted_length), 1000 + accepted_length);
SendForwardDone("r", accepted);
const std::int32_t accepted_end = committed_before + accepted_length;
ASSERT_EQ(scheduler_->GetRequestTokenSize("r"), accepted_end);
const auto history_pages_after = scheduler_->GetRequestPagedCachePageIds("r", "overlap.history");
const auto state_pages_after = scheduler_->GetRequestPagedCachePageIds("r", "overlap.state");
const auto protected_end = accepted_end + overlap_depth * verify_width;
ASSERT_EQ(history_pages_after.size(), static_cast<std::size_t>(protected_end));
ASSERT_EQ(state_pages_after.size(), static_cast<std::size_t>(protected_end));
for (std::int32_t pos = accepted_end; pos < accepted_end + overlap_depth * verify_width; ++pos) {
ASSERT_LT(static_cast<std::size_t>(pos), history_pages_before.size());
ASSERT_LT(static_cast<std::size_t>(pos), history_pages_after.size());
EXPECT_EQ(history_pages_after[static_cast<std::size_t>(pos)],
history_pages_before[static_cast<std::size_t>(pos)])
<< "history pos=" << pos;
ASSERT_LT(static_cast<std::size_t>(pos), state_pages_before.size());
ASSERT_LT(static_cast<std::size_t>(pos), state_pages_after.size());
EXPECT_EQ(state_pages_after[static_cast<std::size_t>(pos)], state_pages_before[static_cast<std::size_t>(pos)])
<< "pos=" << pos;
}
auto next_plan = PlanOnce();
auto* next = GetForwardOp(next_plan);
ASSERT_NE(next, nullptr);
ExpectPagedGroupCoversRange(*next, Config(), "overlap.history", /*row=*/0, accepted_end,
/*token_count=*/(overlap_depth + 1) * verify_width);
ExpectPagedGroupCoversRange(*next, Config(), "overlap.state", /*row=*/0, accepted_end,
/*token_count=*/(overlap_depth + 1) * verify_width);
}
TEST_P(PagedCacheOverlapRetractTest, LateResultRecoveryRebuildsDynamicHorizon) {
const auto [overlap_depth, accepted_length, writeback_first] = GetParam();
Submit(RequestSpec{.request_id = "r", .tokens = {1, 2}});
ASSERT_NE(GetForwardOp(PlanOnce()), nullptr);
SendForwardDone("r", {3});
// Dispatch a decode at C=3, then let the next scheduler pass retract it
// before this in-flight result is committed.
ASSERT_NE(GetForwardOp(PlanOnce()), nullptr);
const auto history_pages_before = scheduler_->GetRequestPagedCachePageIds("r", "retract.history");
const auto state_pages_before = scheduler_->GetRequestPagedCachePageIds("r", "retract.state");
SendReserveNumTokens(/*value=*/100);
auto retract_plan = PlanOnce();
const auto* writeback = GetWriteBack(retract_plan);
ASSERT_NE(writeback, nullptr);
ASSERT_FALSE(writeback->op_ids.empty());
if (writeback_first) {
SendWriteBackDone(writeback->op_ids.front());
ASSERT_EQ(scheduler_->RetractedSize(), 1u);
}
std::vector<std::int32_t> accepted;
for (std::int32_t i = 0; i < accepted_length; ++i) {
accepted.push_back(4 + i);
}
SendForwardDone("r", accepted);
ASSERT_EQ(scheduler_->GetRequestTokenSize("r"), 3 + accepted_length);
const auto retained_end = 3 + accepted_length + overlap_depth * 4;
const auto history_pages_after_late_result = scheduler_->GetRequestPagedCachePageIds("r", "retract.history");
const auto state_pages_after_late_result = scheduler_->GetRequestPagedCachePageIds("r", "retract.state");
ASSERT_EQ(history_pages_after_late_result.size(), static_cast<std::size_t>((retained_end + 1) / 2));
ASSERT_EQ(state_pages_after_late_result.size(), static_cast<std::size_t>(retained_end));
EXPECT_TRUE(std::equal(history_pages_after_late_result.begin(), history_pages_after_late_result.end(),
history_pages_before.begin()));
EXPECT_TRUE(std::equal(state_pages_after_late_result.begin(), state_pages_after_late_result.end(),
state_pages_before.begin()));
if (!writeback_first) {
SendWriteBackDone(writeback->op_ids.front());
}
ASSERT_EQ(scheduler_->RetractedSize(), 1u);
auto recovery_plan = PlanOnce();
const auto* recovery = GetForwardOp(recovery_plan);
ASSERT_NE(recovery, nullptr);
ASSERT_EQ(recovery->request_ids.size(), 1u);
ASSERT_EQ(recovery->request_ids.front(), "r");
ASSERT_EQ(recovery->hist_token_lens.size(), 1u);
EXPECT_EQ(recovery->hist_token_lens.front(), 2 + accepted_length);
EXPECT_EQ(recovery->input_lengths.front(), 4);
ExpectPagedGroupCoversRange(*recovery, Config(), "retract.history", /*row=*/0,
/*first_token=*/2 + accepted_length, /*token_count=*/(overlap_depth + 1) * 4);
ExpectPagedGroupCoversRange(*recovery, Config(), "retract.state", /*row=*/0,
/*first_token=*/2 + accepted_length, /*token_count=*/(overlap_depth + 1) * 4);
const auto history_pages_after = scheduler_->GetRequestPagedCachePageIds("r", "retract.history");
const auto state_pages_after = scheduler_->GetRequestPagedCachePageIds("r", "retract.state");
ASSERT_GE(history_pages_after.size(), history_pages_after_late_result.size());
ASSERT_GE(state_pages_after.size(), state_pages_after_late_result.size());
EXPECT_TRUE(std::equal(history_pages_after_late_result.begin(), history_pages_after_late_result.end(),
history_pages_after.begin()));
EXPECT_TRUE(std::equal(state_pages_after_late_result.begin(), state_pages_after_late_result.end(),
state_pages_after.begin()));
EXPECT_EQ(scheduler_->DecodingSize(), 1u);
EXPECT_EQ(scheduler_->RetractedSize(), 0u);
}
INSTANTIATE_TEST_SUITE_P(OverlapDepthsAndAcceptLengths, PagedCacheOverlapRetractTest,
::testing::Values(std::make_tuple(0, 0, false), std::make_tuple(0, 2, false),
std::make_tuple(0, 4, false), std::make_tuple(1, 0, false),
std::make_tuple(1, 2, false), std::make_tuple(1, 4, false),
std::make_tuple(1, 0, true)));
INSTANTIATE_TEST_SUITE_P(VerifyWidthsAndAcceptLengths, PagedCacheOverlapSchedulerTest,
::testing::Values(std::make_tuple(1, 1, 0), std::make_tuple(2, 1, 0), std::make_tuple(4, 1, 0),
std::make_tuple(8, 1, 0), std::make_tuple(1, 1, 1), std::make_tuple(2, 1, 1),
std::make_tuple(2, 2, 1), std::make_tuple(4, 0, 1), std::make_tuple(4, 2, 1),
std::make_tuple(4, 4, 1), std::make_tuple(8, 0, 1), std::make_tuple(8, 4, 1),
std::make_tuple(8, 7, 1), std::make_tuple(8, 8, 1)));
TEST_F(PagedCacheTerminalMixedSchedulerTest, MixedPrefillDecodePagedTablesCoverScheduledTokens) {
std::vector<std::string> decode_ids;
for (int i = 0; i < 5; ++i) {
decode_ids.push_back("decode_" + std::to_string(i));
Submit(MakeRequestSpec(decode_ids.back(), /*num_pages=*/4, static_cast<token_t>(1000 + i * 100)));
}
PlanOnce();
for (const auto& id : decode_ids) {
SendForwardDone(id, {900});
}
PlanOnce();
for (const auto& id : decode_ids) {
SendForwardDone(id, {901});
}
std::unordered_map<std::string, std::int32_t> decode_first_pos;
for (const auto& id : decode_ids) {
decode_first_pos.emplace(id, scheduler_->GetRequestTokenSize(id));
}
Submit({
MakeRequestSpec("prefill_0", /*num_pages=*/16, /*start=*/1),
MakeRequestSpec("prefill_1", /*num_pages=*/16, /*start=*/100),
MakeRequestSpec("prefill_2", /*num_pages=*/16, /*start=*/200),
});
auto plan = PlanOnce();
auto* fwd = GetForwardOp(plan);
ASSERT_NE(fwd, nullptr);
ASSERT_EQ(fwd->request_ids.size(), 8u);
ASSERT_EQ(fwd->extend_prefix_lens.size(), 3u);
for (std::size_t row = 0; row < fwd->request_ids.size(); ++row) {
std::int32_t first_token = 0;
if (row < fwd->extend_prefix_lens.size()) {
first_token = fwd->extend_prefix_lens[row];
} else {
auto it = decode_first_pos.find(fwd->request_ids[row]);
ASSERT_NE(it, decode_first_pos.end()) << "request_id=" << fwd->request_ids[row];
first_token = it->second;
}
ASSERT_LT(row, fwd->input_lengths.size());
ExpectPagedGroupCoversRange(*fwd, Config(), "fh", row, first_token, fwd->input_lengths[row]);
ExpectPagedGroupCoversRange(*fwd, Config(), "swa", row, first_token, fwd->input_lengths[row]);
}
}
} // namespace tokenspeed::test