项目文件夹

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

133 行
6.8 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.
// Coverage: passive snapshot detach on KV LRU eviction returns pages via RAII.
#include "paged_cache_test_fixture.h"
namespace tokenspeed::test {
using PagedCacheEvictionTest = PagedCacheSmallFixture;
// Two branches, evict A with snapshot attached; pages must return via RAII.
//
// The contract under test is purely observable: after the snapshot-bearing KV
// node is evicted, every paged-cache page the snapshot was holding must be
// back in its group allocator's free list. We deliberately avoid hard-coding
// "how many pages a snapshot consumes" (that depends on commit-time semantics
// such as State-window trim) — instead, we capture the allocator state right
// before attach and require it to be restored after eviction.
TEST_F(PagedCacheEvictionTest, PassiveEvictionReleasesPagedCachePages) {
InsertDevicePages(/*num_pages=*/2, /*token_start=*/1); // branch A
auto* leaf_b = InsertDevicePages(/*num_pages=*/2, /*token_start=*/100); // branch B
ASSERT_NE(leaf_b, nullptr);
TreeNode* attach_a = kv_cache_->GetRadixTree().SplitAt(
kv_cache_->Match(MakeAlignedTokens(2, kPageSize, /*start=*/1)).device.last_node, kLcm);
ASSERT_NE(attach_a, nullptr);
// Baseline: paged-cache pools fully free, no snapshot attached.
const std::int32_t fh_before = fh_alloc_->AvailablePages();
const std::int32_t swa_before = swa_alloc_->AvailablePages();
hybrid_->AttachPagedCacheSnapshotToNode(attach_a, MakeCompleteSnapshot(kLcm));
EXPECT_TRUE(attach_a->HasPagedCacheSnapshot());
// The snapshot must hold *some* pages from each group, otherwise the test
// below ("eviction returns them") is vacuous. We do NOT assert the exact
// count — that depends on snapshot-build semantics (e.g. State-window
// trim) and is covered by dedicated build/commit tests.
EXPECT_LT(fh_alloc_->AvailablePages(), fh_before);
EXPECT_LT(swa_alloc_->AvailablePages(), swa_before);
// Pin branch B so eviction targets A. Without this lock the LRU policy
// could evict either branch.
auto match_b = kv_cache_->Match(MakeAlignedTokens(2, kPageSize, /*start=*/100));
DeviceNodeRef ref_b{match_b.device.last_node};
// Force eviction of branch A by demanding one more page than the device
// allocator currently has free. Branch B's 2 device pages are pinned by
// `ref_b`, so the LRU must drop branch A — which carries our snapshot.
const std::int32_t target_available = device_alloc_->AvailablePages() + 1;
const bool ok = kv_cache_->EnsureCapacityByEvict<ResourceType::Device>(target_available);
EXPECT_TRUE(ok);
// Note: after eviction `attach_a` may be freed by tree pruning, so we do
// not dereference it. The observable proof that OnKVEvict detached the
// snapshot is that the paged-cache allocator pools are restored below.
// Observable contract: every paged-cache page the snapshot held is now
// back in its allocator's free list (OwnedPages RAII via OnKVEvict ->
// DetachPagedCacheSnapshotFromNode).
EXPECT_EQ(fh_alloc_->AvailablePages(), fh_before);
EXPECT_EQ(swa_alloc_->AvailablePages(), swa_before);
}
TEST_F(PagedCacheEvictionTest, BorrowedSnapshotSurvivesDeviceEviction) {
InsertDevicePages(/*num_pages=*/2, /*token_start=*/1); // branch A
auto* leaf_b = InsertDevicePages(/*num_pages=*/2, /*token_start=*/100); // branch B
ASSERT_NE(leaf_b, nullptr);
TreeNode* attach_a = kv_cache_->GetRadixTree().SplitAt(
kv_cache_->Match(MakeAlignedTokens(2, kPageSize, /*start=*/1)).device.last_node, kLcm);
ASSERT_NE(attach_a, nullptr);
PageAllocator host_alloc{kPageSize, /*total_pages=*/8};
const auto host_pages = static_cast<std::int32_t>(attach_a->Tokens().size()) / kPageSize;
attach_a->AttachResource(std::make_unique<NodeResource<ResourceType::Host>>(host_alloc.Allocate(host_pages)));
auto host_ref = std::make_unique<HostNodeRef>(attach_a);
const std::int32_t fh_before = fh_alloc_->AvailablePages();
const std::int32_t swa_before = swa_alloc_->AvailablePages();
hybrid_->AttachPagedCacheSnapshotToNode(attach_a, MakeCompleteSnapshot(kLcm));
const std::int32_t fh_pinned = fh_alloc_->AvailablePages();
const std::int32_t swa_pinned = swa_alloc_->AvailablePages();
ASSERT_LT(fh_pinned, fh_before);
ASSERT_LT(swa_pinned, swa_before);
auto match_a = hybrid_->Match(MakeAlignedTokens(2, kPageSize, /*start=*/1));
ASSERT_EQ(match_a.paged_cache.prefix_len_tokens, kLcm);
hybrid_->AcquireForRequest("retracted", /*first_raw_position_of_op=*/0,
/*target_raw_tokens_exclusive=*/kLcm, match_a.paged_cache);
auto match_b = kv_cache_->Match(MakeAlignedTokens(2, kPageSize, /*start=*/100));
DeviceNodeRef ref_b{match_b.device.last_node};
const std::int32_t target_available = device_alloc_->AvailablePages() + 1;
ASSERT_TRUE(kv_cache_->EnsureCapacityByEvict<ResourceType::Device>(target_available));
// Device eviction must not free side-cache pages still borrowed by a
// retracted/loadback request's retained table.
EXPECT_FALSE(attach_a->OnDevice());
EXPECT_TRUE(attach_a->HasPagedCacheSnapshot());
EXPECT_EQ(fh_alloc_->AvailablePages(), fh_pinned);
EXPECT_EQ(swa_alloc_->AvailablePages(), swa_pinned);
EXPECT_THROW(hybrid_->OnNodeDestroyed(attach_a), std::runtime_error);
EXPECT_TRUE(attach_a->HasPagedCacheSnapshot());
// Once the request table drops the borrow, the normal eviction callback
// can reclaim the snapshot and return its pages.
hybrid_->ReleaseRequest("retracted");
hybrid_->OnKVEvict(attach_a);
EXPECT_FALSE(attach_a->HasPagedCacheSnapshot());
EXPECT_EQ(fh_alloc_->AvailablePages(), fh_before);
EXPECT_EQ(swa_alloc_->AvailablePages(), swa_before);
// Destroy the manually attached host resource before its local allocator.
host_ref.reset();
auto host_resource = attach_a->DetachResource<ResourceType::Host>();
ASSERT_NE(host_resource, nullptr);
}
} // namespace tokenspeed::test