Add routing key based schedule policy (#16840)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from sglang.srt.managers.schedule_batch import Req
|
||||
from sglang.srt.managers.schedule_batch import Req, ScheduleBatch
|
||||
from sglang.srt.managers.schedule_policy import (
|
||||
CacheAgnosticPolicy,
|
||||
CacheAwarePolicy,
|
||||
@@ -197,6 +197,118 @@ class TestSchedulePolicy(CustomTestCase):
|
||||
self.assertEqual(waiting_queue[1].rid, 2)
|
||||
self.assertEqual(waiting_queue[2].rid, 3)
|
||||
|
||||
def test_calc_priority_routing_key_scheduling(self):
|
||||
"""Test routing-key policy: prioritize by routing key frequency in running batch."""
|
||||
tree_cache = RadixCache.create_simulated()
|
||||
|
||||
running_reqs = [
|
||||
Req("r1", "a", [1], SamplingParams(), routing_key="key_a"),
|
||||
Req("r2", "b", [2], SamplingParams(), routing_key="key_a"),
|
||||
Req("r3", "c", [3], SamplingParams(), routing_key="key_b"),
|
||||
]
|
||||
running_batch = ScheduleBatch(reqs=running_reqs)
|
||||
|
||||
waiting_queue = [
|
||||
Req("w1", "d", [4], SamplingParams(), routing_key="key_b"),
|
||||
Req("w2", "e", [5], SamplingParams(), routing_key="key_a"),
|
||||
Req("w3", "f", [6], SamplingParams(), routing_key="key_c"),
|
||||
]
|
||||
|
||||
policy = SchedulePolicy(
|
||||
policy="routing-key",
|
||||
tree_cache=tree_cache,
|
||||
enable_hierarchical_cache=False,
|
||||
enable_priority_scheduling=False,
|
||||
schedule_low_priority_values_first=False,
|
||||
)
|
||||
policy.calc_priority(waiting_queue, running_batch)
|
||||
|
||||
self.assertEqual(waiting_queue[0].rid, "w2")
|
||||
self.assertEqual(waiting_queue[1].rid, "w1")
|
||||
self.assertEqual(waiting_queue[2].rid, "w3")
|
||||
|
||||
def test_calc_priority_routing_key_tie_break_by_lexicographic_order(self):
|
||||
"""Test routing-key policy: tie-break by lexicographic order."""
|
||||
tree_cache = RadixCache.create_simulated()
|
||||
|
||||
running_reqs = [
|
||||
Req("r1", "a", [1], SamplingParams(), routing_key="key_b"),
|
||||
Req("r2", "b", [2], SamplingParams(), routing_key="key_a"),
|
||||
]
|
||||
running_batch = ScheduleBatch(reqs=running_reqs)
|
||||
|
||||
waiting_queue = [
|
||||
Req("w1", "d", [4], SamplingParams(), routing_key="key_b"),
|
||||
Req("w2", "e", [5], SamplingParams(), routing_key="key_a"),
|
||||
]
|
||||
|
||||
policy = SchedulePolicy(
|
||||
policy="routing-key",
|
||||
tree_cache=tree_cache,
|
||||
enable_hierarchical_cache=False,
|
||||
enable_priority_scheduling=False,
|
||||
schedule_low_priority_values_first=False,
|
||||
)
|
||||
policy.calc_priority(waiting_queue, running_batch)
|
||||
|
||||
self.assertEqual(waiting_queue[0].rid, "w2")
|
||||
self.assertEqual(waiting_queue[1].rid, "w1")
|
||||
|
||||
def test_calc_priority_routing_key_no_match_deprioritized(self):
|
||||
"""Test routing-key policy: requests without matching routing keys are deprioritized."""
|
||||
tree_cache = RadixCache.create_simulated()
|
||||
|
||||
running_reqs = [
|
||||
Req("r1", "a", [1], SamplingParams(), routing_key="key_a"),
|
||||
Req("r2", "b", [2], SamplingParams(), routing_key="key_b"),
|
||||
Req("r3", "c", [3], SamplingParams(), routing_key="key_c"),
|
||||
]
|
||||
running_batch = ScheduleBatch(reqs=running_reqs)
|
||||
|
||||
waiting_queue = [
|
||||
Req("w1", "d", [4], SamplingParams(), routing_key="key_d"),
|
||||
Req("w2", "e", [5], SamplingParams(), routing_key="key_e"),
|
||||
Req("w3", "f", [6], SamplingParams(), routing_key="key_c"),
|
||||
]
|
||||
|
||||
policy = SchedulePolicy(
|
||||
policy="routing-key",
|
||||
tree_cache=tree_cache,
|
||||
enable_hierarchical_cache=False,
|
||||
enable_priority_scheduling=False,
|
||||
schedule_low_priority_values_first=False,
|
||||
)
|
||||
policy.calc_priority(waiting_queue, running_batch)
|
||||
|
||||
self.assertEqual(waiting_queue[0].rid, "w3")
|
||||
self.assertEqual(waiting_queue[1].rid, "w1")
|
||||
self.assertEqual(waiting_queue[2].rid, "w2")
|
||||
|
||||
def test_calc_priority_routing_key_empty_running_batch(self):
|
||||
"""Test routing-key policy: empty running batch keeps original order."""
|
||||
tree_cache = RadixCache.create_simulated()
|
||||
|
||||
running_batch = ScheduleBatch(reqs=[])
|
||||
|
||||
waiting_queue = [
|
||||
Req("w1", "d", [4], SamplingParams(), routing_key="key_a"),
|
||||
Req("w2", "e", [5], SamplingParams(), routing_key="key_b"),
|
||||
Req("w3", "f", [6], SamplingParams(), routing_key="key_c"),
|
||||
]
|
||||
|
||||
policy = SchedulePolicy(
|
||||
policy="routing-key",
|
||||
tree_cache=tree_cache,
|
||||
enable_hierarchical_cache=False,
|
||||
enable_priority_scheduling=False,
|
||||
schedule_low_priority_values_first=False,
|
||||
)
|
||||
policy.calc_priority(waiting_queue, running_batch)
|
||||
|
||||
self.assertEqual(waiting_queue[0].rid, "w1")
|
||||
self.assertEqual(waiting_queue[1].rid, "w2")
|
||||
self.assertEqual(waiting_queue[2].rid, "w3")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
122
test/registered/scheduler/test_routing_key_scheduling.py
Normal file
122
test/registered/scheduler/test_routing_key_scheduling.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import aiohttp
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
STDERR_FILENAME,
|
||||
STDOUT_FILENAME,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=120, suite="nightly-1-gpu", nightly=True)
|
||||
|
||||
|
||||
class TestRoutingKeyScheduling(CustomTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
os.environ["SGLANG_ROUTING_KEY_POLICY_DEBUG_LOG"] = "1"
|
||||
|
||||
cls.model = "Qwen/Qwen3-0.6B"
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
|
||||
cls.stdout = open(STDOUT_FILENAME, "w")
|
||||
cls.stderr = open(STDERR_FILENAME, "w")
|
||||
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=(
|
||||
"--max-running-requests",
|
||||
"3",
|
||||
"--schedule-policy",
|
||||
"routing-key",
|
||||
),
|
||||
return_stdout_stderr=(cls.stdout, cls.stderr),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
cls.stdout.close()
|
||||
cls.stderr.close()
|
||||
os.remove(STDOUT_FILENAME)
|
||||
os.remove(STDERR_FILENAME)
|
||||
|
||||
def test_routing_key_scheduling_order(self):
|
||||
"""Verify requests with matching routing keys are prioritized.
|
||||
|
||||
Test strategy:
|
||||
1. First send 2 long-running key_a requests to occupy running batch
|
||||
2. Then send 10 key_a and 10 key_b short requests concurrently
|
||||
3. With max_running_requests=3, key_a requests should be prioritized
|
||||
because running batch has 2 key_a requests
|
||||
4. Verify key_a requests finish before key_b requests on average
|
||||
"""
|
||||
asyncio.run(self._test_routing_key_scheduling_order())
|
||||
|
||||
async def _test_routing_key_scheduling_order(self):
|
||||
long_running_tasks = [
|
||||
asyncio.create_task(self._send_chat_request("key_a", 20000)),
|
||||
asyncio.create_task(self._send_chat_request("key_a", 20000)),
|
||||
]
|
||||
|
||||
await asyncio.sleep(2.0)
|
||||
|
||||
short_tasks = []
|
||||
for _ in range(10):
|
||||
short_tasks.append(
|
||||
asyncio.create_task(self._send_chat_request("key_a", 10))
|
||||
)
|
||||
short_tasks.append(
|
||||
asyncio.create_task(self._send_chat_request("key_b", 10))
|
||||
)
|
||||
|
||||
all_short_results = await asyncio.gather(*short_tasks)
|
||||
await asyncio.gather(*long_running_tasks)
|
||||
|
||||
key_a_latencies = [lat for key, lat in all_short_results if key == "key_a"]
|
||||
key_b_latencies = [lat for key, lat in all_short_results if key == "key_b"]
|
||||
|
||||
avg_key_a = sum(key_a_latencies) / len(key_a_latencies)
|
||||
avg_key_b = sum(key_b_latencies) / len(key_b_latencies)
|
||||
|
||||
print(f"Average key_a latency: {avg_key_a:.3f}s")
|
||||
print(f"Average key_b latency: {avg_key_b:.3f}s")
|
||||
|
||||
self.assertLess(
|
||||
avg_key_a,
|
||||
avg_key_b,
|
||||
f"key_a requests (avg={avg_key_a:.3f}s) should finish before key_b (avg={avg_key_b:.3f}s)",
|
||||
)
|
||||
|
||||
async def _send_chat_request(self, routing_key: str, max_tokens: int):
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"messages": [{"role": "user", "content": "What is 1+1?"}],
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": 0,
|
||||
}
|
||||
headers = {"x-smg-routing-key": routing_key}
|
||||
start_time = time.perf_counter()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{self.base_url}/v1/chat/completions",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
) as resp:
|
||||
await resp.json()
|
||||
latency = time.perf_counter() - start_time
|
||||
return routing_key, latency
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user