Add a periodic full-radix-tree KV-canary sweep (#26812)
This commit is contained in:
@@ -38,7 +38,7 @@ def launch_canary_plan_kernels(
|
||||
prefix_lens[r] - swa_window_size) if SWA else 0. slot_idx = req_to_token[req_pool_indices[r], pos]
|
||||
(SWA-translated via full_to_swa_index_mapping if non-None); prev_slot_idx =
|
||||
req_to_token[req_pool_indices[r], pos-1] for pos > 0, else -1. (SWA windows do NOT reset the chain —
|
||||
the writer chains across the entire prefix; verify within an SWA window dereferences the real
|
||||
the writer chains across the entire prefix; sweep verify within an SWA window dereferences the real
|
||||
predecessor for chain-link reconstruction.) Expected-token gather: when
|
||||
``req_to_verify_expected_tokens`` is supplied, ``expected_input_id =
|
||||
req_to_verify_expected_tokens[rp, pos + kv_token_id_vs_position_offset]`` when ``0 <= pos +
|
||||
|
||||
@@ -17,16 +17,20 @@ CANARY_SLOT_BYTES: Final[int] = consts.CANARY_FIELDS_PER_SLOT * 8
|
||||
|
||||
|
||||
class CanaryLaunchTag(IntEnum):
|
||||
"""Unique tag per (head | tail) × (K | V) × (FULL | SWA) launch."""
|
||||
"""Unique tag per (head | tail | sweep) × (K | V) × (FULL | SWA) launch."""
|
||||
|
||||
HEAD_K_FULL = 0
|
||||
HEAD_V_FULL = 1
|
||||
TAIL_K_FULL = 2
|
||||
TAIL_V_FULL = 3
|
||||
HEAD_K_SWA = 4
|
||||
HEAD_V_SWA = 5
|
||||
TAIL_K_SWA = 6
|
||||
TAIL_V_SWA = 7
|
||||
SWEEP_K_FULL = 4
|
||||
SWEEP_V_FULL = 5
|
||||
HEAD_K_SWA = 6
|
||||
HEAD_V_SWA = 7
|
||||
TAIL_K_SWA = 8
|
||||
TAIL_V_SWA = 9
|
||||
SWEEP_K_SWA = 10
|
||||
SWEEP_V_SWA = 11
|
||||
|
||||
|
||||
def _assert_contiguous(tensor: torch.Tensor, name: str) -> None:
|
||||
@@ -68,7 +72,8 @@ class VerifyPlan:
|
||||
"""Flat verify entries consumed by launch_canary_verify_kernel.
|
||||
|
||||
Each row is a self-contained (slot_idx, position, prev_slot_idx) triple, so the verify kernel makes no
|
||||
assumption about the entry's source. prev_slot_idx == -1 flags a chain-seed entry (kernel
|
||||
assumption about the entry's source — per-forward derivation, sweep over running reqs, and sweep over
|
||||
radix-cache orphan slots all populate the same schema. prev_slot_idx == -1 flags a chain-seed entry (kernel
|
||||
anchors on the hardcoded CANARY_CHAIN_ANCHOR constant instead of reading a predecessor).
|
||||
|
||||
Sized to a cuda-graph-captured capacity; active prefix is verify_num_valid[0]. Padding tail entries are
|
||||
|
||||
@@ -635,7 +635,7 @@ def test_pipeline_ring_overflow_via_real_plan() -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kernel_kind", [CanaryLaunchTag.HEAD_K_FULL, CanaryLaunchTag.TAIL_V_SWA]
|
||||
"kernel_kind", [CanaryLaunchTag.HEAD_K_FULL, CanaryLaunchTag.SWEEP_V_SWA]
|
||||
)
|
||||
def test_pipeline_kernel_kind_propagates(kernel_kind: CanaryLaunchTag) -> None:
|
||||
"""Different CanaryLaunchTag values: violation ring's kernel_kind field matches on both sides."""
|
||||
|
||||
@@ -966,7 +966,7 @@ class TestViolationRing:
|
||||
buf_pair = _buf_pair()
|
||||
_stamp_head(buf_pair, slot_idx=1, token=1)
|
||||
|
||||
for tag in (CanaryLaunchTag.HEAD_K_FULL, CanaryLaunchTag.TAIL_V_SWA):
|
||||
for tag in (CanaryLaunchTag.HEAD_K_FULL, CanaryLaunchTag.SWEEP_V_SWA):
|
||||
plan_pair = _plan_pair_single(slot_idx=1, position=99)
|
||||
cuda_log, _ = run_verify_diff(
|
||||
buf_pair=buf_pair, plan_pair=plan_pair, kernel_kind=tag
|
||||
|
||||
@@ -30,10 +30,14 @@ class CanaryConfig:
|
||||
violations propagate to host as RuntimeError after the next D2H pump.
|
||||
ring_capacity: Violation ring capacity (rows in ViolationLog.violation_ring). Sized generously;
|
||||
overflow only drops detail beyond row N, the monotonic counter still grows.
|
||||
sweep_interval: 0 disables sweep entirely; positive N means every N-th forward step the runner
|
||||
additionally walks all radix-tree-held slots (overlap with per-forward HEAD/TAIL is harmless
|
||||
redundancy) and verifies them.
|
||||
"""
|
||||
|
||||
mode: CanaryMode
|
||||
ring_capacity: int
|
||||
sweep_interval: int
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, server_args: "ServerArgs") -> "CanaryConfig":
|
||||
@@ -46,4 +50,5 @@ class CanaryConfig:
|
||||
return cls(
|
||||
mode=CanaryMode(mode_raw),
|
||||
ring_capacity=envs.SGLANG_KV_CANARY_RING_CAPACITY.get(),
|
||||
sweep_interval=server_args.kv_canary_sweep_interval,
|
||||
)
|
||||
|
||||
@@ -45,6 +45,11 @@ class CanaryEndpoint:
|
||||
expected_inputs: ExpectedInputs,
|
||||
violation_log: ViolationLog,
|
||||
) -> None:
|
||||
if _is_sweep_tag(self.kernel_kind):
|
||||
raise NotImplementedError(
|
||||
f"kv-canary: launch_per_forward not supported on sweep endpoint {self.kernel_kind.name}"
|
||||
)
|
||||
|
||||
context = self._make_verify_or_write_context(
|
||||
violation_log=violation_log,
|
||||
)
|
||||
@@ -77,6 +82,25 @@ class CanaryEndpoint:
|
||||
expected_input_positions=expected_input_positions,
|
||||
)
|
||||
|
||||
def launch_sweep(
|
||||
self,
|
||||
*,
|
||||
verify_plan: VerifyPlan,
|
||||
violation_log: ViolationLog,
|
||||
) -> None:
|
||||
if not _is_sweep_tag(self.kernel_kind):
|
||||
raise NotImplementedError(
|
||||
f"kv-canary: launch_sweep not supported on non-sweep endpoint {self.kernel_kind.name}"
|
||||
)
|
||||
|
||||
launch_canary_verify_kernel(
|
||||
context=self._make_verify_or_write_context(
|
||||
violation_log=violation_log,
|
||||
),
|
||||
plan=verify_plan,
|
||||
check_verify_expected_token=False,
|
||||
)
|
||||
|
||||
def _make_verify_or_write_context(
|
||||
self,
|
||||
*,
|
||||
@@ -93,6 +117,15 @@ class CanaryEndpoint:
|
||||
)
|
||||
|
||||
|
||||
def _is_sweep_tag(tag: CanaryLaunchTag) -> bool:
|
||||
return tag in (
|
||||
CanaryLaunchTag.SWEEP_K_FULL,
|
||||
CanaryLaunchTag.SWEEP_V_FULL,
|
||||
CanaryLaunchTag.SWEEP_K_SWA,
|
||||
CanaryLaunchTag.SWEEP_V_SWA,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_canary_buf(
|
||||
*,
|
||||
slot: str,
|
||||
@@ -113,6 +146,8 @@ _FULL_LAYOUT: tuple[tuple[CanaryLaunchTag, str, str], ...] = (
|
||||
(CanaryLaunchTag.HEAD_V_FULL, "HEAD", "V"),
|
||||
(CanaryLaunchTag.TAIL_K_FULL, "TAIL", "K"),
|
||||
(CanaryLaunchTag.TAIL_V_FULL, "TAIL", "V"),
|
||||
(CanaryLaunchTag.SWEEP_K_FULL, "SWEEP", "K"),
|
||||
(CanaryLaunchTag.SWEEP_V_FULL, "SWEEP", "V"),
|
||||
)
|
||||
|
||||
|
||||
@@ -121,6 +156,8 @@ _SWA_LAYOUT: tuple[tuple[CanaryLaunchTag, str, str], ...] = (
|
||||
(CanaryLaunchTag.HEAD_V_SWA, "HEAD", "V"),
|
||||
(CanaryLaunchTag.TAIL_K_SWA, "TAIL", "K"),
|
||||
(CanaryLaunchTag.TAIL_V_SWA, "TAIL", "V"),
|
||||
(CanaryLaunchTag.SWEEP_K_SWA, "SWEEP", "K"),
|
||||
(CanaryLaunchTag.SWEEP_V_SWA, "SWEEP", "V"),
|
||||
)
|
||||
|
||||
|
||||
@@ -138,7 +175,8 @@ def build_endpoints_from_group(
|
||||
if half == "V" and not group.has_v_half:
|
||||
continue
|
||||
|
||||
canary_buf = _resolve_canary_buf(slot=slot, half=half, group=group)
|
||||
buf_slot = "TAIL" if slot == "SWEEP" else slot
|
||||
canary_buf = _resolve_canary_buf(slot=buf_slot, half=half, group=group)
|
||||
lut = group.swa_index_lut if pool_kind is PoolKind.SWA else None
|
||||
slot_view = device_state.slot_run_counters[tag.value : tag.value + 1]
|
||||
kernel_view = device_state.kernel_run_counters[tag.value : tag.value + 1]
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.radix_cache import RadixCache
|
||||
from sglang.srt.mem_cache.swa_radix_cache import SWARadixCache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
|
||||
from sglang.srt.mem_cache.radix_cache import TreeNode
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, kw_only=True)
|
||||
class RadixCacheWalkResult:
|
||||
slot_indices: torch.Tensor
|
||||
positions: torch.Tensor
|
||||
prev_slot_indices: torch.Tensor
|
||||
|
||||
|
||||
def walk_radix_cache_for_canary(
|
||||
*,
|
||||
radix_cache: "BasePrefixCache",
|
||||
unlocked_only: bool = False,
|
||||
swa_resident_only: bool = False,
|
||||
) -> RadixCacheWalkResult:
|
||||
"""Walk the radix tree and emit flat (slot_indices, positions, prev_slot_indices) tensors.
|
||||
|
||||
With both flags False (default), emits every slot held by the radix cache (including slots
|
||||
also referenced by a currently-running req — that overlap is harmless redundancy with the
|
||||
per-forward HEAD/TAIL path). ``unlocked_only=True`` skips nodes still locked by a running
|
||||
req. ``swa_resident_only=True`` skips SWA-tombstoned nodes (slots evicted from the SWA
|
||||
window)."""
|
||||
cache_type = type(radix_cache)
|
||||
if cache_type is not RadixCache and cache_type is not SWARadixCache:
|
||||
raise NotImplementedError(
|
||||
f"walk_radix_cache_for_canary does not support {cache_type.__name__}"
|
||||
)
|
||||
|
||||
slot_buf: list[int] = []
|
||||
position_buf: list[int] = []
|
||||
prev_slot_buf: list[int] = []
|
||||
|
||||
_walk_radix_subtree(
|
||||
node=radix_cache.root_node,
|
||||
radix_cache=radix_cache,
|
||||
depth=0,
|
||||
parent_last_slot=-1,
|
||||
slot_buf=slot_buf,
|
||||
position_buf=position_buf,
|
||||
prev_slot_buf=prev_slot_buf,
|
||||
is_root=True,
|
||||
unlocked_only=unlocked_only,
|
||||
swa_resident_only=swa_resident_only,
|
||||
)
|
||||
|
||||
slot_tensor = torch.tensor(slot_buf, dtype=torch.int64)
|
||||
position_tensor = torch.tensor(position_buf, dtype=torch.int64)
|
||||
prev_slot_tensor = torch.tensor(prev_slot_buf, dtype=torch.int64)
|
||||
return RadixCacheWalkResult(
|
||||
slot_indices=slot_tensor,
|
||||
positions=position_tensor,
|
||||
prev_slot_indices=prev_slot_tensor,
|
||||
)
|
||||
|
||||
|
||||
def _walk_radix_subtree(
|
||||
*,
|
||||
node: "TreeNode",
|
||||
radix_cache: "BasePrefixCache",
|
||||
depth: int,
|
||||
parent_last_slot: int,
|
||||
slot_buf: list[int],
|
||||
position_buf: list[int],
|
||||
prev_slot_buf: list[int],
|
||||
is_root: bool,
|
||||
unlocked_only: bool,
|
||||
swa_resident_only: bool,
|
||||
) -> None:
|
||||
if isinstance(node.value, torch.Tensor):
|
||||
node_slots = [int(s) for s in node.value.tolist()]
|
||||
else:
|
||||
node_slots = []
|
||||
|
||||
if unlocked_only:
|
||||
emit_slots = not is_root and _node_is_unlocked_for_canary(
|
||||
node=node, radix_cache=radix_cache
|
||||
)
|
||||
else:
|
||||
emit_slots = not is_root
|
||||
if swa_resident_only:
|
||||
emit_slots = emit_slots and _node_is_swa_resident_for_canary(
|
||||
node=node,
|
||||
radix_cache=radix_cache,
|
||||
)
|
||||
|
||||
chain_last_slot = parent_last_slot
|
||||
for j, slot in enumerate(node_slots):
|
||||
prev = parent_last_slot if j == 0 else node_slots[j - 1]
|
||||
if emit_slots:
|
||||
slot_buf.append(slot)
|
||||
position_buf.append(depth + j)
|
||||
prev_slot_buf.append(prev)
|
||||
chain_last_slot = slot
|
||||
|
||||
child_depth = depth + len(node_slots)
|
||||
for child in node.children.values():
|
||||
_walk_radix_subtree(
|
||||
node=child,
|
||||
radix_cache=radix_cache,
|
||||
depth=child_depth,
|
||||
parent_last_slot=chain_last_slot,
|
||||
slot_buf=slot_buf,
|
||||
position_buf=position_buf,
|
||||
prev_slot_buf=prev_slot_buf,
|
||||
is_root=False,
|
||||
unlocked_only=unlocked_only,
|
||||
swa_resident_only=swa_resident_only,
|
||||
)
|
||||
|
||||
|
||||
def _node_is_unlocked_for_canary(
|
||||
*,
|
||||
node: "TreeNode",
|
||||
radix_cache: "BasePrefixCache",
|
||||
) -> bool:
|
||||
if type(radix_cache) is RadixCache:
|
||||
return node.lock_ref == 0
|
||||
|
||||
if type(radix_cache) is SWARadixCache:
|
||||
return node.full_lock_ref == 0
|
||||
|
||||
raise NotImplementedError(
|
||||
f"walk_radix_cache_for_canary does not support {type(radix_cache).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def _node_is_swa_resident_for_canary(
|
||||
*,
|
||||
node: "TreeNode",
|
||||
radix_cache: "BasePrefixCache",
|
||||
) -> bool:
|
||||
if type(radix_cache) is not SWARadixCache:
|
||||
return True
|
||||
|
||||
return not node.swa_tombstone
|
||||
@@ -16,6 +16,7 @@ from sglang.srt.kv_canary.endpoint import (
|
||||
CanaryEndpoint,
|
||||
build_endpoints_from_group,
|
||||
)
|
||||
from sglang.srt.kv_canary.runner.sweep import SweepOrchestrator
|
||||
from sglang.srt.kv_canary.runner.violation_manager import ViolationManager
|
||||
from sglang.srt.kv_canary.single_forward_manager.manager import (
|
||||
SingleForwardManager,
|
||||
@@ -24,6 +25,7 @@ from sglang.srt.kv_canary.single_forward_manager.manager import (
|
||||
from sglang.srt.kv_canary.state import CanaryDeviceState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
|
||||
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
|
||||
@@ -82,6 +84,14 @@ class CanaryManager:
|
||||
d2h_stream=self._d2h_stream,
|
||||
outer_step_counter_getter=self._get_outer_step_counter,
|
||||
)
|
||||
self._sweep_orchestrator = SweepOrchestrator(
|
||||
config=config,
|
||||
device_state=self._device_state,
|
||||
buffer_groups=self._buffer_groups,
|
||||
endpoints=self._endpoints,
|
||||
swa_window_size=self._swa_window_size,
|
||||
outer_step_counter_getter=self._get_outer_step_counter,
|
||||
)
|
||||
self._single_forward_managers: tuple[SingleForwardManager, ...] = (
|
||||
SingleForwardManager(
|
||||
config=config,
|
||||
@@ -174,6 +184,7 @@ class CanaryManager:
|
||||
) -> None:
|
||||
for idx in single_forward_indices:
|
||||
self._single_forward_managers[idx].post_ops_outside_graph()
|
||||
self._sweep_orchestrator.maybe_run_sweep()
|
||||
self._outer_step_counter += 1
|
||||
self._violation_manager.step()
|
||||
|
||||
@@ -182,6 +193,9 @@ class CanaryManager:
|
||||
single_forward_manager.phase_checker.enable_assert()
|
||||
self._device_state.enable_chain_position_assert.fill_(1)
|
||||
|
||||
def attach_radix_cache(self, radix_cache: "BasePrefixCache") -> None:
|
||||
self._sweep_orchestrator.attach_radix_cache(radix_cache)
|
||||
|
||||
def _get_outer_step_counter(self) -> int:
|
||||
return self._outer_step_counter
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ def launch_endpoints_per_forward(
|
||||
for endpoint in endpoints
|
||||
if _endpoint_belongs_to_group(endpoint, group)
|
||||
and tag_filter(endpoint.kernel_kind)
|
||||
and not _is_sweep_tag(endpoint.kernel_kind)
|
||||
and passes_v_half_gate(endpoint.kernel_kind)
|
||||
]
|
||||
assert len(active_endpoints) > 0
|
||||
@@ -106,12 +107,46 @@ def launch_endpoints_per_forward(
|
||||
)
|
||||
|
||||
|
||||
def launch_endpoints_sweep(
|
||||
*,
|
||||
endpoints: tuple[CanaryEndpoint, ...],
|
||||
group: CanaryBufferGroup,
|
||||
verify_plan: VerifyPlan,
|
||||
violation_log: ViolationLog,
|
||||
) -> None:
|
||||
active_endpoints = [
|
||||
endpoint
|
||||
for endpoint in endpoints
|
||||
if _endpoint_belongs_to_group(endpoint, group)
|
||||
and _is_sweep_tag(endpoint.kernel_kind)
|
||||
and passes_v_half_gate(endpoint.kernel_kind)
|
||||
]
|
||||
assert len(active_endpoints) > 0
|
||||
|
||||
for endpoint in active_endpoints:
|
||||
endpoint.launch_sweep(
|
||||
verify_plan=verify_plan,
|
||||
violation_log=violation_log,
|
||||
)
|
||||
|
||||
|
||||
def _is_sweep_tag(tag: CanaryLaunchTag) -> bool:
|
||||
return tag in (
|
||||
CanaryLaunchTag.SWEEP_K_FULL,
|
||||
CanaryLaunchTag.SWEEP_V_FULL,
|
||||
CanaryLaunchTag.SWEEP_K_SWA,
|
||||
CanaryLaunchTag.SWEEP_V_SWA,
|
||||
)
|
||||
|
||||
|
||||
def _is_v_half_tag(tag: CanaryLaunchTag) -> bool:
|
||||
return tag in (
|
||||
CanaryLaunchTag.HEAD_V_FULL,
|
||||
CanaryLaunchTag.TAIL_V_FULL,
|
||||
CanaryLaunchTag.SWEEP_V_FULL,
|
||||
CanaryLaunchTag.HEAD_V_SWA,
|
||||
CanaryLaunchTag.TAIL_V_SWA,
|
||||
CanaryLaunchTag.SWEEP_V_SWA,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup, PoolKind
|
||||
from sglang.srt.kv_canary.config import CanaryConfig
|
||||
from sglang.srt.kv_canary.endpoint import CanaryEndpoint
|
||||
from sglang.srt.kv_canary.runner.kernel_launcher import launch_endpoints_sweep
|
||||
from sglang.srt.kv_canary.state import CanaryDeviceState
|
||||
from sglang.srt.kv_canary.sweep_plan_builder import build_verify_plan_radix_sweep
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SweepOrchestrator:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
config: CanaryConfig,
|
||||
device_state: CanaryDeviceState,
|
||||
buffer_groups: tuple[CanaryBufferGroup, ...],
|
||||
endpoints: tuple[CanaryEndpoint, ...],
|
||||
swa_window_size: int,
|
||||
outer_step_counter_getter: Callable[[], int],
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._device_state = device_state
|
||||
self._buffer_groups = buffer_groups
|
||||
self._endpoints = endpoints
|
||||
self._swa_window_size = swa_window_size
|
||||
self._outer_step_counter_getter = outer_step_counter_getter
|
||||
self._radix_cache: Optional["BasePrefixCache"] = None
|
||||
|
||||
self._last_sweep_step: int = -1
|
||||
self._sweep_passes: int = 0
|
||||
|
||||
@property
|
||||
def sweep_passes(self) -> int:
|
||||
return self._sweep_passes
|
||||
|
||||
def attach_radix_cache(self, radix_cache: "BasePrefixCache") -> None:
|
||||
self._radix_cache = radix_cache
|
||||
|
||||
def maybe_run_sweep(self) -> None:
|
||||
if self._config.sweep_interval == 0:
|
||||
return
|
||||
outer_step_counter = self._outer_step_counter_getter()
|
||||
if (
|
||||
self._last_sweep_step >= 0
|
||||
and outer_step_counter - self._last_sweep_step < self._config.sweep_interval
|
||||
):
|
||||
return
|
||||
self._last_sweep_step = outer_step_counter
|
||||
|
||||
if self._radix_cache is None:
|
||||
return
|
||||
|
||||
violation_log = self._device_state.violation_log
|
||||
for group in self._buffer_groups:
|
||||
window = self._swa_window_size if group.kind is PoolKind.SWA else 0
|
||||
verify_plan = build_verify_plan_radix_sweep(
|
||||
radix_cache=self._radix_cache,
|
||||
swa_window_size=window,
|
||||
full_to_swa_index_mapping=group.swa_index_lut,
|
||||
)
|
||||
launch_endpoints_sweep(
|
||||
endpoints=self._endpoints,
|
||||
group=group,
|
||||
verify_plan=verify_plan,
|
||||
violation_log=violation_log,
|
||||
)
|
||||
|
||||
self._sweep_passes += 1
|
||||
logger.info(
|
||||
"[canary] sweep succeeded %d times (last_step=%d)",
|
||||
self._sweep_passes,
|
||||
outer_step_counter,
|
||||
)
|
||||
@@ -69,6 +69,13 @@ class ViolationReporter:
|
||||
|
||||
def _canary_kind_label(tag: CanaryLaunchTag) -> str:
|
||||
name_lower = tag.name.lower()
|
||||
if tag in (
|
||||
CanaryLaunchTag.SWEEP_K_FULL,
|
||||
CanaryLaunchTag.SWEEP_V_FULL,
|
||||
CanaryLaunchTag.SWEEP_K_SWA,
|
||||
CanaryLaunchTag.SWEEP_V_SWA,
|
||||
):
|
||||
return name_lower
|
||||
return f"per_forward_{name_lower}"
|
||||
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ from sglang.srt.kv_canary.config import CanaryConfig
|
||||
class ViolationLog:
|
||||
"""Global violation sink shared across all canary launches.
|
||||
|
||||
One instance per canary runner — every launch (head / tail, K / V half, FULL / SWA group) writes
|
||||
One instance per canary runner — every launch (head / tail / sweep, K / V half, FULL / SWA group) writes
|
||||
into the same ring. The kernel_kind field stamped into each violation row identifies which launch fired
|
||||
(kernel_kind is a static IntEnum tag — :class:`CanaryLaunchTag` in
|
||||
``sglang.jit_kernel.kv_canary.verify`` — with a unique value per (head|tail, K|V, FULL|SWA) tuple).
|
||||
``sglang.jit_kernel.kv_canary.verify`` — with a unique value per (head|tail|sweep, K|V, FULL|SWA) tuple).
|
||||
|
||||
Ring capacity is sized generously (≥ 1024) so overflow is a non-concern in practice — violations are
|
||||
cold-path and the host raises at the first one anyway (or just logs it in mode="log"). atomicAdd
|
||||
@@ -63,7 +63,7 @@ class CanaryDeviceState:
|
||||
no per-step allocation.
|
||||
|
||||
Fields:
|
||||
violation_log: The single ViolationLog shared by every launch (head / tail × K / V ×
|
||||
violation_log: The single ViolationLog shared by every launch (head / tail / sweep × K / V ×
|
||||
FULL / SWA). All kernels atomicAdd into violation_log.violation_write_index and stamp their
|
||||
CanaryLaunchTag into each violation row.
|
||||
kernel_run_counters: Per-CanaryLaunchTag int64 counter array, shape [num_tags], device. The
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.kv_canary.verify import VerifyPlan
|
||||
from sglang.srt.kv_canary.radix_cache_walker import walk_radix_cache_for_canary
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
|
||||
|
||||
|
||||
def build_verify_plan_radix_sweep(
|
||||
*,
|
||||
radix_cache: "BasePrefixCache",
|
||||
swa_window_size: int,
|
||||
full_to_swa_index_mapping: Optional[torch.Tensor],
|
||||
unlocked_only: bool = False,
|
||||
) -> VerifyPlan:
|
||||
"""Build a sweep VerifyPlan directly from the radix-cache walker.
|
||||
|
||||
The walker covers every slot held by the radix tree. There is no exclusion for slots also owned
|
||||
by running requests because the overlap with per-forward HEAD/TAIL coverage is harmless
|
||||
redundancy. The helper applies the SWA LUT before writing the plan.
|
||||
"""
|
||||
device = radix_cache.req_to_token_pool.req_to_token.device
|
||||
|
||||
walk_result = walk_radix_cache_for_canary(
|
||||
radix_cache=radix_cache,
|
||||
unlocked_only=unlocked_only,
|
||||
)
|
||||
slot_indices = walk_result.slot_indices.to(device)
|
||||
positions = walk_result.positions.to(device)
|
||||
prev_slot_indices = walk_result.prev_slot_indices.to(device)
|
||||
|
||||
if swa_window_size > 0:
|
||||
assert (
|
||||
full_to_swa_index_mapping is not None
|
||||
), "full_to_swa_index_mapping is required when SWA is enabled"
|
||||
slot_indices = _swa_translate(
|
||||
indices=slot_indices, lut=full_to_swa_index_mapping
|
||||
)
|
||||
prev_slot_indices = _swa_translate(
|
||||
indices=prev_slot_indices, lut=full_to_swa_index_mapping
|
||||
)
|
||||
|
||||
num_valid = int(slot_indices.shape[0])
|
||||
verify_plan = VerifyPlan.allocate(verify_capacity=max(1, num_valid), device=device)
|
||||
|
||||
verify_plan.verify_slot_indices[:num_valid].copy_(slot_indices)
|
||||
verify_plan.verify_expected_positions[:num_valid].copy_(positions)
|
||||
verify_plan.verify_prev_slot_indices[:num_valid].copy_(prev_slot_indices)
|
||||
verify_plan.verify_num_valid.fill_(num_valid)
|
||||
verify_plan.enable.fill_(1)
|
||||
|
||||
return verify_plan
|
||||
|
||||
|
||||
def _swa_translate(
|
||||
*,
|
||||
indices: torch.Tensor,
|
||||
lut: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
# 0 is both SWAKVPool's evicted-sentinel and the kernel's kTokenToKvSlotPadding,
|
||||
# so evicted indices propagate as the canonical "no real slot" value and the
|
||||
# verify kernel handles them.
|
||||
if indices.numel() == 0:
|
||||
return indices
|
||||
lut_dev = lut.to(indices.device).to(torch.int64)
|
||||
anchor_mask = indices < 0
|
||||
safe = torch.where(anchor_mask, torch.zeros_like(indices), indices).to(torch.int64)
|
||||
looked_up = lut_dev[safe]
|
||||
return torch.where(anchor_mask, indices.to(torch.int64), looked_up)
|
||||
@@ -451,6 +451,9 @@ class Scheduler(
|
||||
self.disable_radix_cache = result.disable_radix_cache
|
||||
self.tree_cache = result.tree_cache
|
||||
|
||||
if (c := self.tp_worker.model_runner.canary_manager) is not None:
|
||||
c.attach_radix_cache(self.tree_cache)
|
||||
|
||||
if self.enable_hisparse:
|
||||
# Coordinator was created inside ModelRunner.initialize() before CUDA graph capture
|
||||
self.hisparse_coordinator = self.tp_worker.model_runner.hisparse_coordinator
|
||||
|
||||
@@ -778,6 +778,7 @@ class ServerArgs:
|
||||
disable_attn_tp_gather: bool = False
|
||||
gc_threshold: Optional[List[int]] = None
|
||||
kv_canary: str = "none"
|
||||
kv_canary_sweep_interval: int = 0
|
||||
# Context parallelism used in the long sequence prefill phase of DeepSeek v3.2
|
||||
enable_dsa_prefill_context_parallel: bool = False
|
||||
dsa_prefill_cp_mode: str = "round-robin-split"
|
||||
@@ -6318,6 +6319,12 @@ class ServerArgs:
|
||||
"'raise' fails the server on the first detected mismatch (CI lane)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kv-canary-sweep-interval",
|
||||
type=int,
|
||||
default=ServerArgs.kv_canary_sweep_interval,
|
||||
help="Every N forward steps, run a full-pool sweep.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cuda-graph-max-bs",
|
||||
type=int,
|
||||
@@ -7367,6 +7374,11 @@ class ServerArgs:
|
||||
"When setting gc_threshold, it must contain 1 to 3 integers."
|
||||
)
|
||||
|
||||
if self.kv_canary_sweep_interval > 0 and self.kv_canary == "none":
|
||||
raise ValueError(
|
||||
"--kv-canary-sweep-interval requires --kv-canary in {log, raise}"
|
||||
)
|
||||
|
||||
def check_lora_server_args(self):
|
||||
assert self.max_loras_per_batch > 0, "max_loras_per_batch must be positive"
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ def make_base_config() -> CanaryConfig:
|
||||
return CanaryConfig(
|
||||
mode=CanaryMode.RAISE,
|
||||
ring_capacity=1024,
|
||||
sweep_interval=0,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ def make_config(
|
||||
*,
|
||||
mode: CanaryMode = CanaryMode.RAISE,
|
||||
ring_capacity: int = 1024,
|
||||
sweep_interval: int = 0,
|
||||
) -> CanaryConfig:
|
||||
return CanaryConfig(
|
||||
mode=mode,
|
||||
ring_capacity=ring_capacity,
|
||||
sweep_interval=sweep_interval,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user