Add the KV-canary perturb framework for fault-injection self-tests (#26816)

This commit is contained in:
fzyzcjy
2026-05-31 09:58:14 +08:00
committed by GitHub
parent 678e73a9ee
commit cdee16e144
9 changed files with 331 additions and 2 deletions
+1
View File
@@ -755,6 +755,7 @@ class Envs:
SGLANG_KV_CANARY_RING_CAPACITY = EnvInt(1024)
SGLANG_KV_CANARY_ENABLE_WRITE_INPUT_ASSERT = EnvBool(False)
SGLANG_KV_CANARY_PERTURB_WARMUP_STEPS = EnvInt(50)
SGLANG_KV_CANARY_PERTURB_TARGET_GROUP = EnvStr(None)
SGLANG_KV_CANARY_PERTURB_NEXT_TOKEN_SWAP_PROB = EnvFloat(0.0)
SGLANG_KV_CANARY_ENABLE_TOKEN_ORACLE = EnvBool(False)
SGLANG_KV_CANARY_ENABLE_MHA_V = EnvBool(False)
+5 -1
View File
@@ -7,6 +7,7 @@ import torch
from sglang.srt.kv_canary.capacities import CanaryLaunchCapacities
from sglang.srt.kv_canary.config import CanaryConfig, CanaryMode
from sglang.srt.kv_canary.perturb.config import PerturbConfig
from sglang.srt.kv_canary.pool_patcher.api import attach_canary_buffers
from sglang.srt.kv_canary.pool_patcher.utils import wrap_method
from sglang.srt.kv_canary.runner.canary_manager import CanaryManager
@@ -36,6 +37,7 @@ def install_canary(
"when canary is enabled"
)
perturb_config = PerturbConfig.from_env()
device = torch.device(model_runner.device)
# EAGLE draft worker pools rotate input_ids so slot ``p`` stores K/V for the token at position ``p+1``;
# target pools have no such shift. Threaded into the plan-side expected-token gather kernel.
@@ -56,6 +58,7 @@ def install_canary(
speculative_num_steps = int(server_args.speculative_num_steps or 1)
manager = CanaryManager(
config=config,
perturb_config=perturb_config,
buffer_groups=buffer_groups,
device=device,
req_to_token_pool=model_runner.req_to_token_pool,
@@ -71,11 +74,12 @@ def install_canary(
# Single-line summary of every knob that controls canary behavior at boot time.
# Disaggregation mode is included so PD logs are unambiguous about which side this is.
logger.info(
"install_canary: disaggregation_mode=%s config=%s "
"install_canary: disaggregation_mode=%s config=%s perturb_config=%s "
"launch_capacities=%s n_buffer_groups=%d buffer_group_kinds=%s "
"swa_window_size=%d speculative_num_steps=%d",
server_args.disaggregation_mode,
config,
perturb_config,
launch_capacities,
len(buffer_groups),
[g.kind.name for g in buffer_groups],
@@ -0,0 +1,67 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import IntEnum
from sglang.srt.environ import envs
from sglang.srt.kv_canary.buffer_group import PoolKind
class TargetGroupKind(IntEnum):
FULL = PoolKind.FULL.value
SWA = PoolKind.SWA.value
def __str__(self) -> str:
return self.name.lower()
@dataclass(frozen=True, slots=True, kw_only=True)
class PerturbConfig:
target_group_kind: TargetGroupKind | None
warmup_steps: int
@classmethod
def from_env(cls) -> "PerturbConfig":
return cls(
target_group_kind=_parse_target_group_kind_from_env(
raw=envs.SGLANG_KV_CANARY_PERTURB_TARGET_GROUP.get(),
),
warmup_steps=envs.SGLANG_KV_CANARY_PERTURB_WARMUP_STEPS.get(),
)
def _parse_target_group_kind_from_env(
*,
raw: str | None,
) -> TargetGroupKind | None:
if raw is not None and raw.strip():
return _parse_target_group_kind(raw)
return None
def require_target_group_kind(
*, target_group_kind: TargetGroupKind | None, perturb_name: str
) -> TargetGroupKind:
if target_group_kind is None:
raise ValueError(
"SGLANG_KV_CANARY_PERTURB_TARGET_GROUP must be explicitly set to "
f"'full' or 'swa' when {perturb_name} perturbation is enabled"
)
return target_group_kind
def _parse_target_group_kind(raw: str | None) -> TargetGroupKind:
if raw is None or not raw.strip():
raise ValueError(
"SGLANG_KV_CANARY_PERTURB_TARGET_GROUP must be explicitly set to "
"'full' or 'swa'"
)
value = raw.strip().lower()
try:
return TargetGroupKind[value.upper()]
except KeyError:
raise ValueError(
"SGLANG_KV_CANARY_PERTURB_TARGET_GROUP must be one of 'full' / "
f"'swa', got {raw!r}"
) from None
@@ -0,0 +1,43 @@
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING, Optional
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup
from sglang.srt.kv_canary.perturb.config import PerturbConfig
from sglang.srt.kv_canary.perturb.utils import WarmupGate
if TYPE_CHECKING:
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
class PerturbManager:
def __init__(
self,
*,
config: PerturbConfig,
buffer_groups: tuple[CanaryBufferGroup, ...],
outer_step_counter_getter: Callable[[], int],
swa_window_size: int = 0,
sweep_interval: int = 0,
) -> None:
self._config = config
self._buffer_groups = buffer_groups
self._outer_step_counter_getter = outer_step_counter_getter
self._swa_window_size = swa_window_size
self._sweep_interval = sweep_interval
self._radix_cache: Optional["BasePrefixCache"] = None
self._warmup_gate = WarmupGate(
config=config, outer_step_counter_getter=outer_step_counter_getter
)
def attach_radix_cache(self, radix_cache: "BasePrefixCache") -> None:
self._radix_cache = radix_cache
def perturb(
self,
*,
maybe_inaccurate_forward_batch: Optional["ForwardBatch"],
) -> None:
pass
@@ -0,0 +1,107 @@
from __future__ import annotations
import logging
import random
from collections.abc import Callable
from typing import TYPE_CHECKING, Optional
import torch
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup, PoolKind
from sglang.srt.kv_canary.perturb.config import PerturbConfig, TargetGroupKind
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
class WarmupGate:
"""Per-hook warmup window check + once-per-lifetime disable/enable log emission.
Shared across the four perturb-point hooks so warmup state is decided in one place
rather than duplicated per hook.
"""
def __init__(
self,
*,
config: PerturbConfig,
outer_step_counter_getter: Callable[[], int],
) -> None:
self._config = config
self._outer_step_counter_getter = outer_step_counter_getter
self._warmup_disable_logged: bool = False
self._warmup_enable_logged: bool = False
def is_in_warmup(self) -> bool:
step = self._outer_step_counter_getter()
warmup_steps = self._config.warmup_steps
if step < warmup_steps:
self._log_warmup_disabled_once(warmup_steps)
return True
self._log_warmup_enabled_once(step)
return False
def _log_warmup_disabled_once(self, warmup_steps: int) -> None:
if self._warmup_disable_logged:
return
logger.info(
"kv_canary perturb: disabled during warmup window "
"(first %d forward steps)",
warmup_steps,
)
self._warmup_disable_logged = True
def _log_warmup_enabled_once(self, step: int) -> None:
if self._warmup_enable_logged:
return
logger.info("kv_canary perturb: enabled after warmup window at step=%d", step)
self._warmup_enable_logged = True
def should_run_perturbation(
*,
perturb_name: str,
probability: float,
warmup_gate: WarmupGate,
maybe_inaccurate_forward_batch: Optional["ForwardBatch"],
require_forward_batch: bool = True,
) -> bool:
if probability <= 0.0:
return False
if warmup_gate.is_in_warmup():
return False
if require_forward_batch and maybe_inaccurate_forward_batch is None:
logger.info(
"kv_canary perturb %s: skipped because maybe_inaccurate_forward_batch is unavailable",
perturb_name,
)
return False
return torch.rand((), device="cpu").item() < probability
def pick_target_group(
*,
buffer_groups: tuple[CanaryBufferGroup, ...],
target_kind: TargetGroupKind,
) -> Optional[CanaryBufferGroup]:
"""Filter buffer_groups by target_kind.
Returns None if no group matches.
"""
if target_kind == TargetGroupKind.FULL:
want = PoolKind.FULL
elif target_kind == TargetGroupKind.SWA:
want = PoolKind.SWA
else:
raise ValueError(f"Unsupported target_group_kind: {target_kind!r}")
filtered = [group for group in buffer_groups if group.kind == want]
if not filtered:
return None
pick = random.randrange(len(filtered))
return filtered[pick]
@@ -16,6 +16,8 @@ from sglang.srt.kv_canary.endpoint import (
CanaryEndpoint,
build_endpoints_from_group,
)
from sglang.srt.kv_canary.perturb.config import PerturbConfig
from sglang.srt.kv_canary.perturb.manager import PerturbManager
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 (
@@ -38,6 +40,7 @@ class CanaryManager:
self,
*,
config: CanaryConfig,
perturb_config: PerturbConfig,
buffer_groups: tuple[CanaryBufferGroup, ...],
device: torch.device,
req_to_token_pool: "ReqToTokenPool",
@@ -96,6 +99,13 @@ class CanaryManager:
swa_window_size=self._swa_window_size,
outer_step_counter_getter=self._get_outer_step_counter,
)
self._perturb_manager = PerturbManager(
config=perturb_config,
buffer_groups=self._buffer_groups,
outer_step_counter_getter=self._get_outer_step_counter,
swa_window_size=self._swa_window_size,
sweep_interval=config.sweep_interval,
)
num_sfms = max(1, speculative_num_steps - 1)
self._single_forward_managers: tuple[SingleForwardManager, ...] = tuple(
SingleForwardManager(
@@ -183,6 +193,9 @@ class CanaryManager:
self._single_forward_managers[idx].pre_ops_outside_graph(
maybe_inaccurate_forward_batch=maybe_inaccurate_forward_batch
)
self._perturb_manager.perturb(
maybe_inaccurate_forward_batch=maybe_inaccurate_forward_batch
)
def _post_ops_outside_graph(
self,
@@ -203,6 +216,7 @@ class CanaryManager:
def attach_radix_cache(self, radix_cache: "BasePrefixCache") -> None:
self._sweep_orchestrator.attach_radix_cache(radix_cache)
self._perturb_manager.attach_radix_cache(radix_cache)
def _get_outer_step_counter(self) -> int:
return self._outer_step_counter
@@ -10,6 +10,7 @@ from sglang.srt.kv_canary import endpoint as endpoint_module
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup
from sglang.srt.kv_canary.capacities import CanaryLaunchCapacities
from sglang.srt.kv_canary.config import CanaryConfig, CanaryMode
from sglang.srt.kv_canary.perturb.config import PerturbConfig
from sglang.srt.kv_canary.runner import kernel_launcher as kernel_launcher_module
from sglang.srt.kv_canary.runner.canary_manager import CanaryManager
from sglang.test.kv_canary.fixtures import (
@@ -44,10 +45,20 @@ class RecordingEndpoint:
self.calls.append(kwargs)
def make_perturb_config() -> PerturbConfig:
"""Build a PerturbConfig with every probability pinned to 0 so the
perturb hooks do nothing during unit tests."""
return PerturbConfig(
target_group_kind=None,
warmup_steps=0,
)
def make_manager(
*,
device: torch.device,
config: CanaryConfig | None = None,
perturb_config: PerturbConfig | None = None,
group: CanaryBufferGroup | None = None,
req_pool: SimpleNamespace | None = None,
per_forward_verify_capacity: int = 16,
@@ -55,12 +66,15 @@ def make_manager(
) -> CanaryManager:
if config is None:
config = make_config()
if perturb_config is None:
perturb_config = make_perturb_config()
if group is None:
group = make_buffer_group(device=device)
if req_pool is None:
req_pool = make_req_to_token_pool(device=device, max_reqs=4, max_seq_len=8)
return CanaryManager(
config=config,
perturb_config=perturb_config,
buffer_groups=(group,),
device=device,
req_to_token_pool=req_pool,
@@ -3,6 +3,7 @@ from __future__ import annotations
import time
from typing import Literal, Optional
from sglang.srt.kv_canary.perturb.config import TargetGroupKind
from sglang.test.kv_canary.violation_log_utils import (
assert_no_violation_in_log,
find_violation_in_log,
@@ -19,16 +20,35 @@ class CanaryViolationAssertMixin:
self,
*,
fail_reason: str,
target_group: Optional[TargetGroupKind] = None,
side: _Side = None,
flush_wait_seconds: float = 2.0,
) -> None:
suffix = "" if target_group is None else f"_{target_group.name}"
self.assert_violation_logged_any(
launch_tag_patterns=("HEAD_*", "TAIL_*"),
launch_tag_patterns=(f"HEAD_*{suffix}", f"TAIL_*{suffix}"),
fail_reason=fail_reason,
side=side,
flush_wait_seconds=flush_wait_seconds,
)
def assert_sweep_violation_reported(
self,
*,
fail_reason: str,
target_group: TargetGroupKind,
side: _Side = None,
flush_wait_seconds: float = 2.0,
max_retries: int = 4,
) -> None:
self.assert_violation_logged_any(
launch_tag_patterns=(f"SWEEP_*_{target_group.name}",),
fail_reason=fail_reason,
side=side,
flush_wait_seconds=flush_wait_seconds,
max_retries=max_retries,
)
def assert_any_launch_tag_violation_reported(
self,
*,