sgl-project--sglang
94057c3d3e
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
429 行
16 KiB
Python
429 行
16 KiB
Python
import asyncio
|
|
import os
|
|
import re
|
|
import unittest
|
|
from typing import Any, List, Optional, Tuple
|
|
|
|
from sglang.srt.utils import kill_process_tree
|
|
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
|
from sglang.test.test_utils import (
|
|
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
|
|
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
|
DEFAULT_URL_FOR_TEST,
|
|
STDERR_FILENAME,
|
|
STDOUT_FILENAME,
|
|
CustomTestCase,
|
|
popen_launch_server,
|
|
send_concurrent_generate_requests_with_custom_params,
|
|
)
|
|
|
|
register_cuda_ci(est_time=149, stage="extra-a", runner_config="1-gpu-small")
|
|
register_amd_ci(est_time=195, suite="stage-b-test-1-gpu-small-amd")
|
|
|
|
|
|
class TestPriorityScheduling(CustomTestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.model = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
|
|
cls.base_url = DEFAULT_URL_FOR_TEST
|
|
|
|
cls.stdout = open(STDOUT_FILENAME, "w")
|
|
cls.stderr = open(STDERR_FILENAME, "w")
|
|
|
|
cls.base_url = DEFAULT_URL_FOR_TEST
|
|
cls.process = popen_launch_server(
|
|
cls.model,
|
|
cls.base_url,
|
|
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
|
other_args=(
|
|
"--max-running-requests", # Enforce max request concurrency is 1
|
|
"1",
|
|
"--max-queued-requests", # Enforce max queued request number is 3
|
|
"3",
|
|
"--enable-priority-scheduling", # Enable priority scheduling
|
|
),
|
|
return_stdout_stderr=(cls.stdout, cls.stderr),
|
|
)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
kill_process_tree(cls.process.pid)
|
|
_verify_max_running_requests_and_max_queued_request_validation(1, 3)
|
|
cls.stdout.close()
|
|
cls.stderr.close()
|
|
os.remove(STDOUT_FILENAME)
|
|
os.remove(STDERR_FILENAME)
|
|
|
|
def test_priority_scheduling_request_ordering_validation(self):
|
|
"""Verify pending requests are ordered by priority and received timestamp."""
|
|
|
|
responses = asyncio.run(
|
|
send_concurrent_generate_requests_with_custom_params(
|
|
self.base_url,
|
|
[
|
|
{
|
|
"priority": 0,
|
|
"sampling_params": {"max_new_tokens": 10000},
|
|
}, # starts being processed first
|
|
{"priority": 1}, # third
|
|
{"priority": 1}, # fourth
|
|
{"priority": 2}, # second
|
|
],
|
|
)
|
|
)
|
|
|
|
expected_status_and_error_messages = [
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
]
|
|
|
|
e2e_latencies = []
|
|
_verify_genereate_responses(
|
|
responses, expected_status_and_error_messages, e2e_latencies
|
|
)
|
|
assert e2e_latencies[0] < e2e_latencies[3] < e2e_latencies[1] < e2e_latencies[2]
|
|
|
|
def test_priority_scheduling_existing_requests_abortion_validation(self):
|
|
"""Verify lower priority requests are aborted when incoming requests have higher priority"""
|
|
|
|
responses = asyncio.run(
|
|
send_concurrent_generate_requests_with_custom_params(
|
|
self.base_url,
|
|
[
|
|
{
|
|
"priority": 1,
|
|
"sampling_params": {"max_new_tokens": 10000},
|
|
}, # starts being processed first and holds the running queue capacity
|
|
{"priority": 2}, # aborted by request 5
|
|
{"priority": 3}, # aborted by request 6
|
|
{"priority": 4}, # aborted by request 7
|
|
{"priority": 5}, # fourth
|
|
{"priority": 6}, # third
|
|
{"priority": 7}, # second
|
|
],
|
|
)
|
|
)
|
|
|
|
expected_status_and_error_messages = [
|
|
(200, None),
|
|
(503, "The request is aborted by a higher priority request."),
|
|
(503, "The request is aborted by a higher priority request."),
|
|
(503, "The request is aborted by a higher priority request."),
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
]
|
|
|
|
e2e_latencies = []
|
|
_verify_genereate_responses(
|
|
responses, expected_status_and_error_messages, e2e_latencies
|
|
)
|
|
assert e2e_latencies[0] < e2e_latencies[6] < e2e_latencies[5] < e2e_latencies[4]
|
|
|
|
def test_priority_scheduling_incoming_request_rejection_validation(self):
|
|
"""Verify incoming requests are rejected when existing requests have higher priority"""
|
|
|
|
responses = asyncio.run(
|
|
send_concurrent_generate_requests_with_custom_params(
|
|
self.base_url,
|
|
[
|
|
{
|
|
"priority": 7,
|
|
"sampling_params": {"max_new_tokens": 10000},
|
|
}, # starts being processed first and holds the running queue capacity
|
|
{"priority": 6}, # second
|
|
{"priority": 5}, # third
|
|
{"priority": 4}, # fourth
|
|
{"priority": 3}, # rejected
|
|
{"priority": 2}, # rejected
|
|
{"priority": 1}, # rejected
|
|
],
|
|
)
|
|
)
|
|
|
|
expected_status_and_error_messages = [
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
(503, "The request queue is full."),
|
|
(503, "The request queue is full."),
|
|
(503, "The request queue is full."),
|
|
]
|
|
|
|
e2e_latencies = []
|
|
_verify_genereate_responses(
|
|
responses, expected_status_and_error_messages, e2e_latencies
|
|
)
|
|
assert e2e_latencies[0] < e2e_latencies[1] < e2e_latencies[2] < e2e_latencies[3]
|
|
|
|
def test_priority_scheduling_preemption_meeting_threshold_validation(self):
|
|
"""Verify running requests are preempted by requests with priorities meeting the preemption threshold"""
|
|
|
|
responses = asyncio.run(
|
|
send_concurrent_generate_requests_with_custom_params(
|
|
self.base_url,
|
|
[
|
|
{
|
|
"priority": 0,
|
|
"sampling_params": {"max_new_tokens": 10000},
|
|
}, # starts being processed first then preempted or pushed by later requests, and finishes last.
|
|
{
|
|
"priority": 10,
|
|
"sampling_params": {"max_new_tokens": 10000},
|
|
}, # scheduled after the third request, and finishes second.
|
|
{
|
|
"priority": 20,
|
|
"sampling_params": {"max_new_tokens": 10000},
|
|
}, # finishes first.
|
|
],
|
|
)
|
|
)
|
|
|
|
expected_status_and_error_messages = [
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
]
|
|
|
|
e2e_latencies = []
|
|
_verify_genereate_responses(
|
|
responses, expected_status_and_error_messages, e2e_latencies
|
|
)
|
|
|
|
assert e2e_latencies[2] < e2e_latencies[1] < e2e_latencies[0]
|
|
|
|
def test_priority_scheduling_preemption_below_threshold_validation(self):
|
|
"""Verify running requests are not preempted by requests with priorities below preemption threshold"""
|
|
|
|
# Stagger sends so priority=0 occupies the running queue before
|
|
# priority=5 arrives -- asyncio.gather gives no arrival-order guarantee.
|
|
# ignore_eos on both: priority=0 stays running when priority=5 arrives
|
|
# (exercises the no-preempt path), and priority=5's runtime must exceed
|
|
# the stagger so its server-side e2e_latency stays > priority=0's.
|
|
async def _send(priority, **sampling):
|
|
return await send_concurrent_generate_requests_with_custom_params(
|
|
self.base_url,
|
|
[{"priority": priority, "sampling_params": sampling}],
|
|
)
|
|
|
|
async def _run():
|
|
first = asyncio.create_task(_send(0, max_new_tokens=1000, ignore_eos=True))
|
|
await asyncio.sleep(1.0)
|
|
second = asyncio.create_task(_send(5, max_new_tokens=1000, ignore_eos=True))
|
|
return (await first) + (await second)
|
|
|
|
responses = asyncio.run(_run())
|
|
|
|
expected_status_and_error_messages = [
|
|
(200, None),
|
|
(200, None),
|
|
]
|
|
|
|
e2e_latencies = []
|
|
_verify_genereate_responses(
|
|
responses, expected_status_and_error_messages, e2e_latencies
|
|
)
|
|
|
|
assert e2e_latencies[0] < e2e_latencies[1]
|
|
|
|
|
|
class TestPrioritySchedulingMultipleRunningRequests(CustomTestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.model = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
|
|
cls.base_url = DEFAULT_URL_FOR_TEST
|
|
|
|
cls.stdout = open(STDOUT_FILENAME, "w")
|
|
cls.stderr = open(STDERR_FILENAME, "w")
|
|
|
|
cls.base_url = DEFAULT_URL_FOR_TEST
|
|
cls.process = popen_launch_server(
|
|
cls.model,
|
|
cls.base_url,
|
|
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
|
other_args=(
|
|
"--max-running-requests", # Enforce max request concurrency is 2
|
|
"2",
|
|
"--max-queued-requests", # Enforce max queued request number is 3
|
|
"3",
|
|
"--enable-priority-scheduling", # Enable priority scheduling
|
|
),
|
|
return_stdout_stderr=(cls.stdout, cls.stderr),
|
|
)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
kill_process_tree(cls.process.pid)
|
|
_verify_max_running_requests_and_max_queued_request_validation(2, 3)
|
|
cls.stdout.close()
|
|
cls.stderr.close()
|
|
os.remove(STDOUT_FILENAME)
|
|
os.remove(STDERR_FILENAME)
|
|
|
|
def test_priority_scheduling_with_multiple_running_requests_preemption(self):
|
|
"""Verify preempting a subset of running requests is safe."""
|
|
|
|
responses = asyncio.run(
|
|
send_concurrent_generate_requests_with_custom_params(
|
|
self.base_url,
|
|
[
|
|
{
|
|
"priority": 10,
|
|
"sampling_params": {"max_new_tokens": 10000},
|
|
}, # finishes first
|
|
{
|
|
"priority": 5,
|
|
"sampling_params": {"max_new_tokens": 10000},
|
|
}, # preempted by fourth request, then finishes third
|
|
{
|
|
"priority": 15,
|
|
"sampling_params": {"max_new_tokens": 10000},
|
|
}, # preempt the first request
|
|
],
|
|
)
|
|
)
|
|
|
|
expected_status_and_error_messages = [
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
]
|
|
|
|
_verify_genereate_responses(responses, expected_status_and_error_messages, [])
|
|
|
|
def test_priority_scheduling_preemption_token_offset_calculation(self):
|
|
"""
|
|
Verify correct token offset calculation during preemption.
|
|
|
|
This test specifically targets the bug where rem_total_token_offset was incorrectly
|
|
calculated using the incoming request's tokens instead of the preempted request's tokens
|
|
(related to issue #13111 and PR #13201).
|
|
|
|
THE BUG:
|
|
In schedule_policy.py line 700, the code was using:
|
|
self.rem_total_token_offset -= self._get_running_request_total_token_offset(req)
|
|
Instead of:
|
|
self.rem_total_token_offset -= self._get_running_request_total_token_offset(running_req)
|
|
|
|
WHY THIS TEST CATCHES THE BUG:
|
|
- Request 1 (preempted): 8000 tokens - This is what SHOULD be freed
|
|
- Request 3 (incoming): 1000 tokens - This is what WAS freed (bug)
|
|
- Token difference: 8000 - 1000 = 7000 tokens incorrectly accounted
|
|
|
|
With the bug, the system thinks it only freed 1000 tokens instead of 8000 tokens.
|
|
This causes incorrect memory accounting and can lead to:
|
|
1. Scheduler believes less memory is available than actually is
|
|
2. Subsequent requests (like Request 4) may fail to schedule or cause issues
|
|
3. Memory calculations become increasingly inaccurate with each preemption
|
|
|
|
The test creates a scenario where:
|
|
1. A low-priority request with many tokens (8000) starts running
|
|
2. A high-priority request with few tokens (1000) arrives and triggers preemption
|
|
3. The system must correctly free 8000 tokens from the preempted request
|
|
4. Additional requests can be scheduled only if tokens were correctly freed
|
|
5. Execution order validates priority-based scheduling works correctly
|
|
|
|
The large token difference (8x) makes the bug's impact obvious and testable.
|
|
"""
|
|
responses = asyncio.run(
|
|
send_concurrent_generate_requests_with_custom_params(
|
|
self.base_url,
|
|
[
|
|
{
|
|
"priority": 0,
|
|
"sampling_params": {"max_new_tokens": 8000},
|
|
}, # Low priority, large token count - will be preempted
|
|
{
|
|
"priority": 1,
|
|
"sampling_params": {"max_new_tokens": 5000},
|
|
}, # Medium priority, medium token count - queued initially
|
|
{
|
|
"priority": 100,
|
|
"sampling_params": {"max_new_tokens": 1000},
|
|
}, # High priority, small token count - triggers preemption
|
|
{
|
|
"priority": 50,
|
|
"sampling_params": {"max_new_tokens": 2000},
|
|
}, # Should be schedulable after correct token accounting
|
|
],
|
|
)
|
|
)
|
|
|
|
# All requests should complete successfully
|
|
# The key is that the fourth request should be schedulable because
|
|
# the system correctly freed tokens from the first (preempted) request
|
|
expected_status_and_error_messages = [
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
(200, None),
|
|
]
|
|
|
|
e2e_latencies = []
|
|
_verify_genereate_responses(
|
|
responses, expected_status_and_error_messages, e2e_latencies
|
|
)
|
|
|
|
# Verify execution order: high priority requests finish before low priority ones
|
|
# Request 3 (priority 100) should finish first
|
|
# Request 4 (priority 50) should finish second
|
|
# Request 2 (priority 1) should finish third
|
|
# Request 1 (priority 0) should finish last (after being preempted)
|
|
|
|
# FIXME(harrison lim)
|
|
# assert e2e_latencies[2] < e2e_latencies[3] < e2e_latencies[1] < e2e_latencies[0]
|
|
|
|
|
|
def _verify_genereate_responses(
|
|
responses: Tuple[int, Any, float],
|
|
expected_code_and_error_message: Tuple[int, Any],
|
|
e2e_latencies: List[Optional[float]],
|
|
):
|
|
"""
|
|
Verify generate response results are as expected based on status code and response json object content.
|
|
In addition, collects e2e latency info to verify scheduling and processing ordering.
|
|
"""
|
|
for got, expected in zip(responses, expected_code_and_error_message):
|
|
got_status, got_json = got
|
|
expected_status, expected_err_msg = expected
|
|
|
|
# Check status code is as expected
|
|
assert got_status == expected_status
|
|
|
|
# Check error message content or fields' existence based on status code
|
|
if got_status != 200:
|
|
assert got_json["object"] == "error"
|
|
assert got_json["message"] == expected_err_msg
|
|
else:
|
|
assert "object" not in got_json
|
|
assert "message" not in got_json
|
|
|
|
# Collect e2e latencies for scheduling validation
|
|
e2e_latencies.append(
|
|
got_json["meta_info"]["e2e_latency"] if got_status == 200 else None
|
|
)
|
|
|
|
|
|
def _verify_max_running_requests_and_max_queued_request_validation(
|
|
max_running_requests: int, max_queued_requests: int
|
|
):
|
|
"""Verify running request and queued request numbers based on server logs."""
|
|
rr_pattern = re.compile(r"#running-req:\s*(\d+)")
|
|
qr_pattern = re.compile(r"#queue-req:\s*(\d+)")
|
|
|
|
with open(STDERR_FILENAME) as lines:
|
|
for line in lines:
|
|
rr_match, qr_match = rr_pattern.search(line), qr_pattern.search(line)
|
|
if rr_match:
|
|
assert int(rr_match.group(1)) <= max_running_requests
|
|
if qr_match:
|
|
assert int(qr_match.group(1)) <= max_queued_requests
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|