[CI] Collapse the EAGLE launch matrix and the scoring engine boots on the per-commit runners (#33756)
This commit is contained in:
@@ -50,13 +50,12 @@ class TestStep3p5FlashChainMTP(GSM8KMixin, DefaultServerBase):
|
||||
gsm8k_accuracy_thres = 0.83
|
||||
gsm8k_accept_length_thres = 2.6
|
||||
|
||||
def test_logprob_spec_v2_match(self):
|
||||
"""Verify spec v2 decode logprobs match prefill scoring logprobs.
|
||||
def test_logprob_decode_match_prefill(self):
|
||||
"""Decode logprobs from the spec path must match prefill scoring.
|
||||
|
||||
Generate tokens with chain MTP spec v2, then score the same sequence
|
||||
via prefill-only (no speculation). The two sets of logprobs should be
|
||||
close, validating that spec v2 + multi-layer EAGLE computes logprobs
|
||||
correctly.
|
||||
Generate tokens with chain MTP, then score the same sequence via
|
||||
prefill-only (no speculation). The two sets of logprobs should be
|
||||
close, validating that multi-layer EAGLE computes logprobs correctly.
|
||||
"""
|
||||
requests.get(self.base_url + "/flush_cache")
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ from sglang.test.test_utils import (
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=222, stage="base-b", runner_config="1-gpu-small")
|
||||
register_cuda_ci(est_time=165, stage="base-b", runner_config="1-gpu-small")
|
||||
register_amd_ci(
|
||||
est_time=186,
|
||||
est_time=140,
|
||||
suite="stage-b-test-1-gpu-small-amd",
|
||||
disabled="see https://github.com/sgl-project/sglang/issues/11127",
|
||||
)
|
||||
@@ -307,52 +307,5 @@ class TestOpenAIServerWithEAGLEAndHiddenStatesEnabled(
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
|
||||
class TestOpenAIServerWithEAGLE3AndHiddenStatesEnabled(
|
||||
CustomTestCase, BaseTestOpenAIServerWithHiddenStates
|
||||
):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = "meta-llama/Llama-3.1-8B-Instruct"
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.api_key = "sk-123456"
|
||||
cls.speculative_algorithm = "EAGLE3"
|
||||
cls.speculative_draft_model = "jamesliu1/sglang-EAGLE3-Llama-3.1-Instruct-8B"
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=[
|
||||
"--speculative-algorithm",
|
||||
cls.speculative_algorithm,
|
||||
"--speculative-draft-model-path",
|
||||
cls.speculative_draft_model,
|
||||
"--speculative-num-steps",
|
||||
5,
|
||||
"--speculative-eagle-topk",
|
||||
16,
|
||||
"--speculative-num-draft-tokens",
|
||||
64,
|
||||
"--mem-fraction-static",
|
||||
0.7,
|
||||
"--chunked-prefill-size",
|
||||
128,
|
||||
"--max-running-requests",
|
||||
8,
|
||||
"--dtype",
|
||||
"float16",
|
||||
"--enable-return-hidden-states",
|
||||
],
|
||||
)
|
||||
cls.base_url += "/v1"
|
||||
cls.tokenizer = get_tokenizer(cls.model)
|
||||
cls.return_hidden_states = [False, True]
|
||||
cls.use_list_input = [True, False]
|
||||
cls.parallel_sample_nums = [1]
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -27,7 +27,7 @@ from sglang.test.test_utils import (
|
||||
CustomTestCase,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=211, stage="base-b", runner_config="1-gpu-small")
|
||||
register_cuda_ci(est_time=120, stage="base-b", runner_config="1-gpu-small")
|
||||
|
||||
TEST_MODEL_NAME = os.environ.get("TEST_MODEL_NAME", DEFAULT_SMALL_MODEL_NAME_FOR_TEST)
|
||||
TEST_CLASSIFICATION_BASE_MODEL = os.environ.get(
|
||||
@@ -145,16 +145,21 @@ class TestMultiItemScoringOptimization(CustomTestCase):
|
||||
|
||||
|
||||
class TestMultiItemScoringClassification(CustomTestCase):
|
||||
"""Test MIS with classification models.
|
||||
"""MIS on a classification model: basics, MIS-vs-single-item parity, score
|
||||
distinctness / determinism / concurrency.
|
||||
|
||||
Uses a pre-trained Qwen3ForSequenceClassification model so that the
|
||||
classification head weights are deterministic across Engine instances.
|
||||
Pre-trained Qwen3ForSequenceClassification, so the head weights are
|
||||
deterministic. One class rather than four because the CI harness demands an
|
||||
idle GPU at every setUpClass -- splitting these means re-booting the same
|
||||
two engines instead of sharing them. score() is stateless and the radix
|
||||
cache is off, so sharing is safe.
|
||||
"""
|
||||
|
||||
NUM_LABELS = _CLS_NUM_LABELS
|
||||
|
||||
def setUp(self):
|
||||
self.engine = Engine(
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.engine = Engine(
|
||||
model_path=TEST_CLASSIFICATION_BASE_MODEL,
|
||||
disable_radix_cache=True,
|
||||
chunked_prefill_size=-1,
|
||||
@@ -162,11 +167,18 @@ class TestMultiItemScoringClassification(CustomTestCase):
|
||||
attention_backend="flashinfer",
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
cls.non_mis_engine = Engine(
|
||||
model_path=TEST_CLASSIFICATION_BASE_MODEL,
|
||||
disable_radix_cache=True,
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
if self.engine is not None:
|
||||
self.engine.shutdown()
|
||||
torch.cuda.empty_cache()
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
for engine in (cls.engine, cls.non_mis_engine):
|
||||
if engine is not None:
|
||||
engine.shutdown()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def test_classification_mis_basic(self):
|
||||
"""Classification MIS: correct shapes, valid softmax probabilities."""
|
||||
@@ -203,149 +215,14 @@ class TestMultiItemScoringClassification(CustomTestCase):
|
||||
|
||||
def test_classification_non_mis_fallback(self):
|
||||
"""Classification model works correctly without --enable-mis."""
|
||||
non_mis_engine = Engine(
|
||||
model_path=TEST_CLASSIFICATION_BASE_MODEL,
|
||||
disable_radix_cache=True,
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
try:
|
||||
scores = non_mis_engine.score(
|
||||
query="Test:", items=["A", "B"], apply_softmax=True
|
||||
).scores
|
||||
|
||||
self.assertEqual(len(scores), 2)
|
||||
for score_list in scores:
|
||||
self.assertEqual(len(score_list), self.NUM_LABELS)
|
||||
self.assertAlmostEqual(sum(score_list), 1.0, places=5)
|
||||
finally:
|
||||
non_mis_engine.shutdown()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
class TestMultiItemScoringParity(CustomTestCase):
|
||||
"""Test that MIS produces the same results as single-item scoring."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.engine_single = Engine(
|
||||
model_path=TEST_MODEL_NAME,
|
||||
disable_radix_cache=True,
|
||||
log_level="error",
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
cls.engine_mis = Engine(
|
||||
model_path=TEST_MODEL_NAME,
|
||||
disable_radix_cache=True,
|
||||
chunked_prefill_size=-1,
|
||||
log_level="error",
|
||||
enable_mis=True,
|
||||
attention_backend="flashinfer",
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.engine_single is not None:
|
||||
cls.engine_single.shutdown()
|
||||
if cls.engine_mis is not None:
|
||||
cls.engine_mis.shutdown()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def _compare_scores(
|
||||
self, query, items, label_token_ids=None, apply_softmax=True, test_name=""
|
||||
):
|
||||
"""Compare MIS vs single-item scoring results."""
|
||||
single_scores = self.engine_single.score(
|
||||
query=query,
|
||||
items=items,
|
||||
label_token_ids=label_token_ids,
|
||||
apply_softmax=apply_softmax,
|
||||
scores = self.non_mis_engine.score(
|
||||
query="Test:", items=["A", "B"], apply_softmax=True
|
||||
).scores
|
||||
|
||||
mis_scores = self.engine_mis.score(
|
||||
query=query,
|
||||
items=items,
|
||||
label_token_ids=label_token_ids,
|
||||
apply_softmax=apply_softmax,
|
||||
).scores
|
||||
|
||||
self.assertEqual(
|
||||
len(mis_scores), len(single_scores), f"{test_name}: count mismatch"
|
||||
)
|
||||
for i, (ms, ss) in enumerate(zip(mis_scores, single_scores)):
|
||||
self.assertEqual(len(ms), len(ss), f"{test_name}: item {i} length mismatch")
|
||||
for j, (m, s) in enumerate(zip(ms, ss)):
|
||||
self.assertAlmostEqual(
|
||||
m,
|
||||
s,
|
||||
places=1,
|
||||
msg=f"{test_name}: item {i} label {j}: MIS={m} vs single={s}",
|
||||
)
|
||||
|
||||
def test_parity_basic(self):
|
||||
tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME)
|
||||
query = "Rate this option:"
|
||||
items = [" Option A", " Option B", " Option C"]
|
||||
labels = [" good", " bad"]
|
||||
label_ids = [tokenizer.encode(lb, add_special_tokens=False)[0] for lb in labels]
|
||||
self._compare_scores(query, items, label_ids, test_name="basic")
|
||||
|
||||
def test_parity_tokenized_inputs(self):
|
||||
tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME)
|
||||
query = "Rate this option:"
|
||||
items = [" Option X", " Option Y"]
|
||||
labels = [" good", " bad"]
|
||||
query_ids = tokenizer.encode(query, add_special_tokens=False)
|
||||
items_ids = [tokenizer.encode(i, add_special_tokens=False) for i in items]
|
||||
label_ids = [tokenizer.encode(lb, add_special_tokens=False)[0] for lb in labels]
|
||||
self._compare_scores(query_ids, items_ids, label_ids, test_name="tokenized")
|
||||
|
||||
def test_parity_without_softmax(self):
|
||||
tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME)
|
||||
query = "The weather today is"
|
||||
items = [" sunny", " cloudy", " rainy"]
|
||||
labels = [" nice", " bad"]
|
||||
label_ids = [tokenizer.encode(lb, add_special_tokens=False)[0] for lb in labels]
|
||||
self._compare_scores(
|
||||
query, items, label_ids, apply_softmax=False, test_name="no_softmax"
|
||||
)
|
||||
|
||||
def test_parity_many_items(self):
|
||||
tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME)
|
||||
query = "Rate this option from 1 to 5:"
|
||||
items = [f" Option {i}" for i in range(10)]
|
||||
labels = [" 1", " 2", " 3", " 4", " 5"]
|
||||
label_ids = [tokenizer.encode(lb, add_special_tokens=False)[0] for lb in labels]
|
||||
self._compare_scores(query, items, label_ids, test_name="many_items")
|
||||
|
||||
|
||||
class TestMultiItemScoringClassificationParity(CustomTestCase):
|
||||
"""Test that MIS multi-item batching matches single-item MIS scoring.
|
||||
|
||||
Both paths use the MIS engine (with delimiter tokens in the attention
|
||||
context). The reference scores each item individually so each gets its
|
||||
own forward pass; the batched path packs all items into one pass.
|
||||
This isolates the MIS batching logic from the delimiter-presence effect.
|
||||
"""
|
||||
|
||||
NUM_LABELS = _CLS_NUM_LABELS
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.engine = Engine(
|
||||
model_path=TEST_CLASSIFICATION_BASE_MODEL,
|
||||
disable_radix_cache=True,
|
||||
chunked_prefill_size=-1,
|
||||
enable_mis=True,
|
||||
attention_backend="flashinfer",
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.engine is not None:
|
||||
cls.engine.shutdown()
|
||||
torch.cuda.empty_cache()
|
||||
self.assertEqual(len(scores), 2)
|
||||
for score_list in scores:
|
||||
self.assertEqual(len(score_list), self.NUM_LABELS)
|
||||
self.assertAlmostEqual(sum(score_list), 1.0, places=5)
|
||||
|
||||
def _compare_scores(self, query, items, apply_softmax=True, test_name=""):
|
||||
"""Compare MIS batched vs MIS single-item scoring results."""
|
||||
@@ -403,89 +280,6 @@ class TestMultiItemScoringClassificationParity(CustomTestCase):
|
||||
items = [f" Option {i}" for i in range(10)]
|
||||
self._compare_scores(query, items, test_name="cls_many_items")
|
||||
|
||||
|
||||
class TestMultiItemScoringClassificationMISvsNonMIS(CustomTestCase):
|
||||
"""Test that MIS single-item approximates non-MIS single-item.
|
||||
|
||||
The MIS path inserts delimiter tokens into the attention context,
|
||||
which slightly perturbs hidden states. After softmax the scores
|
||||
should still be close. Uses places=1 (±0.05) tolerance.
|
||||
|
||||
Runs as a separate class so each engine is created and destroyed
|
||||
independently to avoid GPU OOM.
|
||||
"""
|
||||
|
||||
def test_mis_single_vs_non_mis(self):
|
||||
non_mis_engine = Engine(
|
||||
model_path=TEST_CLASSIFICATION_BASE_MODEL,
|
||||
disable_radix_cache=True,
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
try:
|
||||
query = "Rate this option:"
|
||||
items = [" Option A", " Option B", " Option C"]
|
||||
non_mis_scores = non_mis_engine.score(
|
||||
query=query,
|
||||
items=items,
|
||||
apply_softmax=True,
|
||||
).scores
|
||||
finally:
|
||||
non_mis_engine.shutdown()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
mis_engine = Engine(
|
||||
model_path=TEST_CLASSIFICATION_BASE_MODEL,
|
||||
disable_radix_cache=True,
|
||||
chunked_prefill_size=-1,
|
||||
enable_mis=True,
|
||||
attention_backend="flashinfer",
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
try:
|
||||
mis_scores = mis_engine.score(
|
||||
query=query,
|
||||
items=items,
|
||||
apply_softmax=True,
|
||||
).scores
|
||||
finally:
|
||||
mis_engine.shutdown()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
self.assertEqual(len(mis_scores), len(non_mis_scores))
|
||||
for i, (ms, ns) in enumerate(zip(mis_scores, non_mis_scores)):
|
||||
self.assertEqual(len(ms), len(ns))
|
||||
for j, (m, n) in enumerate(zip(ms, ns)):
|
||||
self.assertAlmostEqual(
|
||||
m,
|
||||
n,
|
||||
places=1,
|
||||
msg=f"item {i} label {j}: MIS={m} vs non-MIS={n}",
|
||||
)
|
||||
|
||||
|
||||
class TestMultiItemScoringClassificationAdvanced(CustomTestCase):
|
||||
"""Advanced MIS tests for classification models: score distinctness,
|
||||
determinism, and concurrent request handling."""
|
||||
|
||||
NUM_LABELS = _CLS_NUM_LABELS
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.engine = Engine(
|
||||
model_path=TEST_CLASSIFICATION_BASE_MODEL,
|
||||
disable_radix_cache=True,
|
||||
chunked_prefill_size=-1,
|
||||
enable_mis=True,
|
||||
attention_backend="flashinfer",
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.engine is not None:
|
||||
cls.engine.shutdown()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def test_items_produce_distinct_scores(self):
|
||||
"""Different items must produce different score vectors.
|
||||
|
||||
@@ -594,6 +388,130 @@ class TestMultiItemScoringClassificationAdvanced(CustomTestCase):
|
||||
f"concurrent={c} vs sequential={s}",
|
||||
)
|
||||
|
||||
def test_mis_single_vs_non_mis(self):
|
||||
"""MIS single-item must approximate non-MIS single-item.
|
||||
|
||||
MIS inserts delimiter tokens into the attention context, which
|
||||
perturbs hidden states; after softmax the scores should still land
|
||||
within places=1 (+-0.05).
|
||||
"""
|
||||
query = "Rate this option:"
|
||||
items = [" Option A", " Option B", " Option C"]
|
||||
non_mis_scores = self.non_mis_engine.score(
|
||||
query=query, items=items, apply_softmax=True
|
||||
).scores
|
||||
mis_scores = self.engine.score(
|
||||
query=query, items=items, apply_softmax=True
|
||||
).scores
|
||||
|
||||
self.assertEqual(len(mis_scores), len(non_mis_scores))
|
||||
for i, (ms, ns) in enumerate(zip(mis_scores, non_mis_scores)):
|
||||
self.assertEqual(len(ms), len(ns))
|
||||
for j, (m, n) in enumerate(zip(ms, ns)):
|
||||
self.assertAlmostEqual(
|
||||
m,
|
||||
n,
|
||||
places=1,
|
||||
msg=f"item {i} label {j}: MIS={m} vs non-MIS={n}",
|
||||
)
|
||||
|
||||
|
||||
class TestMultiItemScoringParity(CustomTestCase):
|
||||
"""Test that MIS produces the same results as single-item scoring."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.engine_single = Engine(
|
||||
model_path=TEST_MODEL_NAME,
|
||||
disable_radix_cache=True,
|
||||
log_level="error",
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
cls.engine_mis = Engine(
|
||||
model_path=TEST_MODEL_NAME,
|
||||
disable_radix_cache=True,
|
||||
chunked_prefill_size=-1,
|
||||
log_level="error",
|
||||
enable_mis=True,
|
||||
attention_backend="flashinfer",
|
||||
mem_fraction_static=0.15,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.engine_single is not None:
|
||||
cls.engine_single.shutdown()
|
||||
if cls.engine_mis is not None:
|
||||
cls.engine_mis.shutdown()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def _compare_scores(
|
||||
self, query, items, label_token_ids=None, apply_softmax=True, test_name=""
|
||||
):
|
||||
"""Compare MIS vs single-item scoring results."""
|
||||
single_scores = self.engine_single.score(
|
||||
query=query,
|
||||
items=items,
|
||||
label_token_ids=label_token_ids,
|
||||
apply_softmax=apply_softmax,
|
||||
).scores
|
||||
|
||||
mis_scores = self.engine_mis.score(
|
||||
query=query,
|
||||
items=items,
|
||||
label_token_ids=label_token_ids,
|
||||
apply_softmax=apply_softmax,
|
||||
).scores
|
||||
|
||||
self.assertEqual(
|
||||
len(mis_scores), len(single_scores), f"{test_name}: count mismatch"
|
||||
)
|
||||
for i, (ms, ss) in enumerate(zip(mis_scores, single_scores)):
|
||||
self.assertEqual(len(ms), len(ss), f"{test_name}: item {i} length mismatch")
|
||||
for j, (m, s) in enumerate(zip(ms, ss)):
|
||||
self.assertAlmostEqual(
|
||||
m,
|
||||
s,
|
||||
places=1,
|
||||
msg=f"{test_name}: item {i} label {j}: MIS={m} vs single={s}",
|
||||
)
|
||||
|
||||
def test_parity_basic(self):
|
||||
tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME)
|
||||
query = "Rate this option:"
|
||||
items = [" Option A", " Option B", " Option C"]
|
||||
labels = [" good", " bad"]
|
||||
label_ids = [tokenizer.encode(lb, add_special_tokens=False)[0] for lb in labels]
|
||||
self._compare_scores(query, items, label_ids, test_name="basic")
|
||||
|
||||
def test_parity_tokenized_inputs(self):
|
||||
tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME)
|
||||
query = "Rate this option:"
|
||||
items = [" Option X", " Option Y"]
|
||||
labels = [" good", " bad"]
|
||||
query_ids = tokenizer.encode(query, add_special_tokens=False)
|
||||
items_ids = [tokenizer.encode(i, add_special_tokens=False) for i in items]
|
||||
label_ids = [tokenizer.encode(lb, add_special_tokens=False)[0] for lb in labels]
|
||||
self._compare_scores(query_ids, items_ids, label_ids, test_name="tokenized")
|
||||
|
||||
def test_parity_without_softmax(self):
|
||||
tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME)
|
||||
query = "The weather today is"
|
||||
items = [" sunny", " cloudy", " rainy"]
|
||||
labels = [" nice", " bad"]
|
||||
label_ids = [tokenizer.encode(lb, add_special_tokens=False)[0] for lb in labels]
|
||||
self._compare_scores(
|
||||
query, items, label_ids, apply_softmax=False, test_name="no_softmax"
|
||||
)
|
||||
|
||||
def test_parity_many_items(self):
|
||||
tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME)
|
||||
query = "Rate this option from 1 to 5:"
|
||||
items = [f" Option {i}" for i in range(10)]
|
||||
labels = [" 1", " 2", " 3", " 4", " 5"]
|
||||
label_ids = [tokenizer.encode(lb, add_special_tokens=False)[0] for lb in labels]
|
||||
self._compare_scores(query, items, label_ids, test_name="many_items")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -3,9 +3,8 @@ import os
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
from sglang.test.ci.ci_register import register_amd_ci
|
||||
|
||||
register_cuda_ci(est_time=103, stage="base-b", runner_config="1-gpu-small")
|
||||
register_amd_ci(est_time=106, suite="stage-b-test-1-gpu-small-amd-mi35x")
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
@@ -14,7 +13,7 @@ import requests
|
||||
import torch
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.srt.utils.common import is_cuda_alike, mxfp_supported
|
||||
from sglang.srt.utils.common import is_cuda_alike, is_gfx95_supported
|
||||
from sglang.test.few_shot_gsm8k import run_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
@@ -36,7 +35,7 @@ class TestOnlineQuantizationMemoryLoad(CustomTestCase):
|
||||
f"test requires {cls.tp} devices, only {torch.cuda.device_count()} are available."
|
||||
)
|
||||
|
||||
if not mxfp_supported():
|
||||
if not is_gfx95_supported():
|
||||
raise unittest.SkipTest(
|
||||
"online MXFP4 quantization requires an AMD ROCm device with "
|
||||
"FP4 hardware support (gfx95x, e.g. MI355x)"
|
||||
|
||||
@@ -161,7 +161,7 @@ class TestDFlashServerNoCudaGraph(TestDFlashServerBase):
|
||||
other_launch_args = ["--disable-cuda-graph"]
|
||||
|
||||
|
||||
class TestDFlashServerSpecV2(TestDFlashServerBase):
|
||||
class TestDFlashServerOverlap(TestDFlashServerBase):
|
||||
disable_overlap = False
|
||||
|
||||
def test_radix_attention(self):
|
||||
@@ -169,7 +169,7 @@ class TestDFlashServerSpecV2(TestDFlashServerBase):
|
||||
assert self.process.poll() is None
|
||||
|
||||
|
||||
class TestDFlashServerSpecV2PlanStream(TestDFlashServerSpecV2):
|
||||
class TestDFlashServerOverlapPlanStream(TestDFlashServerOverlap):
|
||||
overlap_plan_stream = True
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""EAGLE3 spec-decoding core: overlap (spec v2) x no-overlap (spec v1) matrix,
|
||||
same standard config (topk=1, page_size=1), only ``disable_overlap`` differs.
|
||||
"""EAGLE3 spec-decoding core: overlap x no-overlap matrix at the standard
|
||||
config (topk=1, page_size=1); only ``disable_overlap`` differs. Both run the
|
||||
same EAGLEWorkerV2 -- the scheduler just drives it synchronously when overlap
|
||||
is off.
|
||||
flashinfer is pinned (the 5090 default) so a default-selection change can't
|
||||
silently alter what this exercises.
|
||||
"""
|
||||
@@ -35,13 +37,13 @@ class _Core(Eagle3Base):
|
||||
|
||||
|
||||
class TestEagle3Overlap(_Core, *_KITS):
|
||||
"""Spec v2 (overlap scheduler on)."""
|
||||
"""Overlap scheduler on."""
|
||||
|
||||
disable_overlap = False
|
||||
|
||||
|
||||
class TestEagle3NoOverlap(_Core, *_KITS):
|
||||
"""Spec v1 (overlap scheduler off)."""
|
||||
"""Overlap scheduler off (synchronous)."""
|
||||
|
||||
disable_overlap = True
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from sglang.srt.environ import envs
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.spec_server_kits import (
|
||||
SpecAccuracyKit,
|
||||
SpecCorrectnessKit,
|
||||
SpecFeatureKit,
|
||||
SpecLogprobKit,
|
||||
SpecPenaltyKit,
|
||||
@@ -18,11 +17,16 @@ from sglang.test.kits.spec_server_kits import (
|
||||
)
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base, EagleLlama2Base
|
||||
|
||||
register_cuda_ci(est_time=600, stage="base-b", runner_config="1-gpu-large")
|
||||
register_cuda_ci(est_time=250, stage="base-b", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestEagle3Fa3(Eagle3Base, SpecCorrectnessKit, SpecAccuracyKit, SpecLogprobKit):
|
||||
"""EAGLE3 spec v2 topk=1 on fa3 (the H200 default backend)."""
|
||||
class TestEagle3Fa3(Eagle3Base, SpecAccuracyKit, SpecLogprobKit):
|
||||
"""EAGLE3 topk=1 on fa3 (the H200 default backend), overlap on.
|
||||
|
||||
No SpecCorrectnessKit: those checks are scheduler/sampling behaviour, which
|
||||
the 5090 runs already cover. Logprob losslessness stays -- it reads through
|
||||
the verify output, which the attention unit cases do not reach.
|
||||
"""
|
||||
|
||||
attention_backend = "fa3"
|
||||
disable_overlap = False
|
||||
@@ -37,7 +41,7 @@ class TestEagleLlama2Fa3Page256(
|
||||
SpecPerfKit,
|
||||
SpecFeatureKit,
|
||||
):
|
||||
"""EAGLE/Llama-2 topk=5 tree on fa3 + page_size=256 (spec v1)."""
|
||||
"""EAGLE/Llama-2 topk=5 tree on fa3 + page_size=256, overlap off."""
|
||||
|
||||
spec_topk = 5
|
||||
spec_steps = 8
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""page_size > 1 variants at topk=1 (flashinfer).
|
||||
"""EAGLE3 chain drafting (topk=1) at page_size > 1, flashinfer.
|
||||
|
||||
EAGLE3 page64 (spec v2) + EAGLE/Llama-2 page4 (spec v1). topk>1 page variants
|
||||
live in test_spec_eagle_topk.py. Runs on the cheap (5090) runner.
|
||||
topk=1 takes its own fast path in the draft worker, so this cell is not
|
||||
covered by the tree variants in test_spec_eagle_topk_page.py.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
@@ -10,30 +10,28 @@ from sglang.srt.environ import envs
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.spec_server_kits import (
|
||||
SpecAccuracyKit,
|
||||
SpecCorrectnessKit,
|
||||
SpecFeatureKit,
|
||||
SpecLogprobKit,
|
||||
)
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base, EagleLlama2Base
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base
|
||||
|
||||
register_cuda_ci(est_time=360, stage="base-b", runner_config="1-gpu-small")
|
||||
register_cuda_ci(est_time=230, stage="base-b", runner_config="1-gpu-small")
|
||||
|
||||
|
||||
class TestEagle3Page64(Eagle3Base, SpecAccuracyKit, SpecLogprobKit, SpecFeatureKit):
|
||||
"""EAGLE3 spec v2, page_size=64 (flashinfer): + logprob losslessness."""
|
||||
class TestEagle3Page64(
|
||||
Eagle3Base,
|
||||
SpecCorrectnessKit,
|
||||
SpecAccuracyKit,
|
||||
SpecLogprobKit,
|
||||
SpecFeatureKit,
|
||||
):
|
||||
"""Overlap scheduler, page_size=64: + logprob losslessness."""
|
||||
|
||||
page_size = 64
|
||||
disable_overlap = False
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
|
||||
|
||||
class TestEagleLlama2Page4Topk1(EagleLlama2Base, SpecAccuracyKit, SpecFeatureKit):
|
||||
"""Llama-2 topk=1 + page_size=4."""
|
||||
|
||||
spec_topk = 1
|
||||
spec_tokens = 6
|
||||
page_size = 4
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -27,7 +27,7 @@ class _Eagle3ParityBase(Eagle3Base):
|
||||
|
||||
@unittest.skipIf(_is_xpu, "CUDA runner only")
|
||||
class TestEagle3ParityCUDA(SpecParityKit, _Eagle3ParityBase):
|
||||
"""EAGLE3 spec v2 (flashinfer, overlap) greedy output == non-spec reference.
|
||||
"""EAGLE3 (flashinfer, overlap) greedy output == non-spec reference.
|
||||
|
||||
SpecParityKit is first so its setUpClass runs the reference server (and tears
|
||||
it down) before the fixture launches the spec server -- sequential, one model
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
"""Perf + stress: throughput, retract-under-pressure, abort storms, timeouts.
|
||||
"""Perf + stress: throughput and retract-under-pressure.
|
||||
|
||||
These need memory headroom / measure load behavior, so they run on the large
|
||||
(Hopper) runner.
|
||||
These need memory headroom / measure load behaviour, so they run on the large
|
||||
(Hopper) runner. The scheduler timeout paths carry no spec-specific state, so
|
||||
they live in unit/managers/test_scheduler_timeouts.py plus the cheap e2e in
|
||||
scheduler/test_scheduler_control.py.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.abort_timeout_kit import (
|
||||
AbortAllMixin,
|
||||
RunningTimeoutTwoWaveMixin,
|
||||
WaitingTimeoutMixin,
|
||||
)
|
||||
from sglang.test.kits.spec_server_kits import (
|
||||
SpecAccuracyKit,
|
||||
SpecFeatureKit,
|
||||
@@ -20,11 +17,11 @@ from sglang.test.kits.spec_server_kits import (
|
||||
)
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base, EagleLlama2Base
|
||||
|
||||
register_cuda_ci(est_time=780, stage="base-b", runner_config="1-gpu-large")
|
||||
register_cuda_ci(est_time=440, stage="base-b", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestEagle3Perf(Eagle3Base, SpecPerfKit):
|
||||
"""Decode throughput (max_new_tokens=1) on EAGLE3 spec v2."""
|
||||
"""Decode throughput (max_new_tokens=1) on EAGLE3."""
|
||||
|
||||
disable_overlap = False
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
@@ -42,7 +39,7 @@ class TestEagleLlama2Retract(EagleLlama2Base, SpecAccuracyKit, SpecFeatureKit):
|
||||
|
||||
|
||||
class TestEagle3Topk16V2Retract(Eagle3Base, SpecAccuracyKit, SpecFeatureKit):
|
||||
"""EAGLE3 topk=16 tree on spec v2 under retract; must not leak KV. Stresses
|
||||
"""EAGLE3 topk=16 tree under retract; must not leak KV. Stresses
|
||||
the accepted-path KV move (move_accept_tokens_to_target_kvcache)."""
|
||||
|
||||
spec_topk = 16
|
||||
@@ -58,27 +55,5 @@ class TestEagle3Topk16V2Retract(Eagle3Base, SpecAccuracyKit, SpecFeatureKit):
|
||||
)
|
||||
|
||||
|
||||
class TestEagleLlama2AbortAll(EagleLlama2Base, AbortAllMixin):
|
||||
abort_all_max_new_tokens = 4000
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
|
||||
|
||||
class TestEagleLlama2WaitingTimeout(EagleLlama2Base, WaitingTimeoutMixin):
|
||||
max_running_requests = 1
|
||||
env_overrides = (
|
||||
(envs.SGLANG_REQ_WAITING_TIMEOUT, 0.001),
|
||||
(envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),
|
||||
)
|
||||
|
||||
|
||||
class TestEagleLlama2RunningTimeout(EagleLlama2Base, RunningTimeoutTwoWaveMixin):
|
||||
# Regression: https://github.com/sgl-project/sglang/pull/18760
|
||||
max_running_requests = 16
|
||||
env_overrides = (
|
||||
(envs.SGLANG_REQ_RUNNING_TIMEOUT, 3),
|
||||
(envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""topk > 1 tree drafting (EAGLE3 topk16 + EAGLE/Llama-2 topk8).
|
||||
"""topk > 1 tree drafting at page_size=1 (EAGLE3 topk16 + EAGLE/Llama-2 topk8).
|
||||
|
||||
topk > 1 routes to spec v1, except page_size==1 which can also stay on spec v2
|
||||
(overlap). flashinfer is pinned because this runs on the cheap (5090) runner,
|
||||
where fa3 (Hopper-only) isn't available -- functional sanity only, no perf/stress.
|
||||
flashinfer is pinned because this runs on the cheap (5090) runner, where fa3
|
||||
(Hopper-only) isn't available -- functional sanity only, no perf/stress.
|
||||
(topk > 1 on fa3 is covered on the Hopper runner in test_spec_eagle_fa3.py.)
|
||||
"""
|
||||
|
||||
@@ -10,24 +9,37 @@ import unittest
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.abort_timeout_kit import AbortAllMixin
|
||||
from sglang.test.kits.spec_server_kits import (
|
||||
SpecAccuracyKit,
|
||||
SpecCorrectnessKit,
|
||||
SpecFeatureKit,
|
||||
SpecHiddenStatesKit,
|
||||
SpecLogprobKit,
|
||||
SpecPenaltyKit,
|
||||
)
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base, EagleLlama2Base
|
||||
|
||||
register_cuda_ci(est_time=1180, stage="base-b", runner_config="1-gpu-small")
|
||||
register_cuda_ci(est_time=870, stage="base-b", runner_config="1-gpu-small")
|
||||
|
||||
|
||||
class TestEagle3Topk16(Eagle3Base, SpecCorrectnessKit, SpecAccuracyKit, SpecLogprobKit):
|
||||
"""EAGLE3 topk=16 tree (spec v1): correctness + gsm8k + logprob losslessness."""
|
||||
class TestEagle3Topk16(
|
||||
Eagle3Base,
|
||||
SpecCorrectnessKit,
|
||||
SpecAccuracyKit,
|
||||
SpecLogprobKit,
|
||||
SpecFeatureKit,
|
||||
SpecHiddenStatesKit,
|
||||
):
|
||||
"""EAGLE3 topk=16 tree, overlap scheduler: guards the accepted-path
|
||||
compaction (via logprob_decode_match_prefill) and the per-request
|
||||
hidden-state stride slicing that the same compaction feeds.
|
||||
"""
|
||||
|
||||
spec_topk = 16
|
||||
spec_tokens = 64
|
||||
disable_overlap = True # synchronous baseline; SpecV2 subclass flips overlap on
|
||||
disable_overlap = False
|
||||
enable_return_hidden_states = True
|
||||
cuda_graph_max_bs_decode = 5
|
||||
acc_length_thres = 3.1
|
||||
batch_accept_len_thres = 1.75
|
||||
@@ -35,13 +47,6 @@ class TestEagle3Topk16(Eagle3Base, SpecCorrectnessKit, SpecAccuracyKit, SpecLogp
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
|
||||
|
||||
class TestEagle3Topk16SpecV2(TestEagle3Topk16, SpecFeatureKit):
|
||||
"""EAGLE3 topk=16 tree on spec v2 (overlap, page1): guards the v2 tree path's
|
||||
accepted-path compaction, validated by logprob_spec_v2_match."""
|
||||
|
||||
disable_overlap = False
|
||||
|
||||
|
||||
class TestEagleLlama2Suite(
|
||||
EagleLlama2Base,
|
||||
SpecCorrectnessKit,
|
||||
@@ -49,9 +54,16 @@ class TestEagleLlama2Suite(
|
||||
SpecLogprobKit,
|
||||
SpecPenaltyKit,
|
||||
SpecFeatureKit,
|
||||
AbortAllMixin,
|
||||
):
|
||||
"""EAGLE/Llama-2 topk=8 full coverage (kits listed in bases)."""
|
||||
"""EAGLE/Llama-2 topk=8 full coverage (kits listed in bases).
|
||||
|
||||
Hosts AbortAllMixin: aborting mid-decode has to release the tree draft
|
||||
state, and the strict mem check below turns a leak into a failure. It needs
|
||||
no server flags of its own, so it rides this launch.
|
||||
"""
|
||||
|
||||
abort_all_max_new_tokens = 4000
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""topk > 1 tree drafting at page_size > 1 (EAGLE3 topk8 + EAGLE/Llama-2 topk8).
|
||||
"""EAGLE3 tree drafting (topk > 1) at page_size > 1, flashinfer (fa3 is
|
||||
Hopper-only).
|
||||
|
||||
page64 stays on spec v2 (overlap), page4 runs on spec v1 (no overlap). flashinfer is
|
||||
pinned because this runs on the cheap (5090) runner, where fa3 (Hopper-only) isn't
|
||||
available -- functional sanity only, no perf/stress. (page>1 topk>1 on fa3 is covered
|
||||
on the Hopper runner in test_spec_eagle_fa3.py.)
|
||||
page_size=4 with 32 draft tokens spreads the draft window over several pages --
|
||||
the layout the unit fixture refuses to build (tree draft is pinned to
|
||||
page_size=1 there, see speculative_draft_runner.py). The window-inside-one-page
|
||||
regime is covered by test_spec_eagle_fa3.py page256 on the Hopper runner.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
@@ -13,29 +14,26 @@ from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.spec_server_kits import (
|
||||
SpecAccuracyKit,
|
||||
SpecFeatureKit,
|
||||
SpecLogprobKit,
|
||||
)
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base, EagleLlama2Base
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base
|
||||
|
||||
register_cuda_ci(est_time=720, stage="base-b", runner_config="1-gpu-small")
|
||||
register_cuda_ci(est_time=345, stage="base-b", runner_config="1-gpu-small")
|
||||
|
||||
|
||||
class TestEagle3Page64Topk8(Eagle3Base, SpecAccuracyKit, SpecFeatureKit):
|
||||
"""EAGLE3 topk=8 tree + page_size=64 (spec v2)."""
|
||||
class TestEagle3Page4Topk8(Eagle3Base, SpecAccuracyKit, SpecLogprobKit, SpecFeatureKit):
|
||||
"""Overlap scheduler, topk=8 tree, page_size=4."""
|
||||
|
||||
page_size = 64
|
||||
page_size = 4
|
||||
spec_topk = 8
|
||||
spec_tokens = 32
|
||||
disable_overlap = False
|
||||
# Preset accept-length values are topk=1 numbers -- loose for a topk=8
|
||||
# tree; tighten once CI reports the actuals.
|
||||
gsm8k_accept_len_thres = 2.0
|
||||
cuda_graph_max_bs_decode = 5
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
|
||||
|
||||
class TestEagleLlama2Page4Topk8(EagleLlama2Base, SpecAccuracyKit, SpecFeatureKit):
|
||||
"""Llama-2 topk>1 tree + page_size=4 (spec v1)."""
|
||||
|
||||
page_size = 4
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""triton attention backend (EAGLE3 topk=1 chain + EAGLE/Llama-2 topk=8 tree).
|
||||
"""triton attention backend, EAGLE3 chain drafting.
|
||||
|
||||
triton runs everywhere, so this stays on the cheap (5090) runner.
|
||||
triton runs everywhere, so this stays on the cheap (5090) runner. triton tree
|
||||
verify is covered by attention/unittests/dense/test_triton.py, and the tree
|
||||
accept-path compaction e2e lives in test_spec_eagle_topk.py.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
@@ -11,13 +13,12 @@ from sglang.test.kits.matched_stop_kit import MatchedStopMixin
|
||||
from sglang.test.kits.spec_server_kits import (
|
||||
SpecAccuracyKit,
|
||||
SpecFeatureKit,
|
||||
SpecHiddenStatesKit,
|
||||
SpecLogprobKit,
|
||||
SpecPenaltyKit,
|
||||
)
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base, EagleLlama2Base
|
||||
from sglang.test.server_fixtures.spec_eagle_fixture import Eagle3Base
|
||||
|
||||
register_cuda_ci(est_time=350, stage="base-b", runner_config="1-gpu-small")
|
||||
register_cuda_ci(est_time=230, stage="base-b", runner_config="1-gpu-small")
|
||||
|
||||
|
||||
class TestEagle3Triton(
|
||||
@@ -28,7 +29,7 @@ class TestEagle3Triton(
|
||||
SpecPenaltyKit,
|
||||
SpecFeatureKit,
|
||||
):
|
||||
"""EAGLE3 spec v2 on triton (kits listed in bases)."""
|
||||
"""Overlap scheduler on triton (kits listed in bases)."""
|
||||
|
||||
attention_backend = "triton"
|
||||
max_running_requests = 64
|
||||
@@ -38,19 +39,5 @@ class TestEagle3Triton(
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
|
||||
|
||||
class TestEagleLlama2Triton(
|
||||
EagleLlama2Base, SpecAccuracyKit, SpecFeatureKit, SpecHiddenStatesKit
|
||||
):
|
||||
"""EAGLE/Llama-2 topk=8 tree on triton.
|
||||
|
||||
Hosts SpecHiddenStatesKit: topk>1 exercises the tree accept-path
|
||||
compaction that the per-req hidden-state stride slicing depends on.
|
||||
"""
|
||||
|
||||
attention_backend = "triton"
|
||||
enable_return_hidden_states = True
|
||||
env_overrides = ((envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY, 1),)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -4,24 +4,29 @@ from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.json_constrained_kit import JSONConstrainedMixin
|
||||
from sglang.test.kits.regex_constrained_kit import RegexConstrainedMixin
|
||||
from sglang.test.server_fixtures.standalone_fixture import StandaloneServerBase
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
from sglang.test.test_utils import CustomTestCase, is_in_ci
|
||||
|
||||
# V2 standalone speculative decoding tests (FA3, Triton, FlashInfer backends).
|
||||
# V2 standalone speculative decoding. CI runs only fa3 (the backend this is
|
||||
# deployed on); triton / flashinfer stay runnable locally, and their spec verify
|
||||
# numerics live in attention/unittests/dense/test_{triton,flashinfer}.py.
|
||||
# Non-V2 backends moved to test_spec_standalone_extra.py.
|
||||
register_cuda_ci(est_time=450, stage="base-b", runner_config="1-gpu-large")
|
||||
register_cuda_ci(est_time=170, stage="base-b", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
class TestStandaloneV2SpeculativeDecodingBase(StandaloneServerBase, CustomTestCase):
|
||||
class TestStandaloneV2SpeculativeDecodingBase(
|
||||
StandaloneServerBase, CustomTestCase, RegexConstrainedMixin, JSONConstrainedMixin
|
||||
):
|
||||
# Hosts the constrained mixins: overlap is on, so they exercise the
|
||||
# grammar barrier path.
|
||||
attention_backend = "fa3"
|
||||
|
||||
|
||||
class TestStandaloneV2SpeculativeDecodingTriton(
|
||||
StandaloneServerBase, CustomTestCase, RegexConstrainedMixin, JSONConstrainedMixin
|
||||
):
|
||||
# Constrained mixins reuse this server; overlap on -> grammar barrier path.
|
||||
@unittest.skipIf(is_in_ci(), "CI covers fa3 only; run locally for triton.")
|
||||
class TestStandaloneV2SpeculativeDecodingTriton(StandaloneServerBase, CustomTestCase):
|
||||
attention_backend = "triton"
|
||||
|
||||
|
||||
@unittest.skipIf(is_in_ci(), "CI covers fa3 only; run locally for flashinfer.")
|
||||
class TestStandaloneV2SpeculativeDecodingFlashinfer(
|
||||
StandaloneServerBase, CustomTestCase
|
||||
):
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Boundary tests for the scheduler's waiting / running request timeouts.
|
||||
|
||||
Both paths are pure bookkeeping over timestamps -- no model, no GPU, no draft
|
||||
worker -- so they are driven here directly instead of through a server. The
|
||||
e2e side (503 reaching the client, server stays up) is covered by
|
||||
scheduler/test_scheduler_control.py.
|
||||
"""
|
||||
|
||||
import time
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
from sglang.test.test_utils import CustomTestCase, maybe_stub_sgl_kernel
|
||||
|
||||
maybe_stub_sgl_kernel()
|
||||
|
||||
from sglang.srt.managers.scheduler import Scheduler
|
||||
|
||||
register_cpu_ci(est_time=6, suite="base-a-test-cpu")
|
||||
|
||||
|
||||
class _FakeReq:
|
||||
"""Must stay hashable: the waiting-timeout path collects drops in a set."""
|
||||
|
||||
def __init__(self, rid, wait_entry=0.0, forward_entry=0.0, is_finished=False):
|
||||
self.rid = rid
|
||||
self.to_finish = None
|
||||
self._finished = is_finished
|
||||
self.time_stats = SimpleNamespace(
|
||||
wait_queue_entry_time=wait_entry,
|
||||
forward_entry_time=forward_entry,
|
||||
trace_ctx=MagicMock(),
|
||||
)
|
||||
|
||||
def finished(self):
|
||||
return self._finished
|
||||
|
||||
|
||||
def _req(
|
||||
rid: str, *, wait_entry: float = 0.0, forward_entry: float = 0.0, finished=False
|
||||
):
|
||||
return _FakeReq(rid, wait_entry, forward_entry, finished)
|
||||
|
||||
|
||||
def _scheduler(waiting_queue):
|
||||
s = Scheduler.__new__(Scheduler)
|
||||
s.waiting_queue = waiting_queue
|
||||
s.enable_hicache_storage = False
|
||||
s.ipc_channels = SimpleNamespace(send_to_tokenizer=MagicMock())
|
||||
return s
|
||||
|
||||
|
||||
class TestWaitingTimeout(CustomTestCase):
|
||||
def test_drops_only_reqs_past_the_deadline(self):
|
||||
now = time.perf_counter()
|
||||
stale = _req("stale", wait_entry=now - 10)
|
||||
fresh = _req("fresh", wait_entry=now)
|
||||
s = _scheduler([stale, fresh])
|
||||
|
||||
with envs.SGLANG_REQ_WAITING_TIMEOUT.override(1.0):
|
||||
s._abort_on_waiting_timeout()
|
||||
|
||||
self.assertEqual([r.rid for r in s.waiting_queue], ["fresh"])
|
||||
self.assertEqual(s.ipc_channels.send_to_tokenizer.send_output.call_count, 1)
|
||||
|
||||
def test_unset_entry_time_is_never_dropped(self):
|
||||
# 0 is the "not yet stamped" sentinel; the guard is `0 < entry_time`.
|
||||
s = _scheduler([_req("unstamped", wait_entry=0.0)])
|
||||
with envs.SGLANG_REQ_WAITING_TIMEOUT.override(1e-9):
|
||||
s._abort_on_waiting_timeout()
|
||||
self.assertEqual(len(s.waiting_queue), 1)
|
||||
s.ipc_channels.send_to_tokenizer.send_output.assert_not_called()
|
||||
|
||||
def test_disabled_timeout_is_a_no_op(self):
|
||||
s = _scheduler([_req("stale", wait_entry=time.perf_counter() - 100)])
|
||||
with envs.SGLANG_REQ_WAITING_TIMEOUT.override(0):
|
||||
s._abort_on_waiting_timeout()
|
||||
self.assertEqual(len(s.waiting_queue), 1)
|
||||
|
||||
|
||||
class TestRunningTimeout(CustomTestCase):
|
||||
@staticmethod
|
||||
def _batch(reqs):
|
||||
return SimpleNamespace(reqs=reqs, is_empty=lambda: not reqs)
|
||||
|
||||
def test_marks_only_stale_unfinished_reqs(self):
|
||||
now = time.perf_counter()
|
||||
stale = _req("stale", forward_entry=now - 10)
|
||||
fresh = _req("fresh", forward_entry=now)
|
||||
done = _req("done", forward_entry=now - 10, finished=True)
|
||||
s = _scheduler([])
|
||||
|
||||
with envs.SGLANG_REQ_RUNNING_TIMEOUT.override(1.0):
|
||||
s._abort_on_running_timeout(self._batch([stale, fresh, done]))
|
||||
|
||||
self.assertIsNotNone(stale.to_finish)
|
||||
self.assertIsNone(fresh.to_finish)
|
||||
self.assertIsNone(done.to_finish, "a finished req must not be aborted")
|
||||
|
||||
def test_unset_forward_entry_time_is_never_marked(self):
|
||||
s = _scheduler([])
|
||||
req = _req("unstamped", forward_entry=0.0)
|
||||
with envs.SGLANG_REQ_RUNNING_TIMEOUT.override(1e-9):
|
||||
s._abort_on_running_timeout(self._batch([req]))
|
||||
self.assertIsNone(req.to_finish)
|
||||
|
||||
def test_empty_batch_and_disabled_timeout_are_no_ops(self):
|
||||
s = _scheduler([])
|
||||
with envs.SGLANG_REQ_RUNNING_TIMEOUT.override(1.0):
|
||||
s._abort_on_running_timeout(self._batch([]))
|
||||
req = _req("stale", forward_entry=time.perf_counter() - 100)
|
||||
with envs.SGLANG_REQ_RUNNING_TIMEOUT.override(0):
|
||||
s._abort_on_running_timeout(self._batch([req]))
|
||||
self.assertIsNone(req.to_finish)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user