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,134 @@
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
CHUNKED_PREFILL_SIZE = 64
|
||||
LONG_PROMPT = (
|
||||
"The quick brown fox jumps over the lazy dog. "
|
||||
"Pack my box with five dozen liquor jugs. "
|
||||
"Sphinx of black quartz, judge my vow. "
|
||||
) * 900
|
||||
|
||||
|
||||
def _decode_response(response: requests.Response) -> Any:
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError:
|
||||
return response.text
|
||||
|
||||
|
||||
def _is_abort_result(status_code: int, body: Any) -> bool:
|
||||
if status_code == 200:
|
||||
reason = (
|
||||
body.get("meta_info", {}).get("finish_reason", {})
|
||||
if isinstance(body, dict)
|
||||
else {}
|
||||
)
|
||||
return isinstance(reason, dict) and reason.get("type") == "abort"
|
||||
|
||||
if status_code not in (500, 503):
|
||||
return False
|
||||
|
||||
text = body if isinstance(body, str) else str(body)
|
||||
return "abort" in text.lower()
|
||||
|
||||
|
||||
class TestChunkedPrefillAbortE2E(CustomTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=[
|
||||
"--disable-cuda-graph",
|
||||
"--chunked-prefill-size",
|
||||
str(CHUNKED_PREFILL_SIZE),
|
||||
"--max-running-requests",
|
||||
"4",
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.process:
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def test_abort_mid_chunked_prefill_by_rid(self):
|
||||
rid = f"chunked-prefill-abort-{uuid.uuid4().hex}"
|
||||
result: dict[str, Any] = {}
|
||||
|
||||
def run_generate():
|
||||
try:
|
||||
response = requests.post(
|
||||
self.base_url + "/generate",
|
||||
json={
|
||||
"rid": rid,
|
||||
"text": f"{rid}\n{LONG_PROMPT}",
|
||||
"sampling_params": {
|
||||
"temperature": 0,
|
||||
"max_new_tokens": 4096,
|
||||
"ignore_eos": True,
|
||||
},
|
||||
},
|
||||
timeout=180,
|
||||
)
|
||||
result["status_code"] = response.status_code
|
||||
result["body"] = _decode_response(response)
|
||||
except requests.RequestException as exc:
|
||||
result["exception"] = repr(exc)
|
||||
|
||||
thread = threading.Thread(target=run_generate)
|
||||
thread.start()
|
||||
|
||||
time.sleep(0.5)
|
||||
abort_deadline = time.monotonic() + 8
|
||||
while thread.is_alive() and time.monotonic() < abort_deadline:
|
||||
requests.post(
|
||||
self.base_url + "/abort_request",
|
||||
json={"rid": rid, "abort_all": False},
|
||||
timeout=10,
|
||||
)
|
||||
time.sleep(0.2)
|
||||
|
||||
thread.join(timeout=60)
|
||||
self.assertFalse(thread.is_alive(), "Chunked-prefill abort request hung")
|
||||
self.assertNotIn("exception", result, result.get("exception"))
|
||||
self.assertTrue(
|
||||
_is_abort_result(result["status_code"], result["body"]),
|
||||
f"Expected chunked-prefill request to abort, got {result}",
|
||||
)
|
||||
|
||||
health = requests.get(self.base_url + "/health", timeout=10)
|
||||
self.assertEqual(health.status_code, 200, health.text)
|
||||
|
||||
follow_up = requests.post(
|
||||
self.base_url + "/generate",
|
||||
json={
|
||||
"rid": f"chunked-prefill-after-abort-{uuid.uuid4().hex}",
|
||||
"text": "The capital of France is",
|
||||
"sampling_params": {"temperature": 0, "max_new_tokens": 4},
|
||||
},
|
||||
timeout=60,
|
||||
)
|
||||
follow_up_body = _decode_response(follow_up)
|
||||
self.assertEqual(follow_up.status_code, 200, follow_up_body)
|
||||
self.assertFalse(_is_abort_result(follow_up.status_code, follow_up_body))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.test.server_fixtures.disaggregation_fixture import (
|
||||
PDDisaggregationServerBase,
|
||||
)
|
||||
from sglang.test.test_utils import DEFAULT_SMALL_MODEL_NAME_FOR_TEST
|
||||
|
||||
PD_EXTRA_ARGS = ["--max-running-requests", "4", "--chunked-prefill-size", "64"]
|
||||
LONG_PROMPT = (
|
||||
"The quick brown fox jumps over the lazy dog. "
|
||||
"Pack my box with five dozen liquor jugs. "
|
||||
"Sphinx of black quartz, judge my vow. "
|
||||
) * 900
|
||||
|
||||
|
||||
def _decode_response(response: requests.Response) -> Any:
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError:
|
||||
return response.text
|
||||
|
||||
|
||||
def _is_abort_result(status_code: int, body: Any) -> bool:
|
||||
if status_code == 200:
|
||||
reason = (
|
||||
body.get("meta_info", {}).get("finish_reason", {})
|
||||
if isinstance(body, dict)
|
||||
else {}
|
||||
)
|
||||
return isinstance(reason, dict) and reason.get("type") == "abort"
|
||||
|
||||
if status_code not in (500, 503):
|
||||
return False
|
||||
|
||||
text = body if isinstance(body, str) else str(body)
|
||||
return "abort" in text.lower()
|
||||
|
||||
|
||||
class TestDisaggChunkedPrefillAbort(PDDisaggregationServerBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.model = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
|
||||
cls.extra_prefill_args = PD_EXTRA_ARGS
|
||||
cls.extra_decode_args = PD_EXTRA_ARGS
|
||||
cls.launch_all()
|
||||
|
||||
def _post_abort(self, rid: str):
|
||||
for url in (self.prefill_url, self.decode_url):
|
||||
requests.post(
|
||||
url + "/abort_request",
|
||||
json={"rid": rid, "abort_all": False},
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
def test_abort_mid_chunked_prefill_by_rid(self):
|
||||
rid = f"pd-chunked-prefill-abort-{uuid.uuid4().hex}"
|
||||
result: dict[str, Any] = {}
|
||||
|
||||
def run_generate():
|
||||
try:
|
||||
response = requests.post(
|
||||
self.lb_url + "/generate",
|
||||
json={
|
||||
"rid": rid,
|
||||
"text": f"{rid}\n{LONG_PROMPT}",
|
||||
"sampling_params": {
|
||||
"temperature": 0,
|
||||
"max_new_tokens": 4096,
|
||||
"ignore_eos": True,
|
||||
},
|
||||
},
|
||||
timeout=180,
|
||||
)
|
||||
result["status_code"] = response.status_code
|
||||
result["body"] = _decode_response(response)
|
||||
except requests.RequestException as exc:
|
||||
result["exception"] = repr(exc)
|
||||
|
||||
thread = threading.Thread(target=run_generate)
|
||||
thread.start()
|
||||
|
||||
time.sleep(1.0)
|
||||
abort_deadline = time.monotonic() + 8
|
||||
while thread.is_alive() and time.monotonic() < abort_deadline:
|
||||
self._post_abort(rid)
|
||||
time.sleep(0.2)
|
||||
|
||||
thread.join(timeout=60)
|
||||
self.assertFalse(thread.is_alive(), "Chunked-prefill abort request hung")
|
||||
self.assertNotIn("exception", result, result.get("exception"))
|
||||
self.assertTrue(
|
||||
_is_abort_result(result["status_code"], result["body"]),
|
||||
f"Expected chunked-prefill request to abort, got {result}",
|
||||
)
|
||||
|
||||
for url in (self.lb_url, self.prefill_url, self.decode_url):
|
||||
health = requests.get(url + "/health", timeout=10)
|
||||
self.assertEqual(health.status_code, 200, health.text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,316 @@
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError, as_completed
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.test.server_fixtures.disaggregation_fixture import (
|
||||
PDDisaggregationServerBase,
|
||||
)
|
||||
from sglang.test.test_utils import DEFAULT_MODEL_NAME_FOR_TEST
|
||||
|
||||
FAILURE_PROB = 0.12
|
||||
PD_EXTRA_ARGS = [
|
||||
"--max-running-requests",
|
||||
"4",
|
||||
"--chunked-prefill-size",
|
||||
"128",
|
||||
]
|
||||
PD_LONG_PROMPT = (
|
||||
"The quick brown fox jumps over the lazy dog. "
|
||||
"Pack my box with five dozen liquor jugs. "
|
||||
"Sphinx of black quartz, judge my vow. "
|
||||
) * 240
|
||||
|
||||
|
||||
def _body_text(body: Any) -> str:
|
||||
if isinstance(body, str):
|
||||
return body
|
||||
return json.dumps(body, sort_keys=True)
|
||||
|
||||
|
||||
def _decode_response(response: requests.Response) -> Any:
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError:
|
||||
return response.text
|
||||
|
||||
|
||||
def _finish_reason(body: Any) -> dict[str, Any]:
|
||||
if not isinstance(body, dict):
|
||||
return {}
|
||||
reason = body.get("meta_info", {}).get("finish_reason", {})
|
||||
return reason if isinstance(reason, dict) else {}
|
||||
|
||||
|
||||
def _is_abort_result(status_code: int, body: Any) -> bool:
|
||||
if status_code == 200:
|
||||
return _finish_reason(body).get("type") == "abort"
|
||||
|
||||
if status_code not in (500, 503):
|
||||
return False
|
||||
|
||||
text = _body_text(body).lower()
|
||||
return any(
|
||||
marker in text
|
||||
for marker in (
|
||||
"pd peer failed",
|
||||
"prefill bootstrap failed",
|
||||
"abort",
|
||||
"aborted",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _is_success_result(status_code: int, body: Any) -> bool:
|
||||
return (
|
||||
status_code == 200
|
||||
and isinstance(body, dict)
|
||||
and "text" in body
|
||||
and _finish_reason(body).get("type") != "abort"
|
||||
)
|
||||
|
||||
|
||||
class TestDisaggregationPeerLivenessAbort(PDDisaggregationServerBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._disagg_failure_ctx = envs.SGLANG_TEST_DISAGG_FAILURE_PROB.override(
|
||||
FAILURE_PROB
|
||||
)
|
||||
cls._disagg_failure_ctx.__enter__()
|
||||
|
||||
cls.model = DEFAULT_MODEL_NAME_FOR_TEST
|
||||
cls.extra_prefill_args = PD_EXTRA_ARGS
|
||||
cls.extra_decode_args = PD_EXTRA_ARGS
|
||||
cls.launch_all()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
try:
|
||||
super().tearDownClass()
|
||||
finally:
|
||||
if cls._disagg_failure_ctx:
|
||||
cls._disagg_failure_ctx.__exit__(None, None, None)
|
||||
|
||||
def _post_generate(self, rid: str, max_new_tokens: int = 32) -> dict[str, Any]:
|
||||
response = requests.post(
|
||||
self.lb_url + "/generate",
|
||||
json={
|
||||
"rid": rid,
|
||||
"text": f"{rid}\n{PD_LONG_PROMPT}",
|
||||
"sampling_params": {
|
||||
"temperature": 0,
|
||||
"max_new_tokens": max_new_tokens,
|
||||
"ignore_eos": True,
|
||||
},
|
||||
},
|
||||
timeout=75,
|
||||
)
|
||||
return {
|
||||
"rid": rid,
|
||||
"status_code": response.status_code,
|
||||
"body": _decode_response(response),
|
||||
}
|
||||
|
||||
def _assert_servers_healthy(self):
|
||||
for url in (self.lb_url, self.prefill_url, self.decode_url):
|
||||
response = requests.get(url + "/health", timeout=10)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
|
||||
def _load_count(self, url: str) -> int:
|
||||
response = requests.get(
|
||||
url + "/v1/loads?include=core,disagg,queues",
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
total = 0
|
||||
for load in response.json()["loads"]:
|
||||
total += int(load.get("num_running_reqs", 0))
|
||||
total += int(load.get("num_waiting_reqs", 0))
|
||||
disagg = load.get("disaggregation", {})
|
||||
total += int(disagg.get("prefill_bootstrap_queue_reqs", 0))
|
||||
total += int(disagg.get("prefill_inflight_queue_reqs", 0))
|
||||
total += int(disagg.get("decode_prealloc_queue_reqs", 0))
|
||||
total += int(disagg.get("decode_transfer_queue_reqs", 0))
|
||||
total += int(disagg.get("decode_retracted_queue_reqs", 0))
|
||||
queues = load.get("queues", {})
|
||||
total += int(queues.get("waiting", 0))
|
||||
total += int(queues.get("grammar", 0))
|
||||
total += int(queues.get("paused", 0))
|
||||
total += int(queues.get("retracted", 0))
|
||||
return total
|
||||
|
||||
def _assert_eventually_idle(self):
|
||||
deadline = time.monotonic() + 30
|
||||
last_counts = {}
|
||||
while time.monotonic() < deadline:
|
||||
last_counts = {
|
||||
self.prefill_url: self._load_count(self.prefill_url),
|
||||
self.decode_url: self._load_count(self.decode_url),
|
||||
}
|
||||
if all(count == 0 for count in last_counts.values()):
|
||||
return
|
||||
time.sleep(1)
|
||||
|
||||
self.fail(f"PD servers still have request load after cleanup: {last_counts}")
|
||||
|
||||
def _assert_one_successful_generate_with_retries(self):
|
||||
last_result = None
|
||||
for _ in range(20):
|
||||
rid = f"pd-peer-liveness-health-{uuid.uuid4().hex}"
|
||||
last_result = self._post_generate(rid, max_new_tokens=1)
|
||||
if _is_success_result(last_result["status_code"], last_result["body"]):
|
||||
return
|
||||
time.sleep(0.2)
|
||||
|
||||
self.fail(
|
||||
"Server stayed HTTP-healthy but did not complete a follow-up generate "
|
||||
f"under failure injection. Last result: {last_result}"
|
||||
)
|
||||
|
||||
def test_peer_failures_abort_without_hanging(self):
|
||||
num_requests = 36
|
||||
start = time.monotonic()
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
with ThreadPoolExecutor(max_workers=18) as executor:
|
||||
futures = [
|
||||
executor.submit(
|
||||
self._post_generate,
|
||||
f"pd-peer-liveness-{i}-{uuid.uuid4().hex}",
|
||||
)
|
||||
for i in range(num_requests)
|
||||
]
|
||||
|
||||
try:
|
||||
for future in as_completed(futures, timeout=120):
|
||||
results.append(future.result())
|
||||
except TimeoutError:
|
||||
unfinished = sum(not future.done() for future in futures)
|
||||
self.fail(f"{unfinished} generate requests did not return in time")
|
||||
|
||||
elapsed = time.monotonic() - start
|
||||
self.assertLess(elapsed, 120)
|
||||
self.assertEqual(len(results), num_requests)
|
||||
|
||||
aborts = [
|
||||
result
|
||||
for result in results
|
||||
if _is_abort_result(result["status_code"], result["body"])
|
||||
]
|
||||
successes = [
|
||||
result
|
||||
for result in results
|
||||
if _is_success_result(result["status_code"], result["body"])
|
||||
]
|
||||
unexpected = [
|
||||
result
|
||||
for result in results
|
||||
if result not in aborts and result not in successes
|
||||
]
|
||||
|
||||
self.assertFalse(
|
||||
unexpected,
|
||||
"Expected only clean aborts or successful completions. "
|
||||
f"Unexpected results: {unexpected[:3]}",
|
||||
)
|
||||
self.assertGreater(
|
||||
len(aborts),
|
||||
0,
|
||||
"Injected KVPoll.Failed should abort at least one request",
|
||||
)
|
||||
|
||||
self._assert_servers_healthy()
|
||||
self._assert_eventually_idle()
|
||||
self._assert_one_successful_generate_with_retries()
|
||||
|
||||
|
||||
class TestDisaggregationPeerLivenessCleanRecovery(PDDisaggregationServerBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.model = DEFAULT_MODEL_NAME_FOR_TEST
|
||||
cls.extra_prefill_args = PD_EXTRA_ARGS
|
||||
cls.extra_decode_args = PD_EXTRA_ARGS
|
||||
cls.launch_all()
|
||||
|
||||
def _post_abort_to_engines(self, rid: str):
|
||||
for url in (self.prefill_url, self.decode_url):
|
||||
response = requests.post(
|
||||
url + "/abort_request",
|
||||
json={"rid": rid, "abort_all": False},
|
||||
timeout=10,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
|
||||
def _assert_servers_healthy(self):
|
||||
for url in (self.lb_url, self.prefill_url, self.decode_url):
|
||||
response = requests.get(url + "/health", timeout=10)
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
|
||||
def test_clean_generate_and_explicit_abort_by_rid(self):
|
||||
clean_response = requests.post(
|
||||
self.lb_url + "/generate",
|
||||
json={
|
||||
"rid": f"pd-clean-generate-{uuid.uuid4().hex}",
|
||||
"text": "The capital of France is",
|
||||
"sampling_params": {"temperature": 0, "max_new_tokens": 4},
|
||||
},
|
||||
timeout=60,
|
||||
)
|
||||
clean_body = _decode_response(clean_response)
|
||||
self.assertTrue(
|
||||
_is_success_result(clean_response.status_code, clean_body), clean_body
|
||||
)
|
||||
|
||||
rid = f"pd-explicit-abort-{uuid.uuid4().hex}"
|
||||
result: dict[str, Any] = {}
|
||||
|
||||
def run_generate():
|
||||
try:
|
||||
response = requests.post(
|
||||
self.lb_url + "/generate",
|
||||
json={
|
||||
"rid": rid,
|
||||
"text": f"{rid}\n{PD_LONG_PROMPT}",
|
||||
"sampling_params": {
|
||||
"temperature": 0,
|
||||
"max_new_tokens": 4096,
|
||||
"ignore_eos": True,
|
||||
},
|
||||
},
|
||||
timeout=180,
|
||||
)
|
||||
result["status_code"] = response.status_code
|
||||
result["body"] = _decode_response(response)
|
||||
except requests.RequestException as exc:
|
||||
result["exception"] = repr(exc)
|
||||
|
||||
thread = threading.Thread(target=run_generate)
|
||||
thread.start()
|
||||
|
||||
time.sleep(0.25)
|
||||
abort_deadline = time.monotonic() + 8
|
||||
while thread.is_alive() and time.monotonic() < abort_deadline:
|
||||
self._post_abort_to_engines(rid)
|
||||
time.sleep(0.25)
|
||||
|
||||
thread.join(timeout=60)
|
||||
self.assertFalse(thread.is_alive(), "Explicitly aborted PD request hung")
|
||||
self.assertNotIn("exception", result, result.get("exception"))
|
||||
self.assertTrue(
|
||||
_is_abort_result(result["status_code"], result["body"]),
|
||||
f"Expected explicit abort result, got {result}",
|
||||
)
|
||||
|
||||
self._assert_servers_healthy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user