chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
# SWA e2e pool sizing for 8 reqs × ~7K prompt + 2K decode, SWA window 1024.
|
||||
# FULL pool = max-total-tokens; must fit 8 × (7000 + 2048) = 72_384 to avoid preempt.
|
||||
# SWA pool = max-total-tokens × ratio;
|
||||
# ≥ 8 × 1024 = 8192 (else deadlock — in-flight footprint exceeds capacity);
|
||||
# < 8 × 7000 = 56_000 (else allocator never recycles → swa_full_idx_divergence stays 0).
|
||||
# Pick FULL=81920 (72_384 + headroom), SWA=16384 (2× in-flight floor, well under 56K).
|
||||
SWA_POOL_SERVER_ARGS: Final[tuple[str, ...]] = (
|
||||
"--max-total-tokens",
|
||||
"81920",
|
||||
"--swa-full-tokens-ratio",
|
||||
"0.2",
|
||||
)
|
||||
|
||||
DSV4_DEEPEP_CONFIG: Final[str] = (
|
||||
'{"normal_dispatch":{"num_sms":96},"normal_combine":{"num_sms":96}}'
|
||||
)
|
||||
|
||||
DSV4_POOL_SERVER_ARGS: Final[tuple[str, ...]] = (
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--cuda-graph-max-bs-decode",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DSV4_DEEPEP_CONFIG,
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
)
|
||||
|
||||
DSV4_POOL_SERVER_ENV: Final[dict[str, str]] = {
|
||||
"SGLANG_DSV4_FP4_EXPERTS": "0",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import string
|
||||
import time
|
||||
from typing import ClassVar, Literal, Optional
|
||||
|
||||
from sglang.srt.kv_canary.config import CanaryMode
|
||||
from sglang.srt.kv_canary.runner.swa_divergence import SwaDivergenceLog
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.kv_canary.mode_config import _MODE_CONFIGS, _ModeConfig
|
||||
from sglang.test.kv_canary.utils import build_canary_server_args, post_parallel_generate
|
||||
from sglang.test.kv_canary.violation_assert_mixin import CanaryViolationAssertMixin
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
# Long prompt body shared by all canary e2e tests. The repetition count is chosen
|
||||
# so the tokenised prompt comfortably exceeds the SWA sliding window of swa-mode
|
||||
# fixtures (gemma-4-E2B); short prompts would never exercise the SWA-windowed
|
||||
# verify path. Token count is roughly 7k after BPE.
|
||||
_LONG_PROMPT_BODY = ("The quick brown fox jumps over the lazy dog. " * 700).strip()
|
||||
_UNIQUE_PROMPT_FIRST_CHARS = string.ascii_letters + string.digits
|
||||
|
||||
|
||||
class CapturedServerE2EBase(CanaryViolationAssertMixin, CustomTestCase):
|
||||
process: ClassVar[Optional[object]] = None
|
||||
base_url: ClassVar[str] = DEFAULT_URL_FOR_TEST
|
||||
_stdout_buf: ClassVar[Optional[io.StringIO]] = None
|
||||
_stderr_buf: ClassVar[Optional[io.StringIO]] = None
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
if cls.process is not None:
|
||||
kill_process_tree(cls.process.pid)
|
||||
for buf in (cls._stdout_buf, cls._stderr_buf):
|
||||
if buf is not None:
|
||||
buf.close()
|
||||
cls._stdout_buf = None
|
||||
cls._stderr_buf = None
|
||||
|
||||
def _captured_log_text(
|
||||
self, side: Optional[Literal["prefill", "decode"]] = None
|
||||
) -> str:
|
||||
stdout_text = (
|
||||
self._stdout_buf.getvalue() if self._stdout_buf is not None else ""
|
||||
)
|
||||
stderr_text = (
|
||||
self._stderr_buf.getvalue() if self._stderr_buf is not None else ""
|
||||
)
|
||||
return stdout_text + stderr_text
|
||||
|
||||
def assert_log_contains(self, substring: str) -> None:
|
||||
log_text = self._captured_log_text()
|
||||
if substring not in log_text:
|
||||
raise AssertionError(
|
||||
f"Expected substring {substring!r} not found in captured log. "
|
||||
f"Log tail:\n{log_text[-2000:]}"
|
||||
)
|
||||
|
||||
|
||||
class CanaryE2EBase(CapturedServerE2EBase):
|
||||
model_mode: ClassVar[Literal["mha", "swa", "dsv4"]]
|
||||
kv_canary_mode: ClassVar[CanaryMode]
|
||||
extra_env: ClassVar[dict[str, str]] = {}
|
||||
extra_server_args: ClassVar[tuple[str, ...]] = ()
|
||||
use_unique_prompts: ClassVar[bool] = False
|
||||
# SWA divergence assertions need slot recycling across batches; setting > 1 makes the
|
||||
# test methods send N sequential batches so the SWA allocator's full→swa index mapping
|
||||
# diverges from identity. Default 1 keeps MHA tests fast.
|
||||
workload_n_batches: ClassVar[int] = 1
|
||||
|
||||
_cfg: ClassVar[Optional[_ModeConfig]] = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls._cfg = _MODE_CONFIGS[cls.model_mode]
|
||||
server_env = os.environ.copy()
|
||||
server_env.setdefault("SGLANG_KV_CANARY_ENABLE_VERIFY_TOKEN_ASSERT", "1")
|
||||
server_env.update(cls.extra_env)
|
||||
if cls.model_mode == "swa":
|
||||
server_env.setdefault(
|
||||
"SGLANG_KV_CANARY_SWA_DIVERGENCE_STATS_INTERVAL", "20"
|
||||
)
|
||||
# SWA mode uses google/gemma-4-E2B-it, whose forward does a
|
||||
# ``positions += 1`` in-place. canary's WRITE/VERIFY require
|
||||
# forward_batch.positions to stay 0-indexed, so flip the gemma
|
||||
# path to out-of-place shift for these tests.
|
||||
server_env.setdefault("SGLANG_GEMMA_OUT_OF_PLACE_POSITION_MUTATION", "1")
|
||||
|
||||
cls._stdout_buf = io.StringIO()
|
||||
cls._stderr_buf = io.StringIO()
|
||||
|
||||
server_args = build_canary_server_args(
|
||||
kv_canary_mode=cls.kv_canary_mode,
|
||||
mode_cfg=cls._cfg,
|
||||
extra_server_args=(
|
||||
"--max-total-tokens",
|
||||
"65536",
|
||||
"--skip-server-warmup",
|
||||
*cls.extra_server_args,
|
||||
),
|
||||
)
|
||||
cls.process = popen_launch_server(
|
||||
cls._cfg.model_path,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=server_args,
|
||||
env=server_env,
|
||||
return_stdout_stderr=(cls._stdout_buf, cls._stderr_buf),
|
||||
)
|
||||
|
||||
def make_prompts(self, n: int) -> list[str]:
|
||||
if self.use_unique_prompts:
|
||||
return _make_unique_prompts(n)
|
||||
return [_LONG_PROMPT_BODY] * n
|
||||
|
||||
def send_parallel_requests(
|
||||
self,
|
||||
n: int = 8,
|
||||
*,
|
||||
assert_all_success: bool = True,
|
||||
max_new_tokens: int = 2048,
|
||||
timeout: float = 240.0,
|
||||
ignore_eos: Optional[bool] = None,
|
||||
) -> list[dict]:
|
||||
"""Fan out n parallel /generate requests; return list of response dicts."""
|
||||
if ignore_eos is None:
|
||||
ignore_eos = self.model_mode == "swa"
|
||||
results = post_parallel_generate(
|
||||
url=self.base_url + "/generate",
|
||||
prompts=self.make_prompts(n),
|
||||
max_new_tokens=max_new_tokens,
|
||||
timeout=timeout,
|
||||
ignore_eos=ignore_eos,
|
||||
)
|
||||
if assert_all_success:
|
||||
for result in results:
|
||||
self.assertEqual(result.get("status_code"), 200, result)
|
||||
return results
|
||||
|
||||
def maybe_assert_swa_divergence_observed(self) -> None:
|
||||
if self.model_mode == "swa":
|
||||
self.assert_swa_divergence_observed()
|
||||
|
||||
def assert_swa_divergence_observed(
|
||||
self,
|
||||
*,
|
||||
min_swa_out_of_window_tokens: int = 1,
|
||||
min_swa_full_idx_divergence: int = 1,
|
||||
require_verify_lag: bool = True,
|
||||
flush_wait_seconds: float = 3.0,
|
||||
max_retries: int = 10,
|
||||
) -> None:
|
||||
"""Assert that the SWA path was genuinely exercised.
|
||||
|
||||
Three signals must all hold:
|
||||
- ``swa_out_of_window_tokens >= 1``: at least one token has slid out of the
|
||||
sliding window (its SWA mapping is 0). This only appears once a request decodes
|
||||
past the window, so the window evicts — proves the SWA window slide actually ran.
|
||||
- ``swa_full_idx_divergence >= 1``: SWA pool has actually remapped at least one
|
||||
slot to a non-identity index (i.e. real slot reuse / eviction occurred). The
|
||||
workload must drive SWA pool pressure for this to fire — required because the
|
||||
"pool reuse" path is the one production hits under sustained long-context
|
||||
traffic, and we must keep it covered.
|
||||
- ``verify_swa < verify_full``: SWA verify kernel processed fewer tokens than
|
||||
FULL — proves both kernel groups ran and the window short-circuited SWA.
|
||||
|
||||
The first two signals are checked as the *peak* across all sampled forwards, not
|
||||
only the last sample. The divergence reporter snapshots one live forward batch per
|
||||
interval; under PP it snapshots a single micro-batch, which may hold only in-window
|
||||
requests even when another micro-batch diverged. "Was the SWA path ever exercised?"
|
||||
is a max-over-samples question, so a trailing in-window sample must not mask an
|
||||
earlier diverging one. ``verify_swa``/``verify_full`` are monotonic running totals,
|
||||
so the lag check reads the last sample.
|
||||
"""
|
||||
samples: list[tuple[SwaDivergenceLog, str]] = []
|
||||
for _ in range(max_retries):
|
||||
time.sleep(flush_wait_seconds)
|
||||
samples = SwaDivergenceLog.find_all(self._captured_log_text())
|
||||
if samples:
|
||||
break
|
||||
|
||||
if not samples:
|
||||
raise AssertionError(
|
||||
"No kv_canary swa_divergence line found in server log after "
|
||||
f"{max_retries} retries (wait={flush_wait_seconds}s each). "
|
||||
f"Log tail:\n{self._captured_log_text()[-2000:]}"
|
||||
)
|
||||
|
||||
peak_out_of_window = max(p.swa_out_of_window_tokens for p, _ in samples)
|
||||
peak_full_idx_divergence = max(p.swa_full_idx_divergence for p, _ in samples)
|
||||
last_parsed, last_line = samples[-1]
|
||||
|
||||
if peak_out_of_window < min_swa_out_of_window_tokens:
|
||||
raise AssertionError(
|
||||
f"SWA path not exercised: peak swa_out_of_window_tokens={peak_out_of_window} "
|
||||
f"< min={min_swa_out_of_window_tokens} across {len(samples)} samples. "
|
||||
f"Last line: {last_line}"
|
||||
)
|
||||
if peak_full_idx_divergence < min_swa_full_idx_divergence:
|
||||
raise AssertionError(
|
||||
f"SWA pool reuse not exercised: peak swa_full_idx_divergence={peak_full_idx_divergence} "
|
||||
f"< min={min_swa_full_idx_divergence} across {len(samples)} samples. The workload "
|
||||
f"did not drive enough SWA pool pressure to force slot remap. Last line: {last_line}"
|
||||
)
|
||||
if require_verify_lag and not (
|
||||
last_parsed.verify_swa < last_parsed.verify_full
|
||||
):
|
||||
raise AssertionError(
|
||||
f"SWA path not exercised: verify_swa={last_parsed.verify_swa} "
|
||||
f"not strictly less than verify_full={last_parsed.verify_full}. "
|
||||
f"Line: {last_line}"
|
||||
)
|
||||
|
||||
|
||||
def _make_unique_prompts(n: int) -> list[str]:
|
||||
if n > len(_UNIQUE_PROMPT_FIRST_CHARS):
|
||||
raise ValueError(
|
||||
f"unique prompt count {n} exceeds supported count "
|
||||
f"{len(_UNIQUE_PROMPT_FIRST_CHARS)}"
|
||||
)
|
||||
|
||||
return [
|
||||
(
|
||||
f"{_UNIQUE_PROMPT_FIRST_CHARS[i]}"
|
||||
f"{hex(i * 0x9E3779B1 & 0xFFFFFFFF)[2:]} "
|
||||
f"{_LONG_PROMPT_BODY}"
|
||||
)
|
||||
for i in range(n)
|
||||
]
|
||||
@@ -0,0 +1,291 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from types import SimpleNamespace
|
||||
from typing import List, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.kv_canary import consts
|
||||
from sglang.jit_kernel.kv_canary.verify import CANARY_SLOT_BYTES, RealKvSource
|
||||
from sglang.srt.kv_canary.buffer_group import CanaryBufferGroup, PoolKind
|
||||
from sglang.srt.kv_canary.config import CanaryConfig, CanaryMode
|
||||
from sglang.srt.kv_canary.pool_patcher.adapters.mha import attach_mha
|
||||
from sglang.srt.kv_canary.pool_patcher.adapters.swa import attach_swa
|
||||
from sglang.srt.kv_canary.pool_patcher.api import register_pool_attacher
|
||||
from sglang.srt.mem_cache.radix_cache import RadixCache, TreeNode
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
||||
|
||||
DEFAULT_DEVICE: torch.device = torch.device("cuda")
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeMHAPool:
|
||||
layer_num: int
|
||||
k_buffer: List[torch.Tensor]
|
||||
v_buffer: List[torch.Tensor]
|
||||
page_size: int = 1
|
||||
|
||||
def get_contiguous_buf_infos(self):
|
||||
ptrs = [b.data_ptr() for b in self.k_buffer] + [
|
||||
b.data_ptr() for b in self.v_buffer
|
||||
]
|
||||
lens = [b.nbytes for b in self.k_buffer] + [b.nbytes for b in self.v_buffer]
|
||||
item_lens = [b[0].nbytes * self.page_size for b in self.k_buffer] + [
|
||||
b[0].nbytes * self.page_size for b in self.v_buffer
|
||||
]
|
||||
return ptrs, lens, item_lens
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeSwaSubPool:
|
||||
k_buffer: List[torch.Tensor]
|
||||
v_buffer: List[torch.Tensor]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeSWAPool:
|
||||
full_kv_pool: object
|
||||
swa_kv_pool: object
|
||||
full_to_swa_index_mapping: torch.Tensor
|
||||
page_size: int = 1
|
||||
|
||||
def get_contiguous_buf_infos(self):
|
||||
return _kv_buf_infos(
|
||||
k_buffer=self.full_kv_pool.k_buffer,
|
||||
v_buffer=self.full_kv_pool.v_buffer,
|
||||
page_size=self.page_size,
|
||||
)
|
||||
|
||||
def get_state_buf_infos(self):
|
||||
return _kv_buf_infos(
|
||||
k_buffer=self.swa_kv_pool.k_buffer,
|
||||
v_buffer=self.swa_kv_pool.v_buffer,
|
||||
page_size=self.page_size,
|
||||
)
|
||||
|
||||
|
||||
def _kv_buf_infos(
|
||||
*,
|
||||
k_buffer: List[torch.Tensor],
|
||||
v_buffer: List[torch.Tensor],
|
||||
page_size: int,
|
||||
) -> tuple:
|
||||
ptrs = [b.data_ptr() for b in k_buffer] + [b.data_ptr() for b in v_buffer]
|
||||
lens = [b.nbytes for b in k_buffer] + [b.nbytes for b in v_buffer]
|
||||
item_lens = [b[0].nbytes * page_size for b in k_buffer] + [
|
||||
b[0].nbytes * page_size for b in v_buffer
|
||||
]
|
||||
return ptrs, lens, item_lens
|
||||
|
||||
|
||||
def make_mha_pool(
|
||||
device: torch.device = DEFAULT_DEVICE,
|
||||
*,
|
||||
num_slots: int = 16,
|
||||
dim: int = 8,
|
||||
layer_num: int = 2,
|
||||
) -> FakeMHAPool:
|
||||
k_layers = [
|
||||
torch.zeros(num_slots, dim, dtype=torch.float16, device=device)
|
||||
for _ in range(layer_num)
|
||||
]
|
||||
v_layers = [
|
||||
torch.zeros(num_slots, dim, dtype=torch.float16, device=device)
|
||||
for _ in range(layer_num)
|
||||
]
|
||||
return FakeMHAPool(layer_num=layer_num, k_buffer=k_layers, v_buffer=v_layers)
|
||||
|
||||
|
||||
def make_swa_pool(
|
||||
device: torch.device = DEFAULT_DEVICE,
|
||||
*,
|
||||
full_slots: int = 16,
|
||||
swa_slots: int = 8,
|
||||
dim: int = 8,
|
||||
layer_num: int = 1,
|
||||
) -> FakeSWAPool:
|
||||
full = FakeSwaSubPool(
|
||||
k_buffer=[
|
||||
torch.zeros(full_slots, dim, dtype=torch.float16, device=device)
|
||||
for _ in range(layer_num)
|
||||
],
|
||||
v_buffer=[
|
||||
torch.zeros(full_slots, dim, dtype=torch.float16, device=device)
|
||||
for _ in range(layer_num)
|
||||
],
|
||||
)
|
||||
swa = FakeSwaSubPool(
|
||||
k_buffer=[
|
||||
torch.zeros(swa_slots, dim, dtype=torch.float16, device=device)
|
||||
for _ in range(layer_num)
|
||||
],
|
||||
v_buffer=[
|
||||
torch.zeros(swa_slots, dim, dtype=torch.float16, device=device)
|
||||
for _ in range(layer_num)
|
||||
],
|
||||
)
|
||||
lut = torch.full((full_slots + 1,), -1, dtype=torch.int64, device=device)
|
||||
lut[:swa_slots] = torch.arange(swa_slots, dtype=torch.int64, device=device)
|
||||
return FakeSWAPool(
|
||||
full_kv_pool=full, swa_kv_pool=swa, full_to_swa_index_mapping=lut
|
||||
)
|
||||
|
||||
|
||||
def make_base_config() -> CanaryConfig:
|
||||
return CanaryConfig(
|
||||
mode=CanaryMode.RAISE,
|
||||
ring_capacity=1024,
|
||||
sweep_interval=0,
|
||||
real_kv_hash_mode=consts.RealKvHashMode.NONE,
|
||||
enable_write_input_assert=False,
|
||||
enable_verify_token_assert=True,
|
||||
stats_print_every_n_steps=100,
|
||||
)
|
||||
|
||||
|
||||
def make_req_to_token_pool(
|
||||
device: torch.device = DEFAULT_DEVICE,
|
||||
*,
|
||||
max_reqs: int = 8,
|
||||
max_seq_len: int = 32,
|
||||
) -> SimpleNamespace:
|
||||
req_to_token = torch.zeros(max_reqs, max_seq_len, dtype=torch.int32, device=device)
|
||||
return SimpleNamespace(
|
||||
req_to_token=req_to_token, size=max_reqs, max_context_len=max_seq_len
|
||||
)
|
||||
|
||||
|
||||
def make_forward_batch(
|
||||
device: torch.device = DEFAULT_DEVICE,
|
||||
*,
|
||||
bs: int = 2,
|
||||
seq_lens_list: tuple[int, ...] = (3, 4),
|
||||
req_pool_indices: Optional[torch.Tensor] = None,
|
||||
seq_lens: Optional[torch.Tensor] = None,
|
||||
seq_lens_sum: Optional[int] = None,
|
||||
extend_prefix_lens: Optional[torch.Tensor] = None,
|
||||
extend_prefix_lens_cpu: Optional[list] = None,
|
||||
extend_seq_lens: Optional[torch.Tensor] = None,
|
||||
extend_seq_lens_cpu: Optional[list] = None,
|
||||
is_extend: bool = False,
|
||||
is_target_verify: bool = False,
|
||||
is_draft_extend_v2: bool = False,
|
||||
spec_info: Optional[object] = None,
|
||||
input_ids: Optional[torch.Tensor] = None,
|
||||
positions: Optional[torch.Tensor] = None,
|
||||
out_cache_loc: Optional[torch.Tensor] = None,
|
||||
num_token_non_padded_cpu: Optional[int] = None,
|
||||
) -> SimpleNamespace:
|
||||
seq_lens_default = list(seq_lens_list[:bs])
|
||||
if req_pool_indices is None:
|
||||
req_pool_indices = torch.tensor([1, 2][:bs], dtype=torch.int64, device=device)
|
||||
if seq_lens is None:
|
||||
seq_lens = torch.tensor(seq_lens_default, dtype=torch.int32, device=device)
|
||||
if seq_lens_sum is None:
|
||||
seq_lens_sum = int(sum(seq_lens_default))
|
||||
if input_ids is None:
|
||||
input_ids = torch.zeros(bs, dtype=torch.int32, device=device)
|
||||
if positions is None:
|
||||
# Default to decode-canonical: positions = seq_lens - 1 (one-token-per-req decode write
|
||||
# at the post-bump tail). Plan input derives decode prefix_lens from positions
|
||||
# directly, so the default must keep parity.
|
||||
positions = (seq_lens.to(torch.int64) - 1).clamp(min=0).to(torch.int32)
|
||||
if out_cache_loc is None:
|
||||
out_cache_loc = torch.zeros(bs, dtype=torch.int32, device=device)
|
||||
|
||||
if is_extend:
|
||||
forward_mode = ForwardMode.EXTEND
|
||||
elif is_target_verify:
|
||||
forward_mode = ForwardMode.TARGET_VERIFY
|
||||
elif is_draft_extend_v2:
|
||||
forward_mode = ForwardMode.DRAFT_EXTEND_V2
|
||||
else:
|
||||
forward_mode = ForwardMode.DECODE
|
||||
return SimpleNamespace(
|
||||
forward_mode=forward_mode,
|
||||
batch_size=bs,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
seq_lens_sum=seq_lens_sum,
|
||||
extend_prefix_lens=extend_prefix_lens,
|
||||
extend_prefix_lens_cpu=extend_prefix_lens_cpu,
|
||||
extend_seq_lens=extend_seq_lens,
|
||||
extend_seq_lens_cpu=extend_seq_lens_cpu,
|
||||
spec_info=spec_info,
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
out_cache_loc=out_cache_loc,
|
||||
num_token_non_padded_cpu=num_token_non_padded_cpu,
|
||||
req_all_ids_flat=None,
|
||||
req_all_ids_lens=None,
|
||||
)
|
||||
|
||||
|
||||
def make_buffer_group(
|
||||
*,
|
||||
device: torch.device = DEFAULT_DEVICE,
|
||||
kind: PoolKind = PoolKind.FULL,
|
||||
has_v: bool = True,
|
||||
has_real_kv: bool = False,
|
||||
real_kv_source: Optional[RealKvSource] = None,
|
||||
swa_index_lut: Optional[torch.Tensor] = None,
|
||||
num_slots: int = 4,
|
||||
kv_token_id_vs_position_offset: int = 0,
|
||||
) -> CanaryBufferGroup:
|
||||
def _zero() -> torch.Tensor:
|
||||
return torch.zeros(
|
||||
num_slots, CANARY_SLOT_BYTES, dtype=torch.uint8, device=device
|
||||
)
|
||||
|
||||
if has_real_kv:
|
||||
source = real_kv_source or RealKvSource(
|
||||
tensor=torch.zeros(num_slots, 16, dtype=torch.uint8, device=device),
|
||||
page_size=1,
|
||||
num_bytes_per_token=16,
|
||||
read_bytes=16,
|
||||
)
|
||||
real_kv_sources = (source,)
|
||||
else:
|
||||
real_kv_sources = ()
|
||||
|
||||
return CanaryBufferGroup(
|
||||
kind=kind,
|
||||
k_head=_zero(),
|
||||
k_tail=_zero(),
|
||||
v_head=_zero() if has_v else None,
|
||||
v_tail=_zero() if has_v else None,
|
||||
real_kv_sources_k=real_kv_sources,
|
||||
real_kv_sources_v=real_kv_sources if has_v else (),
|
||||
swa_index_lut=swa_index_lut,
|
||||
kv_token_id_vs_position_offset=kv_token_id_vs_position_offset,
|
||||
)
|
||||
|
||||
|
||||
def make_radix_cache(
|
||||
slot_lists: List[List[int]], device: torch.device = DEFAULT_DEVICE
|
||||
):
|
||||
cache = RadixCache.__new__(RadixCache)
|
||||
cache.device = device
|
||||
cache.page_size = 1
|
||||
cache.disable = False
|
||||
|
||||
root = TreeNode()
|
||||
root.value = torch.tensor(
|
||||
slot_lists[0] if slot_lists else [], dtype=torch.int32, device=device
|
||||
)
|
||||
cache.root_node = root
|
||||
|
||||
current = root
|
||||
for child_slots in slot_lists[1:]:
|
||||
child = TreeNode()
|
||||
child.value = torch.tensor(child_slots, dtype=torch.int32, device=device)
|
||||
child.parent = current
|
||||
current.children[child.id] = child
|
||||
current = child
|
||||
|
||||
return cache
|
||||
|
||||
|
||||
register_pool_attacher(FakeMHAPool, attach_mha)
|
||||
register_pool_attacher(FakeSWAPool, attach_swa)
|
||||
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, kw_only=True)
|
||||
class _ModeConfig:
|
||||
model_path: str
|
||||
json_model_override_args: Optional[str] = None
|
||||
|
||||
|
||||
_MODE_CONFIGS: dict[str, _ModeConfig] = {
|
||||
"mha": _ModeConfig(
|
||||
model_path="Qwen/Qwen3-0.6B",
|
||||
),
|
||||
"swa": _ModeConfig(
|
||||
model_path="google/gemma-4-E2B-it",
|
||||
),
|
||||
"dsv4": _ModeConfig(
|
||||
model_path="deepseek-ai/DeepSeek-V4-Flash",
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import ClassVar, Literal, Optional
|
||||
|
||||
from sglang.srt.kv_canary.config import CanaryMode
|
||||
from sglang.test.kv_canary.mode_config import _MODE_CONFIGS, _ModeConfig
|
||||
from sglang.test.kv_canary.utils import build_canary_server_args, post_parallel_generate
|
||||
from sglang.test.kv_canary.violation_assert_mixin import CanaryViolationAssertMixin
|
||||
from sglang.test.server_fixtures.disaggregation_fixture import (
|
||||
PDDisaggregationServerBase,
|
||||
)
|
||||
|
||||
_SHORT_PROMPT_BODY = ("The quick brown fox jumps over the lazy dog. " * 8).strip()
|
||||
|
||||
|
||||
class CanaryPDFixture(CanaryViolationAssertMixin, PDDisaggregationServerBase):
|
||||
capture_per_side_logs = True
|
||||
|
||||
model_mode: ClassVar[Literal["mha", "swa", "dsv4"]]
|
||||
kv_canary_mode: ClassVar[CanaryMode] = CanaryMode.LOG
|
||||
extra_server_args: ClassVar[tuple[str, ...]] = (
|
||||
"--kv-canary-real-data",
|
||||
"partial",
|
||||
"--skip-server-warmup",
|
||||
)
|
||||
|
||||
_cfg: ClassVar[Optional[_ModeConfig]] = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls._cfg = _MODE_CONFIGS[cls.model_mode]
|
||||
cls.model = cls._cfg.model_path
|
||||
|
||||
canary_args = build_canary_server_args(
|
||||
kv_canary_mode=cls.kv_canary_mode,
|
||||
mode_cfg=cls._cfg,
|
||||
extra_server_args=cls.extra_server_args,
|
||||
)
|
||||
cls.extra_prefill_args = list(canary_args)
|
||||
cls.extra_decode_args = list(canary_args)
|
||||
if cls.model_mode == "swa":
|
||||
# SWA mode uses google/gemma-4-E2B-it, whose forward does a
|
||||
# ``positions += 1`` in-place. canary's WRITE/VERIFY require
|
||||
# forward_batch.positions to stay 0-indexed, so flip the gemma
|
||||
# path to out-of-place shift for these tests.
|
||||
cls.extra_prefill_env = {
|
||||
**cls.extra_prefill_env,
|
||||
"SGLANG_GEMMA_OUT_OF_PLACE_POSITION_MUTATION": "1",
|
||||
}
|
||||
cls.extra_decode_env = {
|
||||
**cls.extra_decode_env,
|
||||
"SGLANG_GEMMA_OUT_OF_PLACE_POSITION_MUTATION": "1",
|
||||
}
|
||||
cls.launch_all()
|
||||
|
||||
def send_parallel_short_requests(
|
||||
self,
|
||||
n: int,
|
||||
*,
|
||||
assert_all_success: bool = True,
|
||||
max_new_tokens: int = 100,
|
||||
timeout: float = 60.0,
|
||||
distinct_prompts: bool = False,
|
||||
) -> list[dict]:
|
||||
if distinct_prompts:
|
||||
# Diverge at the very first token (request index before the per-call
|
||||
# nonce) so requests share no radix-dedupable prefix beyond a
|
||||
# tokenizer-added BOS, and retries never hit earlier attempts' cache.
|
||||
nonce = uuid.uuid4().hex[:8]
|
||||
prompts = [f"{i} {nonce} {_SHORT_PROMPT_BODY}" for i in range(n)]
|
||||
else:
|
||||
prompts = [_SHORT_PROMPT_BODY] * n
|
||||
results = post_parallel_generate(
|
||||
url=self.lb_url + "/generate",
|
||||
prompts=prompts,
|
||||
max_new_tokens=max_new_tokens,
|
||||
timeout=timeout,
|
||||
)
|
||||
if assert_all_success:
|
||||
for result in results:
|
||||
self.assertEqual(result.get("status_code"), 200, result)
|
||||
return results
|
||||
|
||||
def _captured_log_text(
|
||||
self, side: Optional[Literal["prefill", "decode"]] = None
|
||||
) -> str:
|
||||
if side == "prefill":
|
||||
stdout_buf = type(self)._prefill_stdout_buf
|
||||
stderr_buf = type(self)._prefill_stderr_buf
|
||||
elif side == "decode":
|
||||
stdout_buf = type(self)._decode_stdout_buf
|
||||
stderr_buf = type(self)._decode_stderr_buf
|
||||
else:
|
||||
raise ValueError(f"Unsupported side={side!r}")
|
||||
stdout_text = stdout_buf.getvalue() if stdout_buf is not None else ""
|
||||
stderr_text = stderr_buf.getvalue() if stderr_buf is not None else ""
|
||||
return stdout_text + stderr_text
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
from sglang.test.kv_canary.consts import SWA_POOL_SERVER_ARGS
|
||||
from sglang.test.kv_canary.e2e_base import CanaryE2EBase
|
||||
|
||||
PP_SIZE: int = 2
|
||||
|
||||
|
||||
class CanaryPPFixture(CanaryE2EBase):
|
||||
|
||||
model_mode: ClassVar[str] = "swa"
|
||||
workload_n_batches: ClassVar[int] = 2
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.extra_server_args = (
|
||||
"--pp-size",
|
||||
str(PP_SIZE),
|
||||
"--disable-cuda-graph",
|
||||
*SWA_POOL_SERVER_ARGS,
|
||||
*cls.extra_server_args,
|
||||
)
|
||||
super().setUpClass()
|
||||
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.kv_canary.consts import RealKvHashMode
|
||||
from sglang.jit_kernel.kv_canary.verify import CanaryLaunchTag
|
||||
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 (
|
||||
DEFAULT_DEVICE,
|
||||
make_buffer_group,
|
||||
make_req_to_token_pool,
|
||||
)
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
|
||||
def make_config(
|
||||
*,
|
||||
mode: CanaryMode = CanaryMode.RAISE,
|
||||
ring_capacity: int = 1024,
|
||||
sweep_interval: int = 0,
|
||||
real_kv_hash_mode: RealKvHashMode = RealKvHashMode.NONE,
|
||||
enable_write_input_assert: bool = False,
|
||||
enable_verify_token_assert: bool = True,
|
||||
stats_print_every_n_steps: int = 100,
|
||||
) -> CanaryConfig:
|
||||
return CanaryConfig(
|
||||
mode=mode,
|
||||
ring_capacity=ring_capacity,
|
||||
sweep_interval=sweep_interval,
|
||||
real_kv_hash_mode=real_kv_hash_mode,
|
||||
enable_write_input_assert=enable_write_input_assert,
|
||||
enable_verify_token_assert=enable_verify_token_assert,
|
||||
stats_print_every_n_steps=stats_print_every_n_steps,
|
||||
)
|
||||
|
||||
|
||||
class RecordingEndpoint:
|
||||
def __init__(self, *, kernel_kind: CanaryLaunchTag) -> None:
|
||||
self.kernel_kind = kernel_kind
|
||||
self.calls: list[dict[str, object]] = []
|
||||
|
||||
def launch_per_forward(self, **kwargs: object) -> None:
|
||||
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(
|
||||
req_to_token_prob=0.0,
|
||||
real_kv_used_prob=0.0,
|
||||
real_kv_unused_cache_prob=0.0,
|
||||
real_kv_post_forward_prob=0.0,
|
||||
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,
|
||||
speculative_num_steps: int = 1,
|
||||
) -> 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,
|
||||
launch_capacities=CanaryLaunchCapacities(
|
||||
per_forward_verify_capacity=per_forward_verify_capacity,
|
||||
per_forward_write_req_capacity=2,
|
||||
per_forward_write_entry_capacity=8,
|
||||
),
|
||||
speculative_num_steps=speculative_num_steps,
|
||||
)
|
||||
|
||||
|
||||
class CanaryManagerTestCase(CustomTestCase):
|
||||
def setUp(self) -> None:
|
||||
self.device = DEFAULT_DEVICE
|
||||
# Stub plan/verify/write kernels so CPU runs don't need CUDA JIT.
|
||||
self._patchers = [
|
||||
patch.object(
|
||||
kernel_launcher_module,
|
||||
"launch_canary_plan_kernels",
|
||||
lambda **kwargs: None,
|
||||
),
|
||||
patch.object(
|
||||
endpoint_module, "launch_canary_verify_kernel", lambda **kwargs: None
|
||||
),
|
||||
patch.object(
|
||||
endpoint_module, "launch_canary_write_kernel", lambda **kwargs: None
|
||||
),
|
||||
]
|
||||
for patcher in self._patchers:
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.kv_canary.config import CanaryMode
|
||||
from sglang.test.kv_canary.mode_config import _ModeConfig
|
||||
|
||||
|
||||
def build_canary_server_args(
|
||||
*,
|
||||
kv_canary_mode: CanaryMode,
|
||||
mode_cfg: _ModeConfig,
|
||||
extra_server_args: tuple[str, ...] = (),
|
||||
) -> list[str]:
|
||||
args = [
|
||||
"--kv-canary",
|
||||
kv_canary_mode.value,
|
||||
"--disable-piecewise-cuda-graph",
|
||||
"--context-length",
|
||||
"16384",
|
||||
*extra_server_args,
|
||||
]
|
||||
if mode_cfg.json_model_override_args is not None:
|
||||
args.extend(["--json-model-override-args", mode_cfg.json_model_override_args])
|
||||
return args
|
||||
|
||||
|
||||
def post_parallel_generate(
|
||||
*,
|
||||
url: str,
|
||||
prompts: list[str],
|
||||
max_new_tokens: int,
|
||||
timeout: float,
|
||||
ignore_eos: bool = False,
|
||||
) -> list[dict]:
|
||||
def _send(prompt: str) -> dict:
|
||||
try:
|
||||
resp = requests.post(
|
||||
url,
|
||||
json={
|
||||
"text": prompt,
|
||||
"sampling_params": {
|
||||
"max_new_tokens": max_new_tokens,
|
||||
"temperature": 0.0,
|
||||
"ignore_eos": ignore_eos,
|
||||
},
|
||||
},
|
||||
timeout=timeout,
|
||||
)
|
||||
return {"status_code": resp.status_code, "body": resp.text}
|
||||
except requests.RequestException as exc:
|
||||
return {"status_code": -1, "error": repr(exc)}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max(1, len(prompts))) as pool:
|
||||
return list(pool.map(_send, prompts))
|
||||
@@ -0,0 +1,145 @@
|
||||
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,
|
||||
)
|
||||
|
||||
_Side = Optional[Literal["prefill", "decode"]]
|
||||
|
||||
|
||||
class CanaryViolationAssertMixin:
|
||||
def _captured_log_text(self, side: _Side = None) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def assert_per_forward_violation_reported(
|
||||
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=(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,
|
||||
*,
|
||||
fail_reason: str,
|
||||
side: _Side = None,
|
||||
flush_wait_seconds: float = 3.0,
|
||||
max_retries: int = 10,
|
||||
) -> None:
|
||||
self.assert_violation_logged_any(
|
||||
launch_tag_patterns=("*",),
|
||||
fail_reason=fail_reason,
|
||||
side=side,
|
||||
flush_wait_seconds=flush_wait_seconds,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
||||
def assert_any_launch_tag_violation_absent(
|
||||
self, *, fail_reason: str, side: _Side = None
|
||||
) -> None:
|
||||
self.assert_no_violation_matching(
|
||||
launch_tag_patterns=("*",), fail_reason=fail_reason, side=side
|
||||
)
|
||||
|
||||
def assert_violation_logged_any(
|
||||
self,
|
||||
*,
|
||||
launch_tag_patterns: tuple[str, ...],
|
||||
fail_reason: str,
|
||||
side: _Side = None,
|
||||
flush_wait_seconds: float = 2.0,
|
||||
max_retries: int = 1,
|
||||
) -> None:
|
||||
log_text = ""
|
||||
for _ in range(max_retries):
|
||||
time.sleep(flush_wait_seconds)
|
||||
log_text = self._captured_log_text(side)
|
||||
if find_violation_in_log(
|
||||
log_text,
|
||||
launch_tag_patterns=launch_tag_patterns,
|
||||
fail_reason=fail_reason,
|
||||
):
|
||||
return
|
||||
side_label = "" if side is None else f" on side={side}"
|
||||
other_side_diag = ""
|
||||
if side in ("prefill", "decode"):
|
||||
other = "decode" if side == "prefill" else "prefill"
|
||||
try:
|
||||
other_text = self._captured_log_text(other)
|
||||
other_match = find_violation_in_log(
|
||||
other_text,
|
||||
launch_tag_patterns=launch_tag_patterns,
|
||||
fail_reason=fail_reason,
|
||||
)
|
||||
other_side_diag = (
|
||||
f"\n[diag] other side ({other}) buf len={len(other_text)} "
|
||||
f"contains_match={other_match}"
|
||||
)
|
||||
except (NotImplementedError, ValueError):
|
||||
pass
|
||||
raise AssertionError(
|
||||
f"No canary violation matching launch_tag_patterns={launch_tag_patterns!r} "
|
||||
f"fail_reason={fail_reason!r}{side_label} after max_retries={max_retries} "
|
||||
f"(wait={flush_wait_seconds}s each). "
|
||||
f"log_text len={len(log_text)}.{other_side_diag} Log tail:\n"
|
||||
f"{log_text[-2000:]}"
|
||||
)
|
||||
|
||||
def assert_no_violation_matching(
|
||||
self,
|
||||
*,
|
||||
launch_tag_patterns: tuple[str, ...],
|
||||
fail_reason: str,
|
||||
side: _Side = None,
|
||||
) -> None:
|
||||
log_text = self._captured_log_text(side)
|
||||
if find_violation_in_log(
|
||||
log_text,
|
||||
launch_tag_patterns=launch_tag_patterns,
|
||||
fail_reason=fail_reason,
|
||||
):
|
||||
raise AssertionError(
|
||||
f"Unexpected canary violation matching "
|
||||
f"launch_tag_patterns={launch_tag_patterns!r} "
|
||||
f"fail_reason={fail_reason!r}. Log tail:\n{log_text[-2000:]}"
|
||||
)
|
||||
|
||||
def assert_no_violation(
|
||||
self,
|
||||
*,
|
||||
side: _Side = None,
|
||||
wait_seconds: float = 2.0,
|
||||
) -> None:
|
||||
time.sleep(wait_seconds)
|
||||
assert_no_violation_in_log(self._captured_log_text(side))
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import re
|
||||
|
||||
_VIOLATION_LINE_RE = re.compile(
|
||||
r"kv_canary violation: launch_tag=(\S+) fail_reason=(\S+)"
|
||||
)
|
||||
|
||||
|
||||
def find_violation_in_log(
|
||||
log_text: str,
|
||||
*,
|
||||
launch_tag_patterns: tuple[str, ...],
|
||||
fail_reason: str,
|
||||
) -> bool:
|
||||
for match in _VIOLATION_LINE_RE.finditer(log_text):
|
||||
tag = match.group(1)
|
||||
reason_field = match.group(2)
|
||||
if fail_reason not in reason_field.split("+"):
|
||||
continue
|
||||
if any(fnmatch.fnmatchcase(tag, pattern) for pattern in launch_tag_patterns):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def assert_no_violation_in_log(log_text: str) -> None:
|
||||
if "kv_canary violation:" in log_text:
|
||||
raise AssertionError(
|
||||
f"Unexpected canary violation found. Log tail:\n{log_text[-2000:]}"
|
||||
)
|
||||
Reference in New Issue
Block a user