Add a periodic full-radix-tree KV-canary sweep (#26812)

This commit is contained in:
fzyzcjy
2026-05-31 09:56:42 +08:00
committed by GitHub
parent 27eb139ef7
commit 30a22cc360
22 changed files with 797 additions and 35 deletions
@@ -11,7 +11,7 @@ register_cuda_ci(est_time=60, stage="extra-a", runner_config="1-gpu-small")
class _BaselineBase(CanaryE2EBase):
"""No perturb, kv-canary=log. Server should run clean with no canary
"""No perturb, kv-canary=log, sweep off. Server should run clean with no canary
violations and every request must come back 200."""
kv_canary_mode = CanaryMode.LOG
@@ -63,6 +63,28 @@ class TestSelfUnitEndpoint(CustomTestCase):
def setUp(self):
self.device = DEFAULT_DEVICE
def test_launch_sweep_only_calls_verify(self):
"""Verify sweep launch invokes only the verify kernel."""
calls: list[str] = []
with patch.object(
endpoint_module,
"launch_canary_verify_kernel",
lambda **kwargs: calls.append("verify"),
), patch.object(
endpoint_module,
"launch_canary_write_kernel",
lambda **kwargs: calls.append("write"),
):
ep = _make_endpoint(
device=self.device, kernel_kind=CanaryLaunchTag.SWEEP_K_FULL
)
args = _make_kernel_args(self.device)
ep.launch_sweep(
verify_plan=args.verify_plan,
violation_log=args.violation_log,
)
self.assertEqual(calls, ["verify"])
def test_launch_per_forward_passes_kernel_kind(self):
"""Verify per-forward launch passes the endpoint kernel kind."""
captured: list[tuple[str, CanaryLaunchTag]] = []
@@ -109,33 +131,19 @@ class TestSelfUnitEndpoint(CustomTestCase):
):
shared_log = ViolationLog.allocate(ring_capacity=2, device=self.device)
ep_a = _make_endpoint(
device=self.device, kernel_kind=CanaryLaunchTag.HEAD_K_FULL
device=self.device, kernel_kind=CanaryLaunchTag.SWEEP_K_FULL
)
ep_b = _make_endpoint(
device=self.device, kernel_kind=CanaryLaunchTag.HEAD_V_FULL
device=self.device, kernel_kind=CanaryLaunchTag.SWEEP_V_FULL
)
args = _make_kernel_args(self.device)
ep_a.launch_per_forward(
verify_plan=args.verify_plan,
write_plan=args.write_plan,
input_ids=args.input_ids,
positions=args.positions,
out_cache_loc=args.out_cache_loc,
enable_write_input_assert=args.enable_write_input_assert,
enable_verify_token_assert=args.enable_verify_token_assert,
expected_inputs=args.expected_inputs,
plan = VerifyPlan.allocate(verify_capacity=1, device=self.device)
ep_a.launch_sweep(
verify_plan=plan,
violation_log=shared_log,
)
ep_b.launch_per_forward(
verify_plan=args.verify_plan,
write_plan=args.write_plan,
input_ids=args.input_ids,
positions=args.positions,
out_cache_loc=args.out_cache_loc,
enable_write_input_assert=args.enable_write_input_assert,
enable_verify_token_assert=args.enable_verify_token_assert,
expected_inputs=args.expected_inputs,
ep_b.launch_sweep(
verify_plan=plan,
violation_log=shared_log,
)
self.assertEqual(captured_rings[0], captured_rings[1])
@@ -0,0 +1,128 @@
from __future__ import annotations
import unittest
import torch
from sglang.srt.kv_canary.radix_cache_walker import walk_radix_cache_for_canary
from sglang.srt.mem_cache.swa_radix_cache import SWARadixCache, TreeNode
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kv_canary.fixtures import DEFAULT_DEVICE, make_radix_cache
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=30, stage="extra-a", runner_config="1-gpu-small")
class TestSelfUnitRadixWalker(CustomTestCase):
def setUp(self):
self.device = DEFAULT_DEVICE
def test_single_node_chain_positions_increase(self):
"""Verify a single radix chain emits increasing positions."""
chain = [10, 20, 30, 40]
cache = make_radix_cache([[], chain], device=self.device)
result = walk_radix_cache_for_canary(radix_cache=cache)
self.assertEqual(result.slot_indices.tolist(), chain)
self.assertEqual(result.positions.tolist(), [0, 1, 2, 3])
self.assertEqual(result.prev_slot_indices.tolist(), [-1, 10, 20, 30])
def test_child_node_first_slot_prev_is_parent_last(self):
"""Verify child chains link their first slot to the parent tail."""
parent = [7, 8]
child = [9, 10]
cache = make_radix_cache([[], parent, child], device=self.device)
result = walk_radix_cache_for_canary(radix_cache=cache)
self.assertEqual(result.slot_indices.tolist(), parent + child)
self.assertEqual(result.prev_slot_indices.tolist()[len(parent)], parent[-1])
def test_root_child_first_slot_prev_minus_one(self):
"""Verify root child chains use -1 as the initial previous slot."""
cache = make_radix_cache([[], [42, 43]], device=self.device)
result = walk_radix_cache_for_canary(radix_cache=cache)
self.assertEqual(int(result.prev_slot_indices[0]), -1)
def test_position_equals_depth_from_root(self):
"""Verify emitted positions match depth from the radix root."""
cache = make_radix_cache([[], [1, 2], [3], [4, 5]], device=self.device)
result = walk_radix_cache_for_canary(radix_cache=cache)
self.assertEqual(result.positions.tolist(), [0, 1, 2, 3, 4])
self.assertEqual(result.slot_indices.tolist(), [1, 2, 3, 4, 5])
def test_walk_includes_locked_nodes_by_default(self):
"""Verify radix walking includes locked nodes by default."""
cache = make_radix_cache([[], [1, 2], [3, 4]], device=self.device)
locked_node = next(iter(cache.root_node.children.values()))
locked_node.lock_ref = 1
result = walk_radix_cache_for_canary(radix_cache=cache)
self.assertEqual(result.slot_indices.tolist(), [1, 2, 3, 4])
def test_walk_unlocked_only_skips_locked(self):
"""Verify unlocked-only radix walking skips locked nodes."""
cache = make_radix_cache([[], [1, 2], [3, 4]], device=self.device)
locked_node = next(iter(cache.root_node.children.values()))
locked_node.lock_ref = 1
result = walk_radix_cache_for_canary(radix_cache=cache, unlocked_only=True)
self.assertEqual(result.slot_indices.tolist(), [3, 4])
def test_walk_unlocked_only_uses_swa_full_lock_ref(self):
"""Verify SWA radix walking honors full-pool lock references."""
cache = SWARadixCache.__new__(SWARadixCache)
cache.device = self.device
cache.page_size = 1
cache.disable = False
root = TreeNode()
root.value = torch.tensor([], dtype=torch.int32, device=self.device)
cache.root_node = root
locked_child = TreeNode()
locked_child.value = torch.tensor([1, 2], dtype=torch.int32, device=self.device)
locked_child.parent = root
locked_child.full_lock_ref = 1
root.children[locked_child.id] = locked_child
unlocked_child = TreeNode()
unlocked_child.value = torch.tensor(
[3, 4], dtype=torch.int32, device=self.device
)
unlocked_child.parent = root
root.children[unlocked_child.id] = unlocked_child
result = walk_radix_cache_for_canary(radix_cache=cache, unlocked_only=True)
self.assertEqual(result.slot_indices.tolist(), [3, 4])
def test_swa_resident_only_skips_tombstoned_nodes(self):
"""Verify SWA radix walking skips nodes whose SWA storage was evicted."""
cache = SWARadixCache.__new__(SWARadixCache)
cache.device = self.device
cache.page_size = 1
cache.disable = False
root = TreeNode()
root.value = torch.tensor([], dtype=torch.int32, device=self.device)
cache.root_node = root
tombstoned_child = TreeNode()
tombstoned_child.value = torch.tensor(
[1, 2], dtype=torch.int32, device=self.device
)
tombstoned_child.parent = root
tombstoned_child.swa_tombstone = True
root.children[tombstoned_child.id] = tombstoned_child
resident_child = TreeNode()
resident_child.value = torch.tensor(
[3, 4], dtype=torch.int32, device=self.device
)
resident_child.parent = root
root.children[resident_child.id] = resident_child
result = walk_radix_cache_for_canary(
radix_cache=cache,
swa_resident_only=True,
)
self.assertEqual(result.slot_indices.tolist(), [3, 4])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,99 @@
from __future__ import annotations
import unittest
from unittest.mock import patch
from sglang.srt.kv_canary import endpoint as endpoint_module
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kv_canary.fixtures import (
make_forward_batch,
make_radix_cache,
make_req_to_token_pool,
)
from sglang.test.kv_canary.runner_test_base import (
CanaryManagerTestCase,
make_config,
make_manager,
)
register_cuda_ci(est_time=45, stage="extra-a", runner_config="1-gpu-small")
def _run_one_cycle(manager, forward_batch) -> None:
with manager.with_ops_outside_graph(
single_forward_indices=[0],
maybe_inaccurate_forward_batch=forward_batch,
):
with manager.with_active_single_forward_manager(0):
pre_ops_output = manager.pre_ops_maybe_inside_graph(forward_batch)
manager.post_ops_maybe_inside_graph(forward_batch, pre_ops_output)
class TestSelfUnitManagerSweep(CanaryManagerTestCase):
def test_sweep_every_n_cadence(self) -> None:
"""Verify sweep execution follows the configured step cadence."""
config = make_config(sweep_interval=4)
manager = make_manager(device=self.device, config=config)
forward_batch = make_forward_batch(self.device)
sweep_calls: list[int] = []
real_maybe = manager._sweep_orchestrator.maybe_run_sweep
def _spy() -> None:
before = manager._sweep_orchestrator._last_sweep_step
real_maybe()
if manager._sweep_orchestrator._last_sweep_step != before:
sweep_calls.append(manager._outer_step_counter)
with patch.object(manager._sweep_orchestrator, "maybe_run_sweep", _spy):
for _ in range(12):
_run_one_cycle(manager, forward_batch)
self.assertEqual(sweep_calls, [0, 4, 8])
def test_sweep_path_launches_sweep_kernels(self) -> None:
"""Verify sweep paths launch sweep verify kernels."""
config = make_config(sweep_interval=1)
manager = make_manager(device=self.device, config=config)
forward_batch = make_forward_batch(self.device)
manager._single_forward_managers[0].pre_ops_outside_graph(
maybe_inaccurate_forward_batch=forward_batch
)
with manager.with_active_single_forward_manager(0):
manager.pre_ops_maybe_inside_graph(forward_batch)
cache = make_radix_cache([[], [10, 11, 12]], device=self.device)
cache.req_to_token_pool = make_req_to_token_pool(self.device)
manager.attach_radix_cache(cache)
sweep_kernel_kinds: list[str] = []
with patch.object(
endpoint_module,
"launch_canary_verify_kernel",
lambda **kwargs: sweep_kernel_kinds.append(
kwargs["context"].kernel_kind.name
),
):
manager._sweep_orchestrator.maybe_run_sweep()
self.assertTrue(any("SWEEP" in kind for kind in sweep_kernel_kinds))
def test_sweep_allocates_verify_plan_from_walker_output(self) -> None:
"""Verify sweep planning sizes the verify plan from walker output."""
manager = make_manager(device=self.device)
cache = make_radix_cache([[], [10, 11], [12, 13, 14]], device=self.device)
cache.req_to_token_pool = make_req_to_token_pool(self.device)
manager.attach_radix_cache(cache)
valid_counts: list[int] = []
with patch.object(
endpoint_module,
"launch_canary_verify_kernel",
lambda **kwargs: valid_counts.append(
int(kwargs["plan"].verify_num_valid.item())
),
):
manager._sweep_orchestrator.maybe_run_sweep()
self.assertTrue(all(count == 5 for count in valid_counts))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,100 @@
from __future__ import annotations
import unittest
import torch
from sglang.srt.kv_canary.sweep_plan_builder import build_verify_plan_radix_sweep
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kv_canary.fixtures import (
DEFAULT_DEVICE,
make_radix_cache,
make_req_to_token_pool,
)
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=30, stage="extra-a", runner_config="1-gpu-small")
class TestSelfUnitSweepPlanBuilder(CustomTestCase):
def setUp(self) -> None:
self.device = DEFAULT_DEVICE
def test_build_verify_plan_radix_sweep(self) -> None:
"""Verify radix sweep verify plans include cached slot chains."""
empty_cache = make_radix_cache([[]], device=self.device)
empty_cache.req_to_token_pool = make_req_to_token_pool(self.device)
empty_out = build_verify_plan_radix_sweep(
radix_cache=empty_cache,
swa_window_size=0,
full_to_swa_index_mapping=None,
)
self.assertEqual(int(empty_out.verify_num_valid.item()), 0)
cache = make_radix_cache([[], [100, 101, 102]], device=self.device)
cache.req_to_token_pool = make_req_to_token_pool(self.device)
out = build_verify_plan_radix_sweep(
radix_cache=cache,
swa_window_size=0,
full_to_swa_index_mapping=None,
)
self.assertEqual(int(out.verify_num_valid.item()), 3)
self.assertEqual(out.verify_slot_indices.dtype, torch.int64)
self.assertEqual(out.verify_expected_positions.dtype, torch.int64)
self.assertEqual(out.verify_prev_slot_indices.dtype, torch.int64)
self.assertEqual(out.verify_slot_indices[:3].tolist(), [100, 101, 102])
self.assertEqual(out.verify_expected_positions[:3].tolist(), [0, 1, 2])
self.assertEqual(out.verify_prev_slot_indices[:3].tolist(), [-1, 100, 101])
def test_radix_held_slot_still_swept(self) -> None:
"""Verify held radix slots are still included in sweep plans."""
cache = make_radix_cache([[], [42, 43, 44]], device=self.device)
cache.req_to_token_pool = make_req_to_token_pool(self.device)
out = build_verify_plan_radix_sweep(
radix_cache=cache,
swa_window_size=0,
full_to_swa_index_mapping=None,
)
num_valid = int(out.verify_num_valid.item())
self.assertEqual(num_valid, 3)
self.assertEqual(
set(out.verify_slot_indices[:num_valid].tolist()), {42, 43, 44}
)
def test_truly_free_slot_not_swept(self) -> None:
"""Verify free radix slots are excluded from sweep plans."""
empty_cache = make_radix_cache([[]], device=self.device)
empty_cache.req_to_token_pool = make_req_to_token_pool(self.device)
out = build_verify_plan_radix_sweep(
radix_cache=empty_cache,
swa_window_size=0,
full_to_swa_index_mapping=None,
)
self.assertEqual(int(out.verify_num_valid.item()), 0)
def test_swa_translate_preserves_evicted_as_padding_sentinel(self) -> None:
"""Evicted (LUT=0) slots stay in the plan as the padding sentinel; the kernel does the skipping."""
cache = make_radix_cache([[], [100, 101, 102]], device=self.device)
cache.req_to_token_pool = make_req_to_token_pool(self.device)
lut = torch.zeros(200, dtype=torch.int64, device=self.device)
lut[100] = 500
lut[101] = 0
lut[102] = 502
out = build_verify_plan_radix_sweep(
radix_cache=cache,
swa_window_size=128,
full_to_swa_index_mapping=lut,
)
num_valid = int(out.verify_num_valid.item())
self.assertEqual(num_valid, 3)
self.assertEqual(out.verify_slot_indices[:num_valid].tolist(), [500, 0, 502])
self.assertEqual(
out.verify_prev_slot_indices[:num_valid].tolist(), [-1, 500, 0]
)
self.assertEqual(out.verify_expected_positions[:num_valid].tolist(), [0, 1, 2])
if __name__ == "__main__":
unittest.main()