[CI] Graceful teardown for kv_canary and EAGLE spec fixtures (#32829)

This commit is contained in:
Alison Shao
2026-08-02 01:09:59 -07:00
committed by GitHub
parent 8d106c3d79
commit 43be25b2b7
18 changed files with 58 additions and 46 deletions
@@ -4,7 +4,6 @@ import time
from types import SimpleNamespace from types import SimpleNamespace
from typing import ClassVar, List, Optional from typing import ClassVar, List, Optional
from sglang.srt.utils import kill_process_tree
from sglang.test.run_eval import run_eval from sglang.test.run_eval import run_eval
from sglang.test.server_fixtures.disaggregation_fixture import ( from sglang.test.server_fixtures.disaggregation_fixture import (
PDDisaggregationServerBase, PDDisaggregationServerBase,
@@ -14,6 +13,7 @@ from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
try_cached_model, try_cached_model,
) )
@@ -106,7 +106,7 @@ class ChunkedTestBase(ChunkedGsm8kMixin, CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
if cls.process is not None: if cls.process is not None:
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
class ChunkedTestPDBase(ChunkedGsm8kMixin, PDDisaggregationServerBase): class ChunkedTestPDBase(ChunkedGsm8kMixin, PDDisaggregationServerBase):
+2 -2
View File
@@ -8,7 +8,6 @@ from typing import ClassVar, Literal, Optional
from sglang.srt.kv_canary.config import CanaryMode from sglang.srt.kv_canary.config import CanaryMode
from sglang.srt.kv_canary.runner.swa_divergence import SwaDivergenceLog 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.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.utils import build_canary_server_args, post_parallel_generate
from sglang.test.kv_canary.violation_assert_mixin import CanaryViolationAssertMixin from sglang.test.kv_canary.violation_assert_mixin import CanaryViolationAssertMixin
@@ -17,6 +16,7 @@ from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
# Long prompt body shared by all canary e2e tests. The repetition count is chosen # Long prompt body shared by all canary e2e tests. The repetition count is chosen
@@ -36,7 +36,7 @@ class CapturedServerE2EBase(CanaryViolationAssertMixin, CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls) -> None: def tearDownClass(cls) -> None:
if cls.process is not None: if cls.process is not None:
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
for buf in (cls._stdout_buf, cls._stderr_buf): for buf in (cls._stdout_buf, cls._stderr_buf):
if buf is not None: if buf is not None:
buf.close() buf.close()
@@ -4,12 +4,12 @@ from contextlib import contextmanager
import requests import requests
from sglang.srt.utils import kill_process_tree
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -65,7 +65,7 @@ class DefaultServerBase(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid, wait_timeout=60) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
time.sleep(2) time.sleep(2)
@classmethod @classmethod
@@ -20,12 +20,12 @@ The base itself is NOT a runnable test (no `test_*` methods until a subclass
mixes in the kits), so unittest discovery picks it up as empty. mixes in the kits), so unittest discovery picks it up as empty.
""" """
from sglang.srt.utils import kill_process_tree
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
@@ -95,4 +95,4 @@ class DsaMtpServerBase(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
if hasattr(cls, "process") and cls.process: if hasattr(cls, "process") and cls.process:
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
@@ -4,7 +4,6 @@ import time
import requests import requests
from sglang.srt.utils.common import kill_process_tree
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_DRAFT_MODEL_EAGLE, DEFAULT_DRAFT_MODEL_EAGLE,
DEFAULT_TARGET_MODEL_EAGLE, DEFAULT_TARGET_MODEL_EAGLE,
@@ -12,6 +11,7 @@ from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
PROMPTS = [ PROMPTS = [
@@ -53,7 +53,7 @@ class EagleServerBase(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid, wait_timeout=60) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
def send_request(self): def send_request(self):
time.sleep(random.uniform(0, 2)) time.sleep(random.uniform(0, 2))
@@ -12,7 +12,7 @@ from types import SimpleNamespace
import requests import requests
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.utils import get_device_sm, kill_process_tree from sglang.srt.utils import get_device_sm
from sglang.test.run_eval import run_eval from sglang.test.run_eval import run_eval
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST, DEFAULT_MODEL_NAME_FOR_TEST,
@@ -21,6 +21,7 @@ from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
GSM_DATASET_PATH = None GSM_DATASET_PATH = None
@@ -73,7 +74,7 @@ class TestHybridAttnBackendBase(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
def test_gsm8k(self): def test_gsm8k(self):
requests.get(self.base_url + "/flush_cache") requests.get(self.base_url + "/flush_cache")
@@ -2,12 +2,12 @@ import logging
import os import os
import time import time
from sglang.srt.utils import kill_process_tree
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -65,7 +65,7 @@ class MMMUServerBase(CustomTestCase):
def tearDownClass(cls): def tearDownClass(cls):
if cls.process is not None and cls.process.poll() is None: if cls.process is not None and cls.process.poll() is None:
try: try:
kill_process_tree(cls.process.pid, wait_timeout=60) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
except Exception as e: except Exception as e:
logger.error(f"Error killing process: {e}") logger.error(f"Error killing process: {e}")
time.sleep(2) time.sleep(2)
@@ -15,13 +15,13 @@ mixes in GSM8KMixin), so unittest discovery picks it up as empty.
""" """
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.utils import kill_process_tree
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_TARGET_MODEL_NGRAM, DEFAULT_TARGET_MODEL_NGRAM,
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
DEFAULT_NGRAM_SERVER_ARGS = [ DEFAULT_NGRAM_SERVER_ARGS = [
@@ -71,4 +71,4 @@ class NgramServerBase(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
@@ -16,12 +16,12 @@ from types import SimpleNamespace
import requests import requests
from sglang.srt.utils import kill_process_tree
from sglang.test.run_eval import run_eval from sglang.test.run_eval import run_eval
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
@@ -54,7 +54,7 @@ class PCGSpecBase:
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
def test_gsm8k(self): def test_gsm8k(self):
eval_kwargs = dict( eval_kwargs = dict(
@@ -17,7 +17,6 @@ import time
import requests import requests
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.utils.common import kill_process_tree
from sglang.srt.utils.hf_transformers_utils import get_tokenizer from sglang.srt.utils.hf_transformers_utils import get_tokenizer
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_DRAFT_MODEL_EAGLE, DEFAULT_DRAFT_MODEL_EAGLE,
@@ -28,6 +27,7 @@ from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
# Chat-style prompts shared by send_request / send_requests_abort. # Chat-style prompts shared by send_request / send_requests_abort.
@@ -146,7 +146,7 @@ class SpecEagleServerBase(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid, wait_timeout=60) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
@property @property
def tokenizer(self): def tokenizer(self):
@@ -15,7 +15,6 @@ from types import SimpleNamespace
import requests import requests
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.utils import kill_process_tree
from sglang.test.run_eval import run_eval from sglang.test.run_eval import run_eval
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_DRAFT_MODEL_STANDALONE, DEFAULT_DRAFT_MODEL_STANDALONE,
@@ -23,6 +22,7 @@ from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
GSM_DATASET_PATH = None GSM_DATASET_PATH = None
@@ -88,7 +88,7 @@ class StandaloneServerBase:
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
def test_gsm8k(self): def test_gsm8k(self):
requests.get(self.base_url + "/flush_cache") requests.get(self.base_url + "/flush_cache")
@@ -21,7 +21,6 @@ import aiohttp
import requests import requests
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.utils import kill_process_tree
from sglang.srt.utils.hf_transformers_utils import get_tokenizer from sglang.srt.utils.hf_transformers_utils import get_tokenizer
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_SMALL_MODEL_NAME_FOR_TEST, DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
@@ -29,6 +28,7 @@ from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
LOGPROB_PROMPTS = [ LOGPROB_PROMPTS = [
@@ -432,4 +432,4 @@ class StreamingSessionServerBase(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
@@ -1,12 +1,12 @@
import unittest import unittest
from sglang.srt.utils import kill_process_tree
from sglang.test.test_deterministic import BenchArgs, test_deterministic from sglang.test.test_deterministic import BenchArgs, test_deterministic
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
DEFAULT_MODEL = "Qwen/Qwen3-8B" DEFAULT_MODEL = "Qwen/Qwen3-8B"
@@ -43,7 +43,7 @@ class TestDeterministicBase(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
def _extract_host_and_port(self, url): def _extract_host_and_port(self, url):
return url.split("://")[-1].split(":")[0], int(url.split(":")[-1]) return url.split("://")[-1].split(":")[0], int(url.split(":")[-1])
+21
View File
@@ -1064,6 +1064,27 @@ def popen_launch_server(
raise TimeoutError(error_msg) raise TimeoutError(error_msg)
def terminate_and_kill_process_tree(
process,
terminate_timeout: float = 60,
**kill_kwargs,
) -> None:
"""Shut a launched server down gracefully, then SIGKILL whatever is left.
A bare ``kill_process_tree`` leaves the kernel to unwind the CUDA context
and unpin the host memory during process reclaim, which can hold GPU memory
for minutes on a busy host -- long enough to trip the per-class GPU-idle
gate in the next ``setUpClass``. SIGTERM first so the server releases those
resources in userspace.
"""
process.terminate()
try:
process.wait(timeout=terminate_timeout)
except subprocess.TimeoutExpired:
pass
kill_process_tree(process.pid, **kill_kwargs)
def popen_launch_pd_server( def popen_launch_pd_server(
model: str, model: str,
base_url: str, base_url: str,
+2 -2
View File
@@ -8,12 +8,12 @@ import pybase64
import requests import requests
from PIL import Image from PIL import Image
from sglang.srt.utils import kill_process_tree
from sglang.test.test_utils import ( from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH, DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
# image # image
@@ -60,7 +60,7 @@ class TestOpenAIMLLMServerBase(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
def get_vision_request_kwargs(self): def get_vision_request_kwargs(self):
return self.get_request_kwargs() return self.get_request_kwargs()
@@ -1,12 +1,10 @@
import os import os
import shutil import shutil
import subprocess
import tempfile import tempfile
import unittest import unittest
from test_unified_radix_cache_kl_nightly import AccuracyTwoPassMixin from test_unified_radix_cache_kl_nightly import AccuracyTwoPassMixin
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_cuda_ci from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kits.unified_radix_cache_kit import UnifiedRadixTreeTestMixin from sglang.test.kits.unified_radix_cache_kit import UnifiedRadixTreeTestMixin
from sglang.test.kl_multiturn_utils import ( from sglang.test.kl_multiturn_utils import (
@@ -19,6 +17,7 @@ from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
CustomTestCase, CustomTestCase,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
register_cuda_ci(est_time=800, stage="extra-b", runner_config="4-gpu-h100") register_cuda_ci(est_time=800, stage="extra-b", runner_config="4-gpu-h100")
@@ -69,7 +68,7 @@ class TestUnifiedMambaRadixCache(UnifiedRadixTreeTestMixin, CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
# ─── Mamba + HiCache L2 ────────────────────────────────────────────────────── # ─── Mamba + HiCache L2 ──────────────────────────────────────────────────────
@@ -128,12 +127,7 @@ class TestUnifiedMambaHiCache(UnifiedRadixTreeTestMixin, CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
cls.process.terminate() terminate_and_kill_process_tree(cls.process, wait_timeout=60)
try:
cls.process.wait(timeout=60)
except subprocess.TimeoutExpired:
pass
kill_process_tree(cls.process.pid)
# ─── Mamba + HiCache L3 (file backend) ─────────────────────────────────────── # ─── Mamba + HiCache L3 (file backend) ───────────────────────────────────────
@@ -194,12 +188,7 @@ class TestUnifiedMambaHiCacheL3(AccuracyTwoPassMixin, CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
cls.process.terminate() terminate_and_kill_process_tree(cls.process, wait_timeout=60)
try:
cls.process.wait(timeout=60)
except subprocess.TimeoutExpired:
pass
kill_process_tree(cls.process.pid)
if os.path.isdir(cls.hicache_dir): if os.path.isdir(cls.hicache_dir):
shutil.rmtree(cls.hicache_dir, ignore_errors=True) shutil.rmtree(cls.hicache_dir, ignore_errors=True)
@@ -17,6 +17,7 @@ from sglang.test.vlm_utils import (
OmniOpenAITestMixin, OmniOpenAITestMixin,
TestOpenAIMLLMServerBase, TestOpenAIMLLMServerBase,
VideoOpenAITestMixin, VideoOpenAITestMixin,
terminate_and_kill_process_tree,
) )
register_cuda_ci(est_time=780, stage="base-b", runner_config="1-gpu-large") register_cuda_ci(est_time=780, stage="base-b", runner_config="1-gpu-large")
@@ -74,7 +75,7 @@ class TestQwen2VLContextLengthServer(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
def test_single_image_chat_completion(self): def test_single_image_chat_completion(self):
client = openai.Client(api_key=self.api_key, base_url=self.base_url) client = openai.Client(api_key=self.api_key, base_url=self.base_url)
+2 -2
View File
@@ -29,8 +29,8 @@ from sglang.srt.utils.common import is_xpu
from sglang.test.test_utils import CustomTestCase from sglang.test.test_utils import CustomTestCase
from sglang.test.vlm_utils import ( from sglang.test.vlm_utils import (
DEFAULT_URL_FOR_TEST, DEFAULT_URL_FOR_TEST,
kill_process_tree,
popen_launch_server, popen_launch_server,
terminate_and_kill_process_tree,
) )
MODEL = "google/gemma-4-E2B-it" MODEL = "google/gemma-4-E2B-it"
@@ -98,7 +98,7 @@ class TestGemma4E2BXPU(CustomTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
kill_process_tree(cls.process.pid) terminate_and_kill_process_tree(cls.process, wait_timeout=60)
_empty_xpu_cache() _empty_xpu_cache()
def test_simple_qa(self): def test_simple_qa(self):