Fix gpt_oss_common import path and migrate core tests (#16426)

This commit is contained in:
Alison Shao
2026-01-07 12:58:32 -08:00
committed by GitHub
parent 3e73e12458
commit 0c474273c5
15 changed files with 48 additions and 26 deletions
+1 -24
View File
@@ -9,24 +9,12 @@ from sglang.test.ci.ci_utils import TestFile, run_unittest_files
# NOTE: please sort the test cases alphabetically by the test file name
suites = {
"per-commit-1-gpu": [
TestFile("test_deterministic.py", 228),
TestFile("test_evs.py", 20),
TestFile("test_external_models.py", 30),
TestFile("test_gpt_oss_1gpu.py", 402),
TestFile("test_hidden_states.py", 55),
TestFile("test_input_embeddings.py", 38),
TestFile("test_io_struct.py", 8),
TestFile("test_jinja_template_utils.py", 7),
TestFile("test_model_hooks.py", 6),
TestFile("test_modelopt_loader.py", 11),
TestFile("test_multi_tokenizer.py", 230),
TestFile("test_page_size.py", 60),
TestFile("test_request_queue_validation.py", 47),
TestFile("test_score_api.py", 260),
TestFile("test_server_args.py", 9),
TestFile("test_skip_tokenizer_init.py", 77),
TestFile("test_srt_endpoint.py", 127),
TestFile("test_srt_engine.py", 252),
TestFile("test_utils_update_weights.py", 29),
TestFile("test_video_utils.py", 5),
TestFile("test_modelopt_export.py", 9),
@@ -98,7 +86,6 @@ suites = {
TestFile("test_bench_one_batch.py"),
TestFile("test_bench_serving.py"),
TestFile("test_eval_accuracy_large.py"),
TestFile("test_gpt_oss_common.py"),
TestFile("test_moe_eval_accuracy_large.py"),
TestFile("test_profile_v2.py"),
TestFile("models/test_ministral3_models.py"),
@@ -120,18 +107,10 @@ suite_amd = {
# TestFile("lora/test_lora_qwen3.py", 97), # Disabled temporarily, see https://github.com/sgl-project/sglang/issues/13107
TestFile("test_bench_typebaseddispatcher.py", 10),
TestFile("test_external_models.py", 45),
TestFile("test_input_embeddings.py", 38),
TestFile("test_io_struct.py", 8),
TestFile("test_jinja_template_utils.py", 1),
TestFile("test_model_hooks.py", 10),
TestFile("test_multi_tokenizer.py", 345),
TestFile("test_page_size.py", 60),
TestFile("test_request_queue_validation.py", 70),
TestFile("test_rope_rocm.py", 3),
TestFile("test_server_args.py", 1),
TestFile("test_skip_tokenizer_init.py", 117),
TestFile("test_srt_endpoint.py", 130),
TestFile("test_srt_engine.py", 261),
# TestFile("test_torch_compile_moe.py", 210), # Disabled temporarily, see https://github.com/sgl-project/sglang/issues/13107
TestFile("test_type_based_dispatcher.py", 10),
TestFile("test_video_utils.py", 8),
@@ -143,9 +122,7 @@ suite_amd = {
# TestFile("test_vision_chunked_prefill.py", 175), # Disabled temporarily and track in #7701
# TestFile("test_wave_attention_backend.py", 150), # Disabled temporarily, see https://github.com/sgl-project/sglang/issues/11127
],
"per-commit-amd-mi35x": [
TestFile("test_gpt_oss_1gpu.py", 750),
],
"per-commit-amd-mi35x": [],
"per-commit-2-gpu-amd": [
TestFile("test_data_parallelism.py", 73),
TestFile("test_load_weights_from_remote_instance.py", 72),
-61
View File
@@ -1,61 +0,0 @@
"""
Usage:
cd test/srt
python3 -m unittest test_deterministic.TestDeterministic.TESTCASE
Note that there is also `python/sglang/test/test_deterministic.py` as an interactive test. We are converting that
test into unit tests so that's easily reproducible in CI.
"""
import unittest
from sglang.test.test_deterministic_utils import (
COMMON_SERVER_ARGS,
TestDeterministicBase,
)
class TestFlashinferDeterministic(TestDeterministicBase):
# Test with flashinfer attention backend
@classmethod
def get_server_args(cls):
args = COMMON_SERVER_ARGS
args.extend(
[
"--attention-backend",
"flashinfer",
]
)
return args
class TestFa3Deterministic(TestDeterministicBase):
# Test with fa3 attention backend
@classmethod
def get_server_args(cls):
args = COMMON_SERVER_ARGS
args.extend(
[
"--attention-backend",
"fa3",
]
)
return args
class TestTritonDeterministic(TestDeterministicBase):
# Test with triton attention backend
@classmethod
def get_server_args(cls):
args = COMMON_SERVER_ARGS
args.extend(
[
"--attention-backend",
"triton",
]
)
return args
if __name__ == "__main__":
unittest.main()
-31
View File
@@ -1,31 +0,0 @@
import unittest
from test_gpt_oss_common import BaseTestGptOss
class TestGptOss1Gpu(BaseTestGptOss):
def test_mxfp4_20b(self):
self.run_test(
model_variant="20b",
quantization="mxfp4",
expected_score_of_reasoning_effort={
"low": 0.34,
"medium": 0.34,
"high": 0.27, # TODO investigate
},
)
def test_bf16_20b(self):
self.run_test(
model_variant="20b",
quantization="bf16",
expected_score_of_reasoning_effort={
"low": 0.34,
"medium": 0.34,
"high": 0.27, # TODO investigate
},
)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -1,6 +1,6 @@
import unittest
from test_gpt_oss_common import BaseTestGptOss
from sglang.test.gpt_oss_common import BaseTestGptOss
class TestGptOss4Gpu(BaseTestGptOss):
-152
View File
@@ -1,152 +0,0 @@
import json
import os
from concurrent.futures import ThreadPoolExecutor
from types import SimpleNamespace
from typing import Dict, List, Literal, Optional
import requests
from sglang.srt.utils import is_hip, kill_process_tree
from sglang.test.run_eval import run_eval
from sglang.test.test_utils import (
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST,
CustomTestCase,
is_in_ci,
popen_launch_server,
write_github_step_summary,
)
_base_url = DEFAULT_URL_FOR_TEST
_is_hip = is_hip()
class BaseTestGptOss(CustomTestCase):
def run_test(
self,
model_variant: Literal["20b", "120b"],
quantization: Literal["mxfp4", "bf16"],
expected_score_of_reasoning_effort: Dict[str, float],
other_args: Optional[List[str]] = None,
):
if other_args is None:
other_args = []
model = {
("20b", "bf16"): "lmsys/gpt-oss-20b-bf16",
("120b", "bf16"): "lmsys/gpt-oss-120b-bf16",
("20b", "mxfp4"): "openai/gpt-oss-20b",
("120b", "mxfp4"): "openai/gpt-oss-120b",
}[(model_variant, quantization)]
if model_variant == "20b":
other_args += ["--cuda-graph-max-bs", "600"]
if _is_hip:
os.environ["SGLANG_USE_AITER"] = "0"
self._run_test_raw(
model=model,
expected_score_of_reasoning_effort=expected_score_of_reasoning_effort,
other_args=other_args,
)
def _run_test_raw(
self,
model: str,
expected_score_of_reasoning_effort: Dict[str, float],
other_args: List[str],
):
process = popen_launch_server(
model,
_base_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
other_args=other_args,
)
try:
self._check_streaming_responses_api_request(model)
# run multiple tests in parallel since we are mostly bound by the longest generate sequence
# instead of the number of questions
with ThreadPoolExecutor(max_workers=4) as executor:
list(
executor.map(
lambda d: self._run_one_eval(**d),
[
dict(
model=model,
reasoning_effort=reasoning_effort,
expected_score=expected_score,
)
for reasoning_effort, expected_score in expected_score_of_reasoning_effort.items()
],
)
)
finally:
kill_process_tree(process.pid)
def _check_streaming_responses_api_request(self, model):
# Use requests to verify /v1/responses streaming
url = f"{_base_url}/v1/responses"
payload = {
"model": model,
"input": "What is 1 + 1?",
"stream": True,
"temperature": 0,
}
response = requests.post(url, json=payload, stream=True)
if response.status_code != 200:
print(f"Response API failed: {response.text}")
response.raise_for_status()
content = ""
for line in response.iter_lines():
if line:
decoded_line = line.decode("utf-8")
if decoded_line.startswith("data: "):
data_str = decoded_line[6:]
if data_str.strip() == "[DONE]":
break
try:
data = json.loads(data_str)
if data.get("type") == "response.output_text.delta":
delta = data.get("delta", "")
content += delta
except json.JSONDecodeError:
pass
print(f"Streaming check response: {content}")
self.assertTrue(len(content) > 0)
self.assertIn("2", content)
def _run_one_eval(self, model, reasoning_effort, expected_score):
args = SimpleNamespace(
base_url=_base_url,
model=model,
eval_name="gpqa",
num_examples=198,
# use enough threads to allow parallelism
num_threads=198,
# TODO 4k is still not enough, we need e.g. 64k token, but that is super slow
# otherwise a lot of questions are not answered
max_tokens=4096,
# simple-evals by default use 0.5 and is better than 0.0 temperature
# but here for reproducibility, we use 0.1
temperature=0.1,
reasoning_effort=reasoning_effort,
)
setup = f"model={model} reasoning_effort={reasoning_effort} expected_score={expected_score}"
print(f"Evaluation start: {setup}")
metrics = run_eval(args)
print(f"Evaluation end: {setup} {metrics=}")
self.assertGreaterEqual(metrics["score"], expected_score)
if is_in_ci():
write_github_step_summary(
f"### test_gpt_oss_common\n"
f"Setup: {setup}\n"
f"Score: {metrics['score']:.2f}\n"
)
-139
View File
@@ -1,139 +0,0 @@
import unittest
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
import sglang as sgl
from sglang.test.test_utils import DEFAULT_SMALL_MODEL_NAME_FOR_TEST, CustomTestCase
class TestHiddenState(CustomTestCase):
def test_return_hidden_states(self):
prompts = ["Today is", "Today is a sunny day and I like"]
model_path = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
tokenizer = AutoTokenizer.from_pretrained(model_path)
input_ids = tokenizer(prompts).input_ids
sampling_params = {
"temperature": 0,
"max_new_tokens": 8,
}
engine = sgl.Engine(
model_path=model_path,
random_seed=42,
skip_tokenizer_init=True,
enable_return_hidden_states=True,
)
outputs = engine.generate(
input_ids=input_ids,
sampling_params=sampling_params,
return_hidden_states=True,
)
engine.shutdown()
for output in outputs:
self.assertEqual(len(output["meta_info"]["hidden_states"]), 8)
for i in range(len(output["meta_info"]["hidden_states"])):
assert isinstance(output["meta_info"]["hidden_states"][i], list)
output["meta_info"]["hidden_states"][i] = torch.tensor(
output["meta_info"]["hidden_states"][i], dtype=torch.bfloat16
)
# Checks that splicing of the batch was done correctly
self.assertGreater(
outputs[1]["meta_info"]["hidden_states"][0].shape[0],
outputs[0]["meta_info"]["hidden_states"][0].shape[0],
)
model = AutoModelForCausalLM.from_pretrained(
model_path, torch_dtype=torch.bfloat16, device_map="cuda"
)
for input_id, output in zip(input_ids, outputs):
with torch.inference_mode():
hf_out = model(
torch.tensor(
[input_id + output["output_ids"][:-1]], device=model.device
),
output_hidden_states=True,
)
print("=== HF Hiddens ===")
print(hf_out["hidden_states"][-1][0])
sg_hidden_states = torch.cat(
[
i.unsqueeze(0) if len(i.shape) == 1 else i
for i in output["meta_info"]["hidden_states"]
]
).to("cuda")
print("=== SRT Hiddens ===")
print(sg_hidden_states)
print(
f"Max diff: {torch.max(torch.abs(hf_out['hidden_states'][-1][0] - sg_hidden_states))}"
)
atol = 0.8
self.assertTrue(
torch.allclose(
hf_out["hidden_states"][-1][0],
sg_hidden_states,
atol=atol,
rtol=0,
)
)
def test_repeatedly_changes_hidden_states(self):
prompts = ["Today is", "Today is a sunny day and I like"]
model_path = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
tokenizer = AutoTokenizer.from_pretrained(model_path)
input_ids = tokenizer(prompts).input_ids
sampling_params = {
"temperature": 0,
"max_new_tokens": 8,
}
engine = sgl.Engine(
model_path=model_path,
random_seed=42,
skip_tokenizer_init=True,
enable_return_hidden_states=True,
)
outputs_completion_first_round = engine.generate(
input_ids=input_ids,
sampling_params=sampling_params,
return_hidden_states=True,
)
outputs_hidden_state = engine.generate(
input_ids=input_ids,
sampling_params=sampling_params,
return_hidden_states=False,
)
outputs_completion_last_round = engine.generate(
input_ids=input_ids,
sampling_params=sampling_params,
return_hidden_states=True,
)
engine.shutdown()
for (
output_completion_first_round,
output_hidden_state,
output_completion_last_round,
) in zip(
outputs_completion_first_round,
outputs_hidden_state,
outputs_completion_last_round,
):
self.assertEqual(
len(output_completion_first_round["meta_info"]["hidden_states"]), 8
)
self.assertNotIn("hidden_states", output_hidden_state["meta_info"])
self.assertEqual(
len(output_completion_last_round["meta_info"]["hidden_states"]), 8
)
if __name__ == "__main__":
unittest.main()
-151
View File
@@ -1,151 +0,0 @@
import json
import os
import tempfile
import unittest
import requests
from transformers import AutoModelForCausalLM, AutoTokenizer
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,
)
class TestInputEmbeds(CustomTestCase):
@classmethod
def setUpClass(cls):
cls.model = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
cls.base_url = DEFAULT_URL_FOR_TEST
cls.tokenizer = AutoTokenizer.from_pretrained(cls.model)
cls.ref_model = AutoModelForCausalLM.from_pretrained(cls.model)
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
other_args=["--disable-radix", "--cuda-graph-max-bs", 4],
)
cls.texts = [
"The capital of France is",
"What is the best time of year to visit Japan for cherry blossoms?",
]
def generate_input_embeddings(self, text):
"""Generate input embeddings for a given text."""
input_ids = self.tokenizer(text, return_tensors="pt")["input_ids"]
embeddings = self.ref_model.get_input_embeddings()(input_ids)
return embeddings.squeeze().tolist() # Convert tensor to a list for API use
def send_request(self, payload):
"""Send a POST request to the /generate endpoint and return the response."""
response = requests.post(
self.base_url + "/generate",
json=payload,
timeout=30, # Set a reasonable timeout for the API request
)
if response.status_code == 200:
return response.json()
return {
"error": f"Request failed with status {response.status_code}: {response.text}"
}
def send_file_request(self, file_path):
"""Send a POST request to the /generate_from_file endpoint with a file."""
with open(file_path, "rb") as f:
response = requests.post(
self.base_url + "/generate_from_file",
files={"file": f},
timeout=30, # Set a reasonable timeout for the API request
)
if response.status_code == 200:
return response.json()
return {
"error": f"Request failed with status {response.status_code}: {response.text}"
}
def test_text_based_response(self):
"""Test and print API responses using text-based input."""
for text in self.texts:
payload = {
"model": self.model,
"text": text,
"sampling_params": {"temperature": 0, "max_new_tokens": 50},
}
response = self.send_request(payload)
print(
f"Text Input: {text}\nResponse: {json.dumps(response, indent=2)}\n{'-' * 80}"
)
def test_embedding_based_response(self):
"""Test and print API responses using input embeddings."""
for text in self.texts:
embeddings = self.generate_input_embeddings(text)
payload = {
"model": self.model,
"input_embeds": embeddings,
"sampling_params": {"temperature": 0, "max_new_tokens": 50},
}
response = self.send_request(payload)
print(
f"Embeddings Input (for text '{text}'):\nResponse: {json.dumps(response, indent=2)}\n{'-' * 80}"
)
def test_compare_text_vs_embedding(self):
"""Test and compare responses for text-based and embedding-based inputs."""
for text in self.texts:
# Text-based payload
text_payload = {
"model": self.model,
"text": text,
"sampling_params": {"temperature": 0, "max_new_tokens": 50},
}
# Embedding-based payload
embeddings = self.generate_input_embeddings(text)
embed_payload = {
"model": self.model,
"input_embeds": embeddings,
"sampling_params": {"temperature": 0, "max_new_tokens": 50},
}
# Get responses
text_response = self.send_request(text_payload)
embed_response = self.send_request(embed_payload)
# Print responses
print(
f"Text Input: {text}\nText-Based Response: {json.dumps(text_response, indent=2)}\n"
)
print(
f"Embeddings Input (for text '{text}'):\nEmbedding-Based Response: {json.dumps(embed_response, indent=2)}\n{'-' * 80}"
)
# This is flaky, so we skip this temporarily
# self.assertEqual(text_response["text"], embed_response["text"])
def test_generate_from_file(self):
"""Test the /generate_from_file endpoint using tokenized embeddings."""
for text in self.texts:
embeddings = self.generate_input_embeddings(text)
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
) as tmp_file:
json.dump(embeddings, tmp_file)
tmp_file_path = tmp_file.name
try:
response = self.send_file_request(tmp_file_path)
print(
f"Text Input: {text}\nResponse from /generate_from_file: {json.dumps(response, indent=2)}\n{'-' * 80}"
)
finally:
# Ensure the temporary file is deleted
os.remove(tmp_file_path)
@classmethod
def tearDownClass(cls):
kill_process_tree(cls.process.pid)
if __name__ == "__main__":
unittest.main()
-577
View File
@@ -1,577 +0,0 @@
import copy
import unittest
from sglang.srt.managers.io_struct import GenerateReqInput
from sglang.test.test_utils import (
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
DEFAULT_URL_FOR_TEST,
CustomTestCase,
)
class TestGenerateReqInputNormalization(CustomTestCase):
"""Test the normalization of GenerateReqInput for batch processing and different input formats."""
@classmethod
def setUpClass(cls):
cls.model = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
cls.base_url = DEFAULT_URL_FOR_TEST
def setUp(self):
# Common setup for all tests
self.base_req = GenerateReqInput(
text=["Hello", "World"],
sampling_params=[{}, {}],
rid=["id1", "id2"],
)
def test_single_image_to_list_of_lists(self):
"""Test that a single image is converted to a list of single-image lists."""
req = copy.deepcopy(self.base_req)
req.image_data = "single_image.jpg" # A single image (non-list)
req.normalize_batch_and_arguments()
# Should be converted to [[image], [image]]
self.assertEqual(len(req.image_data), 2)
self.assertEqual(len(req.image_data[0]), 1)
self.assertEqual(len(req.image_data[1]), 1)
self.assertEqual(req.image_data[0][0], "single_image.jpg")
self.assertEqual(req.image_data[1][0], "single_image.jpg")
# Check modalities
self.assertEqual(req.modalities, ["image", "image"])
def test_list_of_images_to_list_of_lists(self):
"""Test that a list of images is converted to a list of single-image lists."""
req = copy.deepcopy(self.base_req)
req.image_data = ["image1.jpg", "image2.jpg"] # List of images
req.normalize_batch_and_arguments()
# Should be converted to [[image1], [image2]]
self.assertEqual(len(req.image_data), 2)
self.assertEqual(len(req.image_data[0]), 1)
self.assertEqual(len(req.image_data[1]), 1)
self.assertEqual(req.image_data[0][0], "image1.jpg")
self.assertEqual(req.image_data[1][0], "image2.jpg")
# Check modalities
self.assertEqual(req.modalities, ["image", "image"])
def test_list_of_lists_with_different_modalities(self):
"""Test handling of list of lists of images with different modalities."""
req = copy.deepcopy(self.base_req)
req.image_data = [
["image1.jpg"], # Single image (image modality)
["image2.jpg", "image3.jpg"], # Multiple images (multi-images modality)
]
req.normalize_batch_and_arguments()
# Structure should remain the same
self.assertEqual(len(req.image_data), 2)
self.assertEqual(len(req.image_data[0]), 1)
self.assertEqual(len(req.image_data[1]), 2)
# Check modalities
self.assertEqual(req.modalities, ["image", "multi-images"])
def test_list_of_lists_with_none_values(self):
"""Test handling of list of lists with None values."""
req = copy.deepcopy(self.base_req)
req.image_data = [
[None], # None value
["image.jpg"], # Single image
]
req.normalize_batch_and_arguments()
# Structure should remain the same
self.assertEqual(len(req.image_data), 2)
self.assertEqual(len(req.image_data[0]), 1)
self.assertEqual(len(req.image_data[1]), 1)
# Check modalities
self.assertEqual(req.modalities, [None, "image"])
def test_expanding_parallel_sample_correlation(self):
"""Test that when expanding with parallel samples, prompts, images and modalities are properly correlated."""
req = copy.deepcopy(self.base_req)
req.text = ["Prompt 1", "Prompt 2"]
req.image_data = [
["image1.jpg"],
["image2.jpg", "image3.jpg"],
]
req.sampling_params = {"n": 3} # All prompts get 3 samples
# Define expected values before normalization
expected_text = req.text * 3
expected_images = req.image_data * 3
expected_modalities = ["image", "multi-images"] * 3
req.normalize_batch_and_arguments()
# Should be expanded to 6 items (2 original * 3 parallel)
self.assertEqual(len(req.image_data), 6)
# Check that images are properly expanded
self.assertEqual(req.image_data, expected_images)
# Check modalities
self.assertEqual(req.modalities, expected_modalities)
# Ensure that text items are properly duplicated too
self.assertEqual(req.text, expected_text)
def test_specific_parallel_n_per_sample(self):
"""Test parallel expansion when different samples have different n values."""
req = copy.deepcopy(self.base_req)
req.text = ["Prompt 1", "Prompt 2"]
req.image_data = [
["image1.jpg"],
["image2.jpg", "image3.jpg"],
]
req.sampling_params = [
{"n": 2},
{"n": 2},
] # First prompt gets 2 samples, second prompt gets 2 samples
expected_images = req.image_data * 2
expected_modalities = ["image", "multi-images"] * 2
expected_text = req.text * 2
req.normalize_batch_and_arguments()
# Should be expanded to 4 items (2 original * 2 parallel)
self.assertEqual(len(req.image_data), 4)
# Check that the first 2 are copies for the first prompt
self.assertEqual(req.image_data, expected_images)
# Check modalities
self.assertEqual(req.modalities, expected_modalities)
# Check text expansion
self.assertEqual(req.text, expected_text)
def test_mixed_none_and_images_with_parallel_samples(self):
"""Test that when some batch items have images and others None, parallel expansion works correctly."""
req = copy.deepcopy(self.base_req)
req.text = ["Prompt 1", "Prompt 2", "Prompt 3"]
req.rid = ["id1", "id2", "id3"]
req.image_data = [
["image1.jpg"],
None,
["image3_1.jpg", "image3_2.jpg"],
]
req.sampling_params = {"n": 2} # All prompts get 2 samples
expected_images = req.image_data * 2
expected_modalities = ["image", None, "multi-images"] * 2
expected_text = req.text * 2
req.normalize_batch_and_arguments()
# Should be expanded to 6 items (3 original * 2 parallel)
self.assertEqual(len(req.image_data), 6)
# Check image data
self.assertEqual(req.image_data, expected_images)
# Check modalities
self.assertEqual(req.modalities, expected_modalities)
# Check text expansion
self.assertEqual(req.text, expected_text)
def test_correlation_with_sampling_params(self):
"""Test that sampling parameters are correctly correlated with prompts during expansion."""
req = copy.deepcopy(self.base_req)
req.text = ["Prompt 1", "Prompt 2"]
req.image_data = [
["image1.jpg"],
["image2.jpg"],
]
req.sampling_params = [
{"temperature": 0.7, "n": 2},
{"temperature": 0.9, "n": 2},
]
req.normalize_batch_and_arguments()
# Check sampling params expansion
self.assertEqual(len(req.sampling_params), 4)
self.assertEqual(req.sampling_params[0]["temperature"], 0.7)
self.assertEqual(req.sampling_params[1]["temperature"], 0.9)
self.assertEqual(req.sampling_params[2]["temperature"], 0.7)
self.assertEqual(req.sampling_params[3]["temperature"], 0.9)
# Should be expanded to 4 items (2 original * 2 parallel)
self.assertEqual(len(req.image_data), 4)
# Check correlation with images
self.assertEqual(req.image_data[0], ["image1.jpg"])
self.assertEqual(req.image_data[1], ["image2.jpg"])
self.assertEqual(req.image_data[2], ["image1.jpg"])
self.assertEqual(req.image_data[3], ["image2.jpg"])
def test_single_example_with_image(self):
"""Test handling of single example with image."""
req = GenerateReqInput(
text="Hello",
image_data="single_image.jpg",
)
req.normalize_batch_and_arguments()
# For single examples, image_data doesn't get processed into lists
self.assertEqual(req.image_data, "single_image.jpg")
self.assertIsNone(req.modalities) # Modalities isn't set for single examples
def test_single_to_batch_with_parallel_sampling(self):
"""Test single example converted to batch with parallel sampling."""
req = GenerateReqInput(
text="Hello",
image_data="single_image.jpg",
sampling_params={"n": 3}, # parallel_sample_num = 3
)
# Define expected values before normalization
expected_text = ["Hello"] * 3
req.normalize_batch_and_arguments()
# Should be converted to batch with text=["Hello"]
self.assertEqual(req.text, expected_text)
# Image should be automatically wrapped to list of lists with length 1*3=3
self.assertEqual(len(req.image_data), 3)
self.assertEqual(req.image_data[0][0], "single_image.jpg")
self.assertEqual(req.image_data[1][0], "single_image.jpg")
self.assertEqual(req.image_data[2][0], "single_image.jpg")
# Modalities should be set for all 3 examples
self.assertEqual(req.modalities, ["image", "image", "image"])
def test_audio_data_handling(self):
"""Test handling of audio_data."""
req = copy.deepcopy(self.base_req)
req.audio_data = "audio.mp3" # Single audio
req.normalize_batch_and_arguments()
# Should be converted to ["audio.mp3", "audio.mp3"]
self.assertEqual(len(req.audio_data), 2)
self.assertEqual(req.audio_data[0], "audio.mp3")
self.assertEqual(req.audio_data[1], "audio.mp3")
# Test with list
req = copy.deepcopy(self.base_req)
req.audio_data = ["audio1.mp3", "audio2.mp3"]
req.normalize_batch_and_arguments()
# Should remain the same
self.assertEqual(len(req.audio_data), 2)
self.assertEqual(req.audio_data[0], "audio1.mp3")
self.assertEqual(req.audio_data[1], "audio2.mp3")
def test_input_ids_normalization(self):
"""Test normalization of input_ids instead of text."""
# Test single input_ids
req = GenerateReqInput(input_ids=[1, 2, 3])
req.normalize_batch_and_arguments()
self.assertTrue(req.is_single)
self.assertEqual(req.batch_size, 1)
# Test batch input_ids
req = GenerateReqInput(input_ids=[[1, 2, 3], [4, 5, 6]])
req.normalize_batch_and_arguments()
self.assertFalse(req.is_single)
self.assertEqual(req.batch_size, 2)
# Test with parallel sampling
req = GenerateReqInput(
input_ids=[[1, 2, 3], [4, 5, 6]], sampling_params={"n": 2}
)
req.normalize_batch_and_arguments()
self.assertEqual(len(req.input_ids), 4) # 2 original * 2 parallel
def test_input_embeds_normalization(self):
"""Test normalization of input_embeds."""
# Test single input_embeds
req = GenerateReqInput(input_embeds=[[0.1, 0.2], [0.3, 0.4]])
req.normalize_batch_and_arguments()
self.assertTrue(req.is_single)
self.assertEqual(req.batch_size, 1)
# Test batch input_embeds
req = GenerateReqInput(input_embeds=[[[0.1, 0.2]], [[0.3, 0.4]]])
req.normalize_batch_and_arguments()
self.assertFalse(req.is_single)
self.assertEqual(req.batch_size, 2)
def test_input_embeds_with_parallel_sampling(self):
"""Test input_embeds normalization with parallel sampling (n > 1)."""
# Test single input_embeds with parallel sampling
req = GenerateReqInput(
input_embeds=[[0.1, 0.2]], # single embedding vector
sampling_params={"n": 2},
)
req.normalize_batch_and_arguments()
# Should be converted from single to batch and then expanded
self.assertFalse(req.is_single)
self.assertEqual(len(req.input_embeds), 2)
# Both should be the same input_embeds
self.assertEqual(req.input_embeds[0], [[0.1, 0.2]])
self.assertEqual(req.input_embeds[1], [[0.1, 0.2]])
# Test batch input_embeds with parallel sampling
req = GenerateReqInput(
input_embeds=[[[0.1, 0.2]], [[0.3, 0.4]]], sampling_params={"n": 3}
)
req.normalize_batch_and_arguments()
# Should be expanded
self.assertFalse(req.is_single)
self.assertEqual(len(req.input_embeds), 6)
# Check that the expansion is correct
expected_embeds = [[[0.1, 0.2]], [[0.3, 0.4]]] * 3
self.assertEqual(req.input_embeds, expected_embeds)
# Test with different n values per sample (should raise error)
req = GenerateReqInput(
input_embeds=[[[0.1, 0.2]], [[0.3, 0.4]]],
sampling_params=[{"n": 2}, {"n": 3}],
)
with self.assertRaises(ValueError):
req.normalize_batch_and_arguments()
def test_input_embeds_single_to_batch_conversion(self):
"""Test that single input_embeds are properly converted to batch when using parallel sampling."""
# Test the specific case that was fixed: single input_embeds with n > 1
req = GenerateReqInput(
input_embeds=[[0.1, 0.2, 0.3]], sampling_params={"n": 2} # Single embedding
)
req.normalize_batch_and_arguments()
# Should convert single to batch and then expand
self.assertFalse(req.is_single)
self.assertEqual(len(req.input_embeds), 2)
# Both should be the same single embedding
self.assertEqual(req.input_embeds[0], [[0.1, 0.2, 0.3]])
self.assertEqual(req.input_embeds[1], [[0.1, 0.2, 0.3]])
# Test with higher n value
req = GenerateReqInput(input_embeds=[[0.1, 0.2, 0.3]], sampling_params={"n": 5})
req.normalize_batch_and_arguments()
self.assertFalse(req.is_single)
self.assertEqual(len(req.input_embeds), 5)
# All should be the same
for i in range(5):
self.assertEqual(req.input_embeds[i], [[0.1, 0.2, 0.3]])
def test_lora_path_normalization(self):
"""Test normalization of lora_path."""
# Test single lora_path with batch input
req = GenerateReqInput(text=["Hello", "World"], lora_path="path/to/lora")
# Define expected lora_paths before normalization
expected_lora_paths = ["path/to/lora", "path/to/lora"]
req.normalize_batch_and_arguments()
self.assertEqual(req.lora_path, expected_lora_paths)
# Test list of lora_paths
req = GenerateReqInput(text=["Hello", "World"], lora_path=["path1", "path2"])
# Define expected lora_paths before normalization
expected_lora_paths = ["path1", "path2"]
req.normalize_batch_and_arguments()
self.assertEqual(req.lora_path, expected_lora_paths)
# Test with parallel sampling
req = GenerateReqInput(
text=["Hello", "World"],
lora_path=["path1", "path2"],
sampling_params={"n": 2},
)
# Define expected lora_paths before normalization
expected_lora_paths = ["path1", "path2"] * 2
req.normalize_batch_and_arguments()
self.assertEqual(req.lora_path, expected_lora_paths)
def test_logprob_parameters_normalization(self):
"""Test normalization of logprob-related parameters."""
# Test single example
req = GenerateReqInput(
text="Hello",
return_logprob=True,
logprob_start_len=10,
top_logprobs_num=5,
token_ids_logprob=[7, 8, 9],
)
req.normalize_batch_and_arguments()
self.assertEqual(req.return_logprob, True)
self.assertEqual(req.logprob_start_len, 10)
self.assertEqual(req.top_logprobs_num, 5)
self.assertEqual(req.token_ids_logprob, [7, 8, 9])
# Test batch with scalar values
req = GenerateReqInput(
text=["Hello", "World"],
return_logprob=True,
logprob_start_len=10,
top_logprobs_num=5,
token_ids_logprob=[7, 8, 9],
)
req.normalize_batch_and_arguments()
self.assertEqual(req.return_logprob, [True, True])
self.assertEqual(req.logprob_start_len, [10, 10])
self.assertEqual(req.top_logprobs_num, [5, 5])
self.assertEqual(req.token_ids_logprob, [[7, 8, 9], [7, 8, 9]])
# Test batch with list values
req = GenerateReqInput(
text=["Hello", "World"],
return_logprob=[True, False],
logprob_start_len=[10, 5],
top_logprobs_num=[5, 3],
token_ids_logprob=[[7, 8, 9], [4, 5, 6]],
return_hidden_states=[False, False, True],
)
req.normalize_batch_and_arguments()
self.assertEqual(req.return_logprob, [True, False])
self.assertEqual(req.logprob_start_len, [10, 5])
self.assertEqual(req.top_logprobs_num, [5, 3])
self.assertEqual(req.token_ids_logprob, [[7, 8, 9], [4, 5, 6]])
self.assertEqual(req.return_hidden_states, [False, False, True])
def test_custom_logit_processor_normalization(self):
"""Test normalization of custom_logit_processor."""
# Test single processor
req = GenerateReqInput(
text=["Hello", "World"], custom_logit_processor="serialized_processor"
)
req.normalize_batch_and_arguments()
self.assertEqual(
req.custom_logit_processor, ["serialized_processor", "serialized_processor"]
)
# Test list of processors
req = GenerateReqInput(
text=["Hello", "World"], custom_logit_processor=["processor1", "processor2"]
)
req.normalize_batch_and_arguments()
self.assertEqual(req.custom_logit_processor, ["processor1", "processor2"])
def test_session_params_handling(self):
"""Test handling of session_params."""
# Test with dict
req = GenerateReqInput(
text=["Hello", "World"], session_params={"id": "session1", "offset": 10}
)
req.normalize_batch_and_arguments()
self.assertEqual(req.session_params, {"id": "session1", "offset": 10})
# Test with list of dicts
req = GenerateReqInput(
text=["Hello", "World"],
session_params=[{"id": "session1"}, {"id": "session2"}],
)
req.normalize_batch_and_arguments()
self.assertEqual(req.session_params, [{"id": "session1"}, {"id": "session2"}])
def test_getitem_method(self):
"""Test the __getitem__ method."""
req = GenerateReqInput(
text=["Hello", "World"],
image_data=[["img1.jpg"], ["img2.jpg"]],
audio_data=["audio1.mp3", "audio2.mp3"],
sampling_params=[{"temp": 0.7}, {"temp": 0.8}],
rid=["id1", "id2"],
return_logprob=[True, False],
logprob_start_len=[10, 5],
top_logprobs_num=[5, 3],
token_ids_logprob=[[7, 8, 9], [4, 5, 6]],
stream=True,
log_metrics=True,
modalities=["image", "image"],
lora_path=["path1", "path2"],
custom_logit_processor=["processor1", "processor2"],
return_hidden_states=True,
)
req.normalize_batch_and_arguments()
# Get the first item
item0 = req[0]
self.assertEqual(item0.text, "Hello")
self.assertEqual(item0.image_data, ["img1.jpg"])
self.assertEqual(item0.audio_data, "audio1.mp3")
self.assertEqual(item0.sampling_params, {"temp": 0.7})
self.assertEqual(item0.rid, "id1")
self.assertEqual(item0.return_logprob, True)
self.assertEqual(item0.logprob_start_len, 10)
self.assertEqual(item0.top_logprobs_num, 5)
self.assertEqual(item0.token_ids_logprob, [7, 8, 9])
self.assertEqual(item0.stream, True)
self.assertEqual(item0.log_metrics, True)
self.assertEqual(item0.modalities, "image")
self.assertEqual(item0.lora_path, "path1")
self.assertEqual(item0.custom_logit_processor, "processor1")
self.assertEqual(item0.return_hidden_states, True)
def test_regenerate_rid(self):
"""Test the regenerate_rid method."""
req = GenerateReqInput(text="Hello")
req.normalize_batch_and_arguments()
original_rid = req.rid
new_rid = req.regenerate_rid()
self.assertNotEqual(original_rid, new_rid)
self.assertEqual(req.rid, new_rid)
def test_error_cases(self):
"""Test various error cases."""
# Test when neither text, input_ids, nor input_embeds is provided
with self.assertRaises(ValueError):
req = GenerateReqInput()
req.normalize_batch_and_arguments()
# Test when all of text, input_ids, and input_embeds are provided
with self.assertRaises(ValueError):
req = GenerateReqInput(
text="Hello", input_ids=[1, 2, 3], input_embeds=[[0.1, 0.2]]
)
req.normalize_batch_and_arguments()
def test_multiple_input_formats(self):
"""Test different combinations of input formats."""
# Test with text only
req = GenerateReqInput(text="Hello")
req.normalize_batch_and_arguments()
self.assertTrue(req.is_single)
# Test with input_ids only
req = GenerateReqInput(input_ids=[1, 2, 3])
req.normalize_batch_and_arguments()
self.assertTrue(req.is_single)
# Test with input_embeds only
req = GenerateReqInput(input_embeds=[[0.1, 0.2]])
req.normalize_batch_and_arguments()
self.assertTrue(req.is_single)
if __name__ == "__main__":
unittest.main()
-152
View File
@@ -1,152 +0,0 @@
import argparse
import json
import torch
import torch.nn as nn
from sglang.srt.model_executor.hook_manager import register_forward_hooks
from sglang.srt.server_args import ServerArgs
from sglang.test.test_utils import CustomTestCase
HOOK_CALLS = []
def dummy_hook_factory(config):
"""Factory that returns a forward hook capturing a tag from config."""
tag = config.get("tag", "default")
def hook(module, inputs, output):
HOOK_CALLS.append(
{
"module_type": type(module).__name__,
"tag": tag,
"shape": tuple(output.shape),
}
)
return output
return hook
class TinyModel(nn.Module):
def __init__(self):
super().__init__()
self.inner = nn.Sequential(
nn.Linear(4, 2),
nn.ReLU(),
)
self.outer = nn.Sequential(
nn.Linear(4, 4),
nn.ReLU(),
self.inner,
)
def forward(self, x):
return self.outer(x)
class TestAttachHooks(CustomTestCase):
"""Tests for register_forward_hooks / resolve_callable integration."""
def setUp(self):
HOOK_CALLS.clear()
def test_hook_is_attached(self):
"""Hook from a factory string is registered and fired."""
hook_specs = [
{
"target_modules": ["outer.0", "outer.1"],
"hook_factory": "test_model_hooks:dummy_hook_factory",
"config": {"tag": "forward-ok"},
},
{
"target_modules": ["inner.*"],
"hook_factory": "test_model_hooks:dummy_hook_factory",
"config": {"tag": "forward-ok"},
},
]
model = TinyModel()
register_forward_hooks(model, hook_specs)
x = torch.randn(3, 4)
_ = model(x)
self.assertEqual(
len(HOOK_CALLS),
4,
"Forward hook was not called correct number of times",
)
tags = {call["tag"] for call in HOOK_CALLS}
self.assertIn("forward-ok", tags)
def test_no_matching_modules_does_not_crash(self):
"""Hook spec with no matching modules should not crash."""
model = TinyModel()
hook_specs = [
{
"name": "no_match",
"target_modules": ["does_not_exist.*"],
"hook_factory": "test_model_hooks:dummy_hook_factory",
"config": {"tag": "unused"},
}
]
register_forward_hooks(model, hook_specs)
x = torch.randn(3, 4)
_ = model(x)
# No hooks should have fired
self.assertEqual(len(HOOK_CALLS), 0)
def test_cli_hooks_reach_model(self):
"""
Ensure that when hooks are provided via CLI, they are parsed into
ServerArgs, passed to register_forward_hooks, and actually
run during a forward pass.
"""
parser = argparse.ArgumentParser()
ServerArgs.add_cli_args(parser)
hooks_spec = [
{
"name": "outer_and_inner_from_cli",
"target_modules": ["outer.0", "outer.1", "inner.*"],
"hook_factory": "test_model_hooks:dummy_hook_factory",
"config": {"tag": "cli-hook"},
}
]
cli_args = [
"--model-path",
"Qwen/Qwen2-7B-Instruct", # Dummy value; not used in this test
"--forward-hooks",
json.dumps(hooks_spec),
]
args = parser.parse_args(cli_args)
server_args = ServerArgs.from_cli_args(args)
self.assertEqual(server_args.forward_hooks, hooks_spec)
model = TinyModel()
register_forward_hooks(model, server_args.forward_hooks)
x = torch.randn(3, 4)
_ = model(x)
# We expect hooks on outer.0, outer.1, inner.0, inner.1 => 4 calls
self.assertEqual(
len(HOOK_CALLS),
4,
"CLI-configured hooks did not fire expected number of times",
)
tags = {call["tag"] for call in HOOK_CALLS}
self.assertEqual(tags, {"cli-hook"})
if __name__ == "__main__":
pass
# unittest.main()
-47
View File
@@ -1,47 +0,0 @@
import os
import unittest
from types import SimpleNamespace
from sglang.srt.utils import kill_process_tree
from sglang.test.run_eval import run_eval
from sglang.test.test_utils import (
DEFAULT_MODEL_NAME_FOR_TEST,
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
DEFAULT_URL_FOR_TEST,
CustomTestCase,
popen_launch_server,
)
class TestPageSize(CustomTestCase):
@classmethod
def setUpClass(cls):
os.environ["SGLANG_DEBUG_MEMORY_POOL"] = "1"
cls.model = DEFAULT_MODEL_NAME_FOR_TEST
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=["--page-size", 4, "--chunked-prefill-size", 128],
)
@classmethod
def tearDownClass(cls):
kill_process_tree(cls.process.pid)
def test_mmlu(self):
args = SimpleNamespace(
base_url=self.base_url,
model=self.model,
eval_name="mmlu",
num_examples=64,
num_threads=32,
)
metrics = run_eval(args)
self.assertGreaterEqual(metrics["score"], 0.65)
if __name__ == "__main__":
unittest.main()
-88
View File
@@ -1,88 +0,0 @@
import asyncio
import os
import re
import unittest
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,
STDERR_FILENAME,
STDOUT_FILENAME,
CustomTestCase,
popen_launch_server,
send_concurrent_generate_requests,
send_generate_requests,
)
class TestMaxQueuedRequests(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 1
"1",
"--attention-backend",
"triton",
),
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_max_queued_requests_validation_with_serial_requests(self):
"""Verify request is not throttled when the max concurrency is 1."""
status_codes = send_generate_requests(
self.base_url,
num_requests=10,
)
for status_code in status_codes:
assert status_code == 200 # request shouldn't be throttled
def test_max_queued_requests_validation_with_concurrent_requests(self):
"""Verify request throttling with concurrent requests."""
status_codes = asyncio.run(
send_concurrent_generate_requests(self.base_url, num_requests=10)
)
self.assertLessEqual(status_codes.count(200), 2)
# expected_status_codes = [200, 200, 503, 503, 503, 503, 503, 503, 503, 503]
# self.assertEqual(status_codes, expected_status_codes)
def test_max_running_requests_and_max_queued_request_validation(self):
"""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)) <= 1
if qr_match:
assert int(qr_match.group(1)) <= 1
if __name__ == "__main__":
unittest.main()
-590
View File
@@ -1,590 +0,0 @@
import unittest
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from sglang.srt.entrypoints.engine import Engine
from sglang.test.test_utils import DEFAULT_SMALL_MODEL_NAME_FOR_TEST, CustomTestCase
TEST_MODEL_NAME = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
class TestScoreAPI(CustomTestCase):
"""Test the scoring API functionality."""
def setUp(self):
"""Set up each test case."""
self.engine = Engine(model_path=TEST_MODEL_NAME)
def tearDown(self):
"""Clean up after each test case."""
if self.engine is not None:
self.engine.shutdown()
torch.cuda.empty_cache()
def compute_hf_scores(
self, query, items, label_token_ids, apply_softmax=False, item_first=False
):
"""Compute scores using direct HuggingFace model inference.
Returns probabilities for each token ID, optionally normalized with softmax.
Args:
query: The query text
items: List of item texts
label_token_ids: List of token IDs to compute probabilities for
apply_softmax: Whether to normalize probabilities using softmax
item_first: If True, prepend items to query. Otherwise append items to query.
"""
# Initialize HF model and tokenizer
tokenizer = AutoTokenizer.from_pretrained(
TEST_MODEL_NAME, trust_remote_code=True
)
model = AutoModelForCausalLM.from_pretrained(
TEST_MODEL_NAME, trust_remote_code=True
)
try:
scores = []
for item in items:
# Construct full text based on item_first parameter
full_text = f"{item}{query}" if item_first else f"{query}{item}"
inputs = tokenizer(full_text, return_tensors="pt").to(model.device)
# Get logits for the last token
with torch.no_grad():
outputs = model(**inputs)
last_token_logits = outputs.logits[0, -1]
# Get logits for just our target tokens
target_logits = last_token_logits[label_token_ids]
# Apply softmax over just the target tokens
target_probs = torch.softmax(target_logits, dim=-1)
# Convert to list of probabilities in order of label_token_ids
probs = [target_probs[i].item() for i in range(len(label_token_ids))]
scores.append(probs)
return scores
finally:
# Clean up HF resources
model.cpu()
del model
del tokenizer
torch.cuda.empty_cache()
def _get_token_ids(self, tokens):
"""Helper method to get token IDs for a list of tokens."""
tokenizer = AutoTokenizer.from_pretrained(
TEST_MODEL_NAME, trust_remote_code=True
)
try:
label_token_ids = []
for token in tokens:
encoding = tokenizer.encode_plus(token, add_special_tokens=False)
token_ids = encoding["input_ids"]
label_token_ids.append(token_ids[0])
return label_token_ids
finally:
del tokenizer
def _compare_scores(self, hf_scores, sglang_scores, label_token_ids, case_name=""):
"""Helper method to compare scores between HF and SGLang using relative tolerance."""
self.assertEqual(
len(hf_scores),
len(sglang_scores),
f"Score lengths don't match for {case_name}",
)
# Use a relative tolerance of 1%
TOLERANCE = 0.01
for hf_score_list, sglang_score_list in zip(hf_scores, sglang_scores):
self.assertEqual(
len(hf_score_list),
len(sglang_score_list),
f"Score list lengths don't match for {case_name}",
)
for hf_score, sglang_score in zip(hf_score_list, sglang_score_list):
diff = abs(hf_score - sglang_score)
self.assertLessEqual(
diff,
TOLERANCE,
msg=f"Scores differ by {diff:.2%} ({case_name}): "
f"HF={hf_score:.6f}, SGLang={sglang_score:.6f}",
)
self.assertGreaterEqual(
sglang_score, 0, f"SGLang score {sglang_score:.6f} not in [0,1]"
)
self.assertLessEqual(
sglang_score, 1, f"SGLang score {sglang_score:.6f} not in [0,1]"
)
self.assertAlmostEqual(
sum(sglang_score_list),
1.0,
places=6,
msg=f"SGLang scores don't sum to 1 ({case_name}): {sum(sglang_score_list):.6f}",
)
def test_score_consistency(self):
"""Test that SGLang scoring matches direct HuggingFace model scoring."""
# Define test cases
test_cases = [
{
"name": "default case",
"query": "I pledge allegiance",
"items": ["", " to"],
"item_first": False,
},
{
"name": "item_first case",
"query": " is a city",
"items": ["Tokyo", "Japan"],
"item_first": True,
},
]
# Common tokens to test for all cases
tokens = [" to", " the"]
label_token_ids = self._get_token_ids(tokens)
# Run each test case
for case in test_cases:
# Get scores from SGLang
sglang_scores = self.engine.score(
query=case["query"],
items=case["items"],
label_token_ids=label_token_ids,
apply_softmax=True,
item_first=case["item_first"],
)
# Get scores from HuggingFace using the same parameters
hf_scores = self.compute_hf_scores(
query=case["query"],
items=case["items"],
label_token_ids=label_token_ids,
apply_softmax=True,
item_first=case["item_first"],
)
# Compare scores
self._compare_scores(
hf_scores, sglang_scores, label_token_ids, case["name"]
)
def test_score_batch_handling(self):
"""Test that batch scoring works correctly."""
# Test with different batch sizes
batch_sizes = [1, 2, 4, 8]
label_token_ids = [1, 2, 3]
for batch_size in batch_sizes:
texts = [f"test {i}" for i in range(batch_size)]
scores = self.engine.score(
query="The test was",
items=texts,
label_token_ids=label_token_ids,
apply_softmax=True,
)
self.assertEqual(
len(scores),
batch_size,
f"Expected {batch_size} scores, got {len(scores)}",
)
# Verify each score list has the correct length
for score_list in scores:
self.assertEqual(
len(score_list),
len(label_token_ids),
f"Score list length {len(score_list)} doesn't match label_token_ids length {len(label_token_ids)}",
)
self.assertTrue(
all(isinstance(v, float) for v in score_list),
"All scores should be floats",
)
self.assertAlmostEqual(
1.0, sum(score_list), 6, "Scores should sum to 1"
)
def test_score_request_construction(self):
"""Test that scoring requests are constructed to avoid decode phase."""
from unittest.mock import patch
# Capture the internal request to verify optimization
captured_requests = []
original_gen = self.engine.tokenizer_manager.generate_request
async def mock_generate_request(req, request=None):
captured_requests.append(req)
async for result in original_gen(req, request):
yield result
# Patch the generate_request method
with patch.object(
self.engine.tokenizer_manager,
"generate_request",
side_effect=mock_generate_request,
):
# Run a scoring request
query = "What is the capital of"
items = ["France", "Germany"]
label_token_ids = [1, 2, 3]
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
# Verify we got results
self.assertEqual(len(scores), len(items))
# Verify the captured request has decode-avoiding properties
self.assertEqual(len(captured_requests), 1)
request = captured_requests[0]
# Key assertions for decode phase avoidance:
# 1. max_new_tokens should be 0 (prevents token generation)
# Handle both single and batch request cases
if isinstance(request.sampling_params, dict):
max_new_tokens = request.sampling_params.get("max_new_tokens", 0)
elif isinstance(request.sampling_params, list):
# For batch requests, check the first item
max_new_tokens = request.sampling_params[0].get("max_new_tokens", 0)
else:
max_new_tokens = getattr(request.sampling_params, "max_new_tokens", 0)
self.assertEqual(
max_new_tokens, 0, "max_new_tokens should be 0 to avoid decode phase"
)
# 2. Should have token_ids_logprob for scoring
# Handle both single and batch request cases
if (
isinstance(request.token_ids_logprob, list)
and len(request.token_ids_logprob) > 0
and isinstance(request.token_ids_logprob[0], list)
):
# Batch case: token_ids_logprob is a list of lists
# Each item in the batch should have the same label_token_ids
for item_token_ids in request.token_ids_logprob:
self.assertEqual(
item_token_ids,
label_token_ids,
"Each batch item should have label_token_ids for scoring",
)
else:
# Single request case
self.assertEqual(
request.token_ids_logprob,
label_token_ids,
"Should have label_token_ids for scoring",
)
# 3. Should request logprobs but not stream
self.assertTrue(
request.return_logprob, "Should request logprobs for scoring"
)
self.assertFalse(request.stream, "Scoring requests should not stream")
def test_multi_item_scoring_basic(self):
"""Test basic multi-item scoring functionality."""
# Test with a simple query and items
query = "What is the capital of California? Answer Yes or No for each of the following options:"
items = ["Sacramento", "San Jose", "San Francisco"]
label_token_ids = [9454, 2753] # "Yes" and "No" tokens
# Get scores using SGLang
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
# Verify we get the expected number of scores
self.assertEqual(len(scores), len(items), "Should get one score list per item")
# Verify each score list has the correct length
for i, score_list in enumerate(scores):
self.assertEqual(
len(score_list),
len(label_token_ids),
f"Item {i} should have {len(label_token_ids)} scores",
)
# Verify scores are probabilities (sum to 1)
self.assertAlmostEqual(
sum(score_list),
1.0,
places=6,
msg=f"Scores for item {i} should sum to 1",
)
# Verify all scores are non-negative
for j, score in enumerate(score_list):
self.assertGreaterEqual(
score, 0, f"Score {j} for item {i} should be non-negative"
)
def test_multi_item_scoring_consistency(self):
"""Test that multi-item scoring gives consistent results."""
query = "Choose the best option:"
items = ["Option A", "Option B", "Option C"]
label_token_ids = [1, 2, 3]
# Run the same test multiple times
scores1 = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
scores2 = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
# Results should be identical (deterministic)
self.assertEqual(len(scores1), len(scores2), "Should get same number of items")
for i, (s1, s2) in enumerate(zip(scores1, scores2)):
self.assertEqual(
len(s1), len(s2), f"Item {i} should have same number of scores"
)
for j, (score1, score2) in enumerate(zip(s1, s2)):
self.assertAlmostEqual(
score1,
score2,
places=6,
msg=f"Score {j} for item {i} should be identical",
)
def test_multi_item_scoring_different_sizes(self):
"""Test multi-item scoring with different numbers of items."""
query = "Rate each option:"
label_token_ids = [1, 2, 3, 4, 5]
# Test with different numbers of items
test_cases = [
["Single item"],
["Item 1", "Item 2"],
["A", "B", "C", "D"],
["X", "Y", "Z", "W", "V", "U"],
]
for items in test_cases:
with self.subTest(items=items):
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
self.assertEqual(
len(scores), len(items), f"Should get {len(items)} score lists"
)
for i, score_list in enumerate(scores):
self.assertEqual(
len(score_list),
len(label_token_ids),
f"Item {i} should have {len(label_token_ids)} scores",
)
self.assertAlmostEqual(sum(score_list), 1.0, places=6)
def test_multi_item_scoring_empty_items(self):
"""Test multi-item scoring with empty items list."""
query = "Test query"
items = []
label_token_ids = [1, 2]
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
self.assertEqual(len(scores), 0, "Should return empty list for empty items")
def test_multi_item_scoring_single_item(self):
"""Test multi-item scoring with single item (should work like regular scoring)."""
query = "Complete this sentence: The capital of France is"
items = ["Paris"]
label_token_ids = [1, 2, 3]
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
self.assertEqual(len(scores), 1, "Should get one score list")
self.assertEqual(
len(scores[0]), len(label_token_ids), "Should have correct number of scores"
)
self.assertAlmostEqual(sum(scores[0]), 1.0, places=6)
def test_multi_item_scoring_different_queries(self):
"""Test multi-item scoring with different types of queries."""
items = ["Yes", "No"]
label_token_ids = [1, 2]
test_queries = [
"Is this true?",
"Choose the correct answer:",
"What is the best option?",
"Select all that apply:",
"", # Empty query
]
for query in test_queries:
with self.subTest(query=query):
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
self.assertEqual(
len(scores),
len(items),
f"Should get {len(items)} score lists for query: '{query}'",
)
for i, score_list in enumerate(scores):
self.assertEqual(len(score_list), len(label_token_ids))
self.assertAlmostEqual(sum(score_list), 1.0, places=6)
def test_multi_item_scoring_different_label_tokens(self):
"""Test multi-item scoring with different label token sets."""
query = "Choose the best option:"
items = ["Option A", "Option B"]
test_label_tokens = [
[1, 2], # Two tokens
[1, 2, 3, 4], # Four tokens
[1], # Single token
[1, 2, 3, 4, 5, 6, 7, 8], # Many tokens
]
for label_token_ids in test_label_tokens:
with self.subTest(label_tokens=label_token_ids):
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
self.assertEqual(len(scores), len(items))
for i, score_list in enumerate(scores):
self.assertEqual(
len(score_list),
len(label_token_ids),
f"Item {i} should have {len(label_token_ids)} scores",
)
self.assertAlmostEqual(sum(score_list), 1.0, places=6)
def test_multi_item_scoring_without_softmax(self):
"""Test multi-item scoring without softmax normalization."""
query = "Rate each option:"
items = ["Good", "Bad", "Neutral"]
label_token_ids = [1, 2, 3]
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=False, # No softmax
)
self.assertEqual(len(scores), len(items))
for i, score_list in enumerate(scores):
self.assertEqual(len(score_list), len(label_token_ids))
# Without softmax, scores don't need to sum to 1
# But they should still be valid logits/probabilities
for j, score in enumerate(score_list):
self.assertIsInstance(
score, (int, float), f"Score {j} for item {i} should be numeric"
)
def test_multi_item_scoring_large_batch(self):
"""Test multi-item scoring with a large number of items."""
query = "Classify each item:"
items = [f"Item {i}" for i in range(20)] # 20 items
label_token_ids = [1, 2, 3]
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
self.assertEqual(len(scores), len(items), "Should handle large batches")
for i, score_list in enumerate(scores):
self.assertEqual(len(score_list), len(label_token_ids))
self.assertAlmostEqual(sum(score_list), 1.0, places=6)
def test_multi_item_scoring_unicode(self):
"""Test multi-item scoring with unicode characters."""
query = "选择最佳选项:"
items = ["选项A", "选项B", "选项C"]
label_token_ids = [1, 2, 3]
scores = self.engine.score(
query=query,
items=items,
label_token_ids=label_token_ids,
apply_softmax=True,
)
self.assertEqual(len(scores), len(items))
for i, score_list in enumerate(scores):
self.assertEqual(len(score_list), len(label_token_ids))
self.assertAlmostEqual(sum(score_list), 1.0, places=6)
def test_multi_item_scoring_error_handling(self):
"""Test multi-item scoring error handling."""
query = "Test query"
items = ["Item 1", "Item 2"]
label_token_ids = [1, 2]
# Test with invalid label_token_ids
with self.assertRaises((ValueError, TypeError)):
self.engine.score(
query=query,
items=items,
label_token_ids="invalid", # Should be list of ints
apply_softmax=True,
)
# Test with None items
with self.assertRaises((ValueError, TypeError)):
self.engine.score(
query=query,
items=None,
label_token_ids=label_token_ids,
apply_softmax=True,
)
if __name__ == "__main__":
unittest.main()
-288
View File
@@ -1,288 +0,0 @@
import json
import unittest
from unittest.mock import patch
from sglang.srt.server_args import PortArgs, ServerArgs, prepare_server_args
from sglang.test.test_utils import CustomTestCase
class TestPrepareServerArgs(CustomTestCase):
def test_prepare_server_args(self):
server_args = prepare_server_args(
[
"--model-path",
"meta-llama/Meta-Llama-3.1-8B-Instruct",
"--json-model-override-args",
'{"rope_scaling": {"factor": 2.0, "rope_type": "linear"}}',
]
)
self.assertEqual(
server_args.model_path, "meta-llama/Meta-Llama-3.1-8B-Instruct"
)
self.assertEqual(
json.loads(server_args.json_model_override_args),
{"rope_scaling": {"factor": 2.0, "rope_type": "linear"}},
)
class TestLoadBalanceMethod(unittest.TestCase):
def test_non_pd_defaults_to_round_robin(self):
server_args = ServerArgs(model_path="dummy", disaggregation_mode="null")
self.assertEqual(server_args.load_balance_method, "round_robin")
def test_pd_prefill_defaults_to_follow_bootstrap_room(self):
server_args = ServerArgs(model_path="dummy", disaggregation_mode="prefill")
self.assertEqual(server_args.load_balance_method, "follow_bootstrap_room")
def test_pd_decode_defaults_to_round_robin(self):
server_args = ServerArgs(model_path="dummy", disaggregation_mode="decode")
self.assertEqual(server_args.load_balance_method, "round_robin")
class TestPortArgs(unittest.TestCase):
@patch("sglang.srt.server_args.is_port_available")
@patch("sglang.srt.server_args.tempfile.NamedTemporaryFile")
def test_init_new_standard_case(self, mock_temp_file, mock_is_port_available):
mock_is_port_available.return_value = True
mock_temp_file.return_value.name = "temp_file"
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = False
port_args = PortArgs.init_new(server_args)
self.assertTrue(port_args.tokenizer_ipc_name.startswith("ipc://"))
self.assertTrue(port_args.scheduler_input_ipc_name.startswith("ipc://"))
self.assertTrue(port_args.detokenizer_ipc_name.startswith("ipc://"))
self.assertIsInstance(port_args.nccl_port, int)
@patch("sglang.srt.server_args.is_port_available")
def test_init_new_with_single_node_dp_attention(self, mock_is_port_available):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 1
server_args.dist_init_addr = None
port_args = PortArgs.init_new(server_args)
self.assertTrue(port_args.tokenizer_ipc_name.startswith("tcp://127.0.0.1:"))
self.assertTrue(
port_args.scheduler_input_ipc_name.startswith("tcp://127.0.0.1:")
)
self.assertTrue(port_args.detokenizer_ipc_name.startswith("tcp://127.0.0.1:"))
self.assertIsInstance(port_args.nccl_port, int)
@patch("sglang.srt.server_args.is_port_available")
def test_init_new_with_dp_rank(self, mock_is_port_available):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 1
server_args.dist_init_addr = "192.168.1.1:25000"
worker_ports = [25006, 25007, 25008, 25009]
port_args = PortArgs.init_new(server_args, dp_rank=2, worker_ports=worker_ports)
self.assertTrue(port_args.scheduler_input_ipc_name.endswith(":25008"))
self.assertTrue(port_args.tokenizer_ipc_name.startswith("tcp://192.168.1.1:"))
self.assertTrue(port_args.detokenizer_ipc_name.startswith("tcp://192.168.1.1:"))
self.assertIsInstance(port_args.nccl_port, int)
@patch("sglang.srt.server_args.is_port_available")
def test_init_new_with_ipv4_address(self, mock_is_port_available):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 2
server_args.dist_init_addr = "192.168.1.1:25000"
port_args = PortArgs.init_new(server_args)
self.assertTrue(port_args.tokenizer_ipc_name.startswith("tcp://192.168.1.1:"))
self.assertTrue(
port_args.scheduler_input_ipc_name.startswith("tcp://192.168.1.1:")
)
self.assertTrue(port_args.detokenizer_ipc_name.startswith("tcp://192.168.1.1:"))
self.assertIsInstance(port_args.nccl_port, int)
@patch("sglang.srt.server_args.is_port_available")
def test_init_new_with_malformed_ipv4_address(self, mock_is_port_available):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 2
server_args.dist_init_addr = "192.168.1.1"
with self.assertRaises(AssertionError) as context:
PortArgs.init_new(server_args)
self.assertIn(
"please provide --dist-init-addr as host:port", str(context.exception)
)
@patch("sglang.srt.server_args.is_port_available")
def test_init_new_with_malformed_ipv4_address_invalid_port(
self, mock_is_port_available
):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 2
server_args.dist_init_addr = "192.168.1.1:abc"
with self.assertRaises(ValueError) as context:
PortArgs.init_new(server_args)
@patch("sglang.srt.server_args.is_port_available")
@patch("sglang.srt.server_args.is_valid_ipv6_address", return_value=True)
def test_init_new_with_ipv6_address(
self, mock_is_valid_ipv6, mock_is_port_available
):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 2
server_args.dist_init_addr = "[2001:db8::1]:25000"
port_args = PortArgs.init_new(server_args)
self.assertTrue(port_args.tokenizer_ipc_name.startswith("tcp://[2001:db8::1]:"))
self.assertTrue(
port_args.scheduler_input_ipc_name.startswith("tcp://[2001:db8::1]:")
)
self.assertTrue(
port_args.detokenizer_ipc_name.startswith("tcp://[2001:db8::1]:")
)
self.assertIsInstance(port_args.nccl_port, int)
@patch("sglang.srt.server_args.is_port_available")
@patch("sglang.srt.server_args.is_valid_ipv6_address", return_value=False)
def test_init_new_with_invalid_ipv6_address(
self, mock_is_valid_ipv6, mock_is_port_available
):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 2
server_args.dist_init_addr = "[invalid-ipv6]:25000"
with self.assertRaises(ValueError) as context:
PortArgs.init_new(server_args)
self.assertIn("invalid IPv6 address", str(context.exception))
@patch("sglang.srt.server_args.is_port_available")
def test_init_new_with_malformed_ipv6_address_missing_bracket(
self, mock_is_port_available
):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 2
server_args.dist_init_addr = "[2001:db8::1:25000"
with self.assertRaises(ValueError) as context:
PortArgs.init_new(server_args)
self.assertIn("invalid IPv6 address format", str(context.exception))
@patch("sglang.srt.server_args.is_port_available")
@patch("sglang.srt.server_args.is_valid_ipv6_address", return_value=True)
def test_init_new_with_malformed_ipv6_address_missing_port(
self, mock_is_valid_ipv6, mock_is_port_available
):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 2
server_args.dist_init_addr = "[2001:db8::1]"
with self.assertRaises(ValueError) as context:
PortArgs.init_new(server_args)
self.assertIn(
"a port must be specified in IPv6 address", str(context.exception)
)
@patch("sglang.srt.server_args.is_port_available")
@patch("sglang.srt.server_args.is_valid_ipv6_address", return_value=True)
def test_init_new_with_malformed_ipv6_address_invalid_port(
self, mock_is_valid_ipv6, mock_is_port_available
):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 2
server_args.dist_init_addr = "[2001:db8::1]:abcde"
with self.assertRaises(ValueError) as context:
PortArgs.init_new(server_args)
self.assertIn("invalid port in IPv6 address", str(context.exception))
@patch("sglang.srt.server_args.is_port_available")
@patch("sglang.srt.server_args.is_valid_ipv6_address", return_value=True)
def test_init_new_with_malformed_ipv6_address_wrong_separator(
self, mock_is_valid_ipv6, mock_is_port_available
):
mock_is_port_available.return_value = True
server_args = ServerArgs(model_path="dummy")
server_args.port = 30000
server_args.nccl_port = None
server_args.enable_dp_attention = True
server_args.nnodes = 2
server_args.dist_init_addr = "[2001:db8::1]#25000"
with self.assertRaises(ValueError) as context:
PortArgs.init_new(server_args)
self.assertIn("expected ':' after ']'", str(context.exception))
if __name__ == "__main__":
unittest.main()
-743
View File
@@ -1,743 +0,0 @@
"""
python3 -m unittest test_srt_endpoint.TestSRTEndpoint.test_simple_decode
python3 -m unittest test_srt_endpoint.TestSRTEndpoint.test_logprob_with_chunked_prefill
python3 -m unittest test_srt_endpoint.TestTokenizeDetokenize
"""
import json
import random
import time
import unittest
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from typing import Optional
import numpy as np
import requests
from sglang.srt.sampling.custom_logit_processor import CustomLogitProcessor
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,
run_logprob_check,
)
class TestSRTEndpoint(CustomTestCase):
@classmethod
def setUpClass(cls):
cls.model = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
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=(
"--enable-custom-logit-processor",
"--mem-fraction-static",
"0.7",
"--cuda-graph-max-bs",
"8",
),
)
@classmethod
def tearDownClass(cls):
kill_process_tree(cls.process.pid)
def run_decode(
self,
return_logprob=False,
top_logprobs_num=0,
return_text=False,
n=1,
stream=False,
batch=False,
):
if batch:
text = ["The capital of France is"]
else:
text = "The capital of France is"
response = requests.post(
self.base_url + "/generate",
json={
"text": text,
"sampling_params": {
"temperature": 0 if n == 1 else 0.5,
"max_new_tokens": 16,
"n": n,
},
"stream": stream,
"return_logprob": return_logprob,
"top_logprobs_num": top_logprobs_num,
"return_text_in_logprobs": return_text,
"logprob_start_len": 0,
},
)
if not stream:
response_json = response.json()
else:
response_json = []
for line in response.iter_lines():
if line.startswith(b"data: ") and line[6:] != b"[DONE]":
response_json.append(json.loads(line[6:]))
print(json.dumps(response_json, indent=2))
print("=" * 100)
def test_simple_decode(self):
self.run_decode()
def test_simple_decode_batch(self):
self.run_decode(batch=True)
def test_parallel_sample(self):
self.run_decode(n=3)
def test_parallel_sample_stream(self):
self.run_decode(n=3, stream=True)
def test_logprob(self):
self.run_decode(
return_logprob=True,
top_logprobs_num=5,
return_text=True,
)
def test_logprob_start_len(self):
logprob_start_len = 4
new_tokens = 4
prompts = [
"I have a very good idea on",
"Today is a sunndy day and",
]
response = requests.post(
self.base_url + "/generate",
json={
"text": prompts,
"sampling_params": {
"temperature": 0,
"max_new_tokens": new_tokens,
},
"return_logprob": True,
"top_logprobs_num": 5,
"return_text_in_logprobs": True,
"logprob_start_len": logprob_start_len,
},
)
response_json = response.json()
print(json.dumps(response_json, indent=2))
for i, res in enumerate(response_json):
self.assertEqual(
res["meta_info"]["prompt_tokens"],
logprob_start_len + len(res["meta_info"]["input_token_logprobs"]),
)
assert prompts[i].endswith(
"".join([x[-1] for x in res["meta_info"]["input_token_logprobs"]])
)
self.assertEqual(res["meta_info"]["completion_tokens"], new_tokens)
self.assertEqual(len(res["meta_info"]["output_token_logprobs"]), new_tokens)
self.assertEqual(
res["text"],
"".join([x[-1] for x in res["meta_info"]["output_token_logprobs"]]),
)
def test_logprob_with_chunked_prefill(self):
"""Test a long prompt that requests output logprobs will not hit OOM."""
new_tokens = 4
prompts = "I have a very good idea on this. " * 8000
response = requests.post(
self.base_url + "/generate",
json={
"text": prompts,
"sampling_params": {
"temperature": 0,
"max_new_tokens": new_tokens,
},
"return_logprob": True,
"logprob_start_len": -1,
"top_logprobs_num": 5,
},
)
response_json = response.json()
# print(json.dumps(response_json, indent=2))
res = response_json
self.assertEqual(res["meta_info"]["completion_tokens"], new_tokens)
# Test the number of tokens are correct
self.assertEqual(len(res["meta_info"]["output_token_logprobs"]), new_tokens)
self.assertEqual(len(res["meta_info"]["output_top_logprobs"]), new_tokens)
# Test the top-1 tokens are the same as output tokens (because temp = 0.0)
for i in range(new_tokens):
self.assertListEqual(
res["meta_info"]["output_token_logprobs"][i],
res["meta_info"]["output_top_logprobs"][i][0],
)
self.assertEqual(len(res["meta_info"]["output_top_logprobs"][i]), 5)
def test_logprob_match(self):
"""Test the output logprobs are close to the input logprobs if we run a prefill again."""
def run_generate(
prompt, return_logprob=False, max_new_tokens=512, logprob_start_len=-1
):
if isinstance(prompt, str):
prompt_kwargs = {"text": prompt}
else:
prompt_kwargs = {"input_ids": prompt}
response = requests.post(
self.base_url + "/generate",
json={
**prompt_kwargs,
"sampling_params": {
"temperature": 1.0,
"max_new_tokens": max_new_tokens,
"ignore_eos": True,
},
"return_logprob": return_logprob,
"return_text_in_logprobs": True,
"logprob_start_len": logprob_start_len,
},
)
return response.json()
prompt = "I have a very good idea on how to"
gen = run_generate(prompt, return_logprob=True, logprob_start_len=0)
output_logprobs = np.array(
[x[0] for x in gen["meta_info"]["output_token_logprobs"]]
)
num_prompts_tokens = gen["meta_info"]["prompt_tokens"]
input_tokens = [x[1] for x in gen["meta_info"]["input_token_logprobs"]]
output_tokens = [x[1] for x in gen["meta_info"]["output_token_logprobs"]]
new_prompt = input_tokens + output_tokens
score = run_generate(
new_prompt, return_logprob=True, logprob_start_len=0, max_new_tokens=0
)
output_logprobs_score = np.array(
[
x[0]
for x in score["meta_info"]["input_token_logprobs"][num_prompts_tokens:]
]
)
print(f"{output_logprobs[-10:]=}")
print(f"{output_logprobs_score[-10:]=}")
diff = np.abs(output_logprobs - output_logprobs_score)
max_diff = np.max(diff)
self.assertLess(max_diff, 0.35)
def test_logprob_mixed(self):
args = []
temperature = 0
# input_len, output_len, temperature, logprob_start_len, return_logprob, top_logprobs_num
for input_len in [1000, 5000, 10000, 50000]:
for output_len in [4, 8]:
for logprob_start_len in [0, 500, 2500, 5000, 25000]:
for return_logprob in [True, False]:
for top_logprobs_num in [0, 5]:
if logprob_start_len >= input_len:
continue
args.append(
(
input_len,
output_len,
temperature,
logprob_start_len,
return_logprob,
top_logprobs_num,
)
)
random.shuffle(args)
func = partial(run_logprob_check, self)
with ThreadPoolExecutor(8) as executor:
list(executor.map(func, args))
def test_logprob_grammar(self):
prompts = "Question: Is Paris the Capital of France? Answer:"
allowed_tokens = [" Yes", " No"]
response = requests.post(
self.base_url + "/generate",
json={
"text": prompts,
"sampling_params": {
"temperature": 1.0,
"max_new_tokens": 1,
"regex": "( Yes| No)",
},
"return_logprob": True,
"top_logprobs_num": 5, # The grammar constraint allows all prefix tokens so we need to use a larger top_k.
"return_text_in_logprobs": True,
},
)
response_json = response.json()
output_top_logprobs = response_json["meta_info"]["output_top_logprobs"][0]
print(f"{output_top_logprobs=}")
# Parse results
# This is because the grammar constraint allows all prefix tokens
logprobs = [None] * 2
for i in range(len(output_top_logprobs)):
try:
idx = allowed_tokens.index(output_top_logprobs[i][2])
except ValueError:
# Not found
continue
logprobs[idx] = output_top_logprobs[i][0]
self.assertTrue(all(x is not None for x in logprobs))
def run_custom_logit_processor(self, target_token_id: Optional[int] = None):
"""Test custom logit processor with custom params.
If target_token_id is None, the custom logit processor won't be passed in.
"""
custom_params = {"token_id": target_token_id}
class DeterministicLogitProcessor(CustomLogitProcessor):
"""A dummy logit processor that changes the logits to always
sample the given token id.
"""
def __call__(self, logits, custom_param_list):
assert logits.shape[0] == len(custom_param_list)
key = "token_id"
for i, param_dict in enumerate(custom_param_list):
# Mask all other tokens
logits[i, :] = -float("inf")
# Assign highest probability to the specified token
logits[i, param_dict[key]] = 0.0
return logits
prompts = "Question: Is Paris the Capital of France? Answer:"
# Base case json data to be posted to the server.
base_json = {
"text": prompts,
"sampling_params": {"temperature": 0.0},
"return_logprob": True,
}
# Custom json data with custom logit processor and params.
custom_json = base_json.copy()
# Only set the custom logit processor if target_token_id is not None.
if target_token_id is not None:
custom_json["custom_logit_processor"] = DeterministicLogitProcessor.to_str()
custom_json["sampling_params"]["custom_params"] = custom_params
custom_response = requests.post(
self.base_url + "/generate",
json=custom_json,
).json()
output_token_logprobs = custom_response["meta_info"]["output_token_logprobs"]
sampled_tokens = [x[1] for x in output_token_logprobs]
# The logit processor should always sample the given token as the logits is deterministic.
if target_token_id is not None:
self.assertTrue(
all(x == custom_params["token_id"] for x in sampled_tokens),
# Print the detailed test case info if the test fails.
f"{target_token_id=}\n{sampled_tokens=}\n{custom_response=}",
)
def run_stateful_custom_logit_processor(
self, first_token_id: int | None, delay: int = 2
):
"""Test custom logit processor with custom params and state.
Should sample the first `delay` tokens normally, then output first_token_id and consecutive tokens after that.
If first_token_id is None, the custom logit processor won't be passed in.
"""
custom_params = {"token_id": first_token_id, "delay": 2}
class DeterministicStatefulLogitProcessor(CustomLogitProcessor):
"""A dummy logit processor that changes the logits to always
sample the given token id.
"""
def __call__(self, logits, custom_param_list):
assert logits.shape[0] == len(custom_param_list)
for i, param_dict in enumerate(custom_param_list):
if param_dict["delay"] > 0:
param_dict["delay"] -= 1
continue
if param_dict["delay"] == 0:
param_dict["delay"] -= 1
force_token = param_dict["token_id"]
else:
output_ids = param_dict["__req__"].output_ids
force_token = output_ids[-1] + 1
# Mask all other tokens
logits[i, :] = -float("inf")
# Assign highest probability to the specified token
logits[i, force_token] = 0.0
return logits
prompts = "Question: Is Paris the Capital of France? Answer:"
# Base case json data to be posted to the server.
base_json = {
"text": prompts,
"sampling_params": {"temperature": 0.0},
"return_logprob": True,
}
# Custom json data with custom logit processor and params.
custom_json = base_json.copy()
# Only set the custom logit processor if target_token_id is not None.
if first_token_id is not None:
custom_json["custom_logit_processor"] = (
DeterministicStatefulLogitProcessor().to_str()
)
custom_json["sampling_params"]["custom_params"] = custom_params
custom_response = requests.post(
self.base_url + "/generate",
json=custom_json,
).json()
output_token_logprobs = custom_response["meta_info"]["output_token_logprobs"]
sampled_tokens = [x[1] for x in output_token_logprobs]
# The logit processor should always sample the given token as the logits is deterministic.
if first_token_id is not None:
self.assertTrue(
all(
x == custom_params["token_id"] + k
for k, x in enumerate(sampled_tokens[custom_params["delay"] :])
),
# Print the detailed test case info if the test fails.
f"{first_token_id=}\n{sampled_tokens=}\n{custom_response=}",
)
def test_custom_logit_processor(self):
"""Test custom logit processor with a single request."""
self.run_custom_logit_processor(target_token_id=5)
def test_custom_logit_processor_batch_mixed(self):
"""Test a batch of requests mixed of requests with and without custom logit processor."""
target_token_ids = list(range(32)) + [None] * 16
random.shuffle(target_token_ids)
with ThreadPoolExecutor(len(target_token_ids)) as executor:
list(executor.map(self.run_custom_logit_processor, target_token_ids))
@unittest.skip("Skip this test because this feature has a bug. See comments below.")
def test_stateful_custom_logit_processor(self):
"""Test custom logit processor with a single request."""
"""
NOTE: This feature has a race condition bug.
This line https://github.com/sgl-project/sglang/blob/ef8ec07b2ce4c70c2a33ec5acda4ce529bc3cda4/test/srt/test_srt_endpoint.py#L395-L396 can be accessed by two concurrent threads at the same time. The access order is not guaranteed.
In sglang, we use two python threads to overlap the GPU computation and CPU scheduling.
Thread 1 (the CPU scheduling thread) will update the `param_dict["__req__"].output_ids`.
Thread 2 (the GPU computation thread) will call `DeterministicStatefulLogitProcessor` because sampling is considered as GPU computation.
We can fix this by moving the call of DeterministicStatefulLogitProcessor to the CPU scheduling thread.
"""
self.run_stateful_custom_logit_processor(first_token_id=5)
@unittest.skip("Skip this test because this feature has a bug. See comments above.")
def test_stateful_custom_logit_processor_batch_mixed(self):
"""Test a batch of requests mixed of requests with and without custom logit processor."""
target_token_ids = list(range(32)) + [None] * 16
random.shuffle(target_token_ids)
with ThreadPoolExecutor(len(target_token_ids)) as executor:
list(
executor.map(self.run_stateful_custom_logit_processor, target_token_ids)
)
def test_cache_tokens(self):
for _ in range(2):
time.sleep(1)
response = requests.post(self.base_url + "/flush_cache")
assert response.status_code == 200
def send_and_check_cached_tokens(input_ids):
response = requests.post(
self.base_url + "/generate",
json={
"input_ids": list(input_ids),
"sampling_params": {
"max_new_tokens": 1,
},
},
)
response_json = response.json()
return response_json["meta_info"]["cached_tokens"]
self.assertEqual(send_and_check_cached_tokens(range(0, 100)), 0)
self.assertEqual(send_and_check_cached_tokens(range(0, 10000)), 100)
self.assertEqual(send_and_check_cached_tokens(range(0, 10000)), 9999)
self.assertEqual(send_and_check_cached_tokens(range(0, 1000)), 999)
self.assertEqual(send_and_check_cached_tokens(range(0, 11000)), 10000)
def test_get_server_info(self):
response = requests.get(self.base_url + "/get_server_info")
response_json = response.json()
max_total_num_tokens = response_json["max_total_num_tokens"]
self.assertIsInstance(max_total_num_tokens, int)
version = response_json["version"]
self.assertIsInstance(version, str)
def test_logit_bias(self):
"""Test that a very high logit bias forces sampling of a specific token."""
# Choose a token ID to bias (using 5 as an example)
target_token_id = 60704 # Paris for meta-llama/Llama-3.2-1B-Instruct, DEFAULT_SMALL_MODEL_NAME_FOR_TEST
logit_bias = {str(target_token_id): 100.0} # Very high positive bias
response = requests.post(
self.base_url + "/generate",
json={
"text": "The capital of France is",
"sampling_params": {
"temperature": 1.0, # Use high temperature to encourage exploration
"max_new_tokens": 4,
"logit_bias": logit_bias,
},
"return_logprob": True,
},
)
response_json = response.json()
# Extract the sampled token IDs from the output
output_token_logprobs = response_json["meta_info"]["output_token_logprobs"]
sampled_tokens = [x[1] for x in output_token_logprobs]
# Verify that all sampled tokens are the target token
self.assertTrue(
all(x == target_token_id for x in sampled_tokens),
f"Expected all tokens to be {target_token_id}, but got {sampled_tokens}",
)
def test_forbidden_token(self):
"""Test that a forbidden token (very negative logit bias) doesn't appear in the output."""
# Choose a token ID to forbid (using 10 as an example)
forbidden_token_id = 23994 # rice for meta-llama/Llama-3.2-1B-Instruct, DEFAULT_SMALL_MODEL_NAME_FOR_TEST
logit_bias = {
str(forbidden_token_id): -100.0
} # Very negative bias to forbid the token
response = requests.post(
self.base_url + "/generate",
json={
"text": "Only output 'rice' exactly like this, in lowercase ONLY: rice",
"sampling_params": {
"temperature": 1.0, # Use high temperature to encourage diverse output
"max_new_tokens": 50, # Generate enough tokens to likely include numbers
"logit_bias": logit_bias,
},
"return_logprob": True,
},
)
response_json = response.json()
# Extract the sampled token IDs from the output
output_token_logprobs = response_json["meta_info"]["output_token_logprobs"]
sampled_tokens = [x[1] for x in output_token_logprobs]
# Verify that the forbidden token doesn't appear in the output
self.assertNotIn(
forbidden_token_id,
sampled_tokens,
f"Expected forbidden token {forbidden_token_id} not to be present, but it was found",
)
def test_logit_bias_isolation(self):
"""Test that logit_bias applied to one request doesn't affect other requests in batch."""
# Choose a token ID to bias in first request only
biased_token_id = 60704 # Paris for meta-llama/Llama-3.2-1B-Instruct, DEFAULT_SMALL_MODEL_NAME_FOR_TEST
# Prepare batch requests - one with logit_bias and one without
requests_data = [
{
"text": "The capital of France is",
"sampling_params": {
"temperature": 1.0,
"max_new_tokens": 4,
"logit_bias": {str(biased_token_id): 100.0}, # Strong bias
},
"return_logprob": True,
},
{
"text": "The capital of France is",
"sampling_params": {
"temperature": 1.0,
"max_new_tokens": 4,
},
"return_logprob": True,
},
]
# Send both requests
responses = []
for req in requests_data:
response = requests.post(self.base_url + "/generate", json=req)
responses.append(response.json())
# Extract token IDs from each response
biased_tokens = [
x[1] for x in responses[0]["meta_info"]["output_token_logprobs"]
]
unbiased_tokens = [
x[1] for x in responses[1]["meta_info"]["output_token_logprobs"]
]
# Verify first response contains only biased tokens
self.assertTrue(
all(x == biased_token_id for x in biased_tokens),
f"Expected all tokens to be {biased_token_id} in first response, but got {biased_tokens}",
)
# Verify second response contains at least some different tokens
# (We can't guarantee exactly what tokens will be generated, but they shouldn't all be the biased token)
self.assertTrue(
any(x != biased_token_id for x in unbiased_tokens),
f"Expected some tokens to be different from {biased_token_id} in second response, but got {unbiased_tokens}",
)
def test_get_server_info_concurrent(self):
"""Make sure the concurrent get_server_info doesn't crash the server."""
tp = ThreadPoolExecutor(max_workers=30)
def s():
server_info = requests.get(self.base_url + "/get_server_info")
server_info.json()
futures = []
for _ in range(4):
futures.append(tp.submit(s))
for f in futures:
f.result()
# -------------------------------------------------------------------------
# /tokenize & /detokenize Test Class: TestTokenizeDetokenize
# -------------------------------------------------------------------------
class TestTokenizeDetokenize(CustomTestCase):
@classmethod
def setUpClass(cls):
cls.model = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
cls.base_url = DEFAULT_URL_FOR_TEST
cls.tokenize_url = f"{cls.base_url}/tokenize"
cls.detokenize_url = f"{cls.base_url}/detokenize"
cls.session = requests.Session()
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
)
@classmethod
def tearDownClass(cls):
kill_process_tree(cls.process.pid)
cls.session.close()
def _post_json(self, url, payload):
r = self.session.post(url, json=payload)
r.raise_for_status()
return r.json()
def test_tokenize_various_inputs(self):
single = "Hello SGLang world! 123 😊, ಪರ್ವತದ ಮೇಲೆ ಹಿಮ."
multi = ["First sentence.", "Second, with 中文."]
scenarios = [
{"prompt": single, "add_special_tokens": True},
{"prompt": single, "add_special_tokens": False},
{"prompt": multi, "add_special_tokens": True},
{"prompt": multi, "add_special_tokens": False},
{"prompt": "", "add_special_tokens": False},
]
for case in scenarios:
payload = {"model": self.model, "prompt": case["prompt"]}
if "add_special_tokens" in case:
payload["add_special_tokens"] = case["add_special_tokens"]
resp = self._post_json(self.tokenize_url, payload)
tokens = resp["tokens"]
count = resp["count"]
self.assertIsInstance(tokens, list)
if not tokens:
self.assertEqual(count, 0)
else:
if isinstance(tokens[0], list):
total = sum(len(t) for t in tokens)
expected = sum(count) if isinstance(count, list) else count
else:
total = len(tokens)
expected = count
self.assertEqual(total, expected)
def test_tokenize_invalid_type(self):
r = self.session.post(
self.tokenize_url, json={"model": self.model, "prompt": 12345}
)
self.assertEqual(r.status_code, 400)
def test_detokenize_roundtrip(self):
text = "Verify detokenization round trip. यह डिटोकेनाइजेशन है"
t0 = self._post_json(
self.tokenize_url,
{"model": self.model, "prompt": text, "add_special_tokens": False},
)["tokens"]
t1 = self._post_json(
self.tokenize_url,
{"model": self.model, "prompt": text, "add_special_tokens": True},
)["tokens"]
cases = [
{"tokens": t0, "skip_special_tokens": True, "expected": text},
{"tokens": t1, "skip_special_tokens": True, "expected": text},
{"tokens": t1, "skip_special_tokens": False, "expected": None},
{"tokens": [], "skip_special_tokens": True, "expected": ""},
]
for case in cases:
payload = {"model": self.model, "tokens": case["tokens"]}
if "skip_special_tokens" in case:
payload["skip_special_tokens"] = case["skip_special_tokens"]
resp = self._post_json(self.detokenize_url, payload)
text_out = resp["text"]
if case["expected"] is not None:
self.assertEqual(text_out, case["expected"])
else:
self.assertIsInstance(text_out, str)
def test_detokenize_invalid_tokens(self):
r = self.session.post(
self.detokenize_url, json={"model": self.model, "tokens": ["a", "b"]}
)
self.assertEqual(r.status_code, 400)
r2 = self.session.post(
self.detokenize_url, json={"model": self.model, "tokens": [1, -1, 2]}
)
self.assertEqual(r2.status_code, 500)
if __name__ == "__main__":
unittest.main()
-219
View File
@@ -1,219 +0,0 @@
"""
Usage:
python3 -m unittest test_srt_engine.TestSRTEngine.test_4_sync_async_stream_combination
"""
import asyncio
import json
import unittest
from types import SimpleNamespace
import torch
import sglang as sgl
from sglang.bench_offline_throughput import BenchArgs, throughput_test
from sglang.srt.server_args import ServerArgs
from sglang.srt.utils.hf_transformers_utils import get_tokenizer
from sglang.test.few_shot_gsm8k_engine import run_eval
from sglang.test.test_utils import (
DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST,
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
CustomTestCase,
)
class TestSRTEngine(CustomTestCase):
def test_1_engine_runtime_consistency(self):
prompt = "Today is a sunny day and I like"
model_path = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
sampling_params = {"temperature": 0, "max_new_tokens": 8}
engine = sgl.Engine(model_path=model_path, random_seed=42)
out1 = engine.generate(prompt, sampling_params)["text"]
engine.shutdown()
runtime = sgl.Runtime(model_path=model_path, random_seed=42)
out2 = json.loads(runtime.generate(prompt, sampling_params))["text"]
runtime.shutdown()
print("==== Answer 1 ====")
print(out1)
print("==== Answer 2 ====")
print(out2)
self.assertEqual(out1, out2)
def test_2_engine_runtime_encode_consistency(self):
prompt = "Today is a sunny day and I like"
model_path = DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST
engine = sgl.Engine(model_path=model_path, is_embedding=True, random_seed=42)
out1 = torch.tensor(engine.encode(prompt)["embedding"])
engine.shutdown()
runtime = sgl.Runtime(model_path=model_path, is_embedding=True, random_seed=42)
out2 = torch.tensor(json.loads(runtime.encode(prompt))["embedding"])
runtime.shutdown()
self.assertTrue(torch.allclose(out1, out2, atol=1e-5, rtol=1e-3))
def test_3_engine_token_ids_consistency(self):
# just to ensure there is no issue running multiple generate calls
prompt = "Today is a sunny day and I like"
model_path = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
sampling_params = {"temperature": 0, "max_new_tokens": 8}
engine = sgl.Engine(
model_path=model_path, random_seed=42, disable_radix_cache=True
)
out1 = engine.generate(prompt, sampling_params)["text"]
tokenizer = get_tokenizer(model_path)
token_ids = tokenizer.encode(prompt)
out2 = engine.generate(input_ids=token_ids, sampling_params=sampling_params)[
"text"
]
engine.shutdown()
print("==== Answer 1 ====")
print(out1)
print("==== Answer 2 ====")
print(out2)
self.assertEqual(out1, out2)
def test_4_sync_async_stream_combination(self):
prompt = "AI safety is"
sampling_params = {"temperature": 0.8, "top_p": 0.95}
# Create an LLM.
llm = sgl.Engine(
model_path=DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
)
if True:
# 1. sync + non streaming
print("\n\n==== 1. sync + non streaming ====")
output = llm.generate(prompt, sampling_params)
print(output["text"])
# 2. sync + streaming
print("\n\n==== 2. sync + streaming ====")
output_generator = llm.generate(prompt, sampling_params, stream=True)
offset = 0
for output in output_generator:
print(output["text"][offset:], end="", flush=True)
offset = len(output["text"])
print()
if True:
loop = asyncio.get_event_loop()
# 3. async + non_streaming
print("\n\n==== 3. async + non streaming ====")
output = loop.run_until_complete(
llm.async_generate(prompt, sampling_params)
)
print(output["text"])
# 4. async + streaming
async def async_streaming(engine):
generator = await engine.async_generate(
prompt, sampling_params, stream=True
)
offset = 0
async for output in generator:
print(output["text"][offset:], end="", flush=True)
offset = len(output["text"])
print()
print("\n\n==== 4. async + streaming ====")
loop.run_until_complete(async_streaming(llm))
llm.shutdown()
def test_5_gsm8k(self):
args = SimpleNamespace(
model_path=DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
local_data_path=None,
num_shots=5,
num_questions=1400,
)
metrics = run_eval(args)
self.assertGreater(metrics["accuracy"], 0.33)
def test_6_engine_cpu_offload(self):
prompt = "Today is a sunny day and I like"
model_path = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
sampling_params = {"temperature": 0, "max_new_tokens": 8}
engine = sgl.Engine(
model_path=model_path,
random_seed=42,
max_total_tokens=128,
)
out1 = engine.generate(prompt, sampling_params)["text"]
engine.shutdown()
engine = sgl.Engine(
model_path=model_path,
random_seed=42,
max_total_tokens=128,
cpu_offload_gb=3,
)
out2 = engine.generate(prompt, sampling_params)["text"]
engine.shutdown()
print("==== Answer 1 ====")
print(out1)
print("==== Answer 2 ====")
print(out2)
self.assertEqual(out1, out2)
def test_7_engine_offline_throughput(self):
server_args = ServerArgs(
model_path=DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
)
bench_args = BenchArgs(num_prompts=10)
result = throughput_test(server_args=server_args, bench_args=bench_args)
self.assertGreater(result["total_throughput"], 3000)
def test_8_engine_async_encode_consistency(self):
prompt = "Today is a sunny day and I like"
model_path = DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST
engine = sgl.Engine(
model_path=model_path,
is_embedding=True,
random_seed=42,
disable_radix_cache=True,
)
# Get sync and async embeddings
out1 = torch.tensor(engine.encode(prompt)["embedding"])
loop = asyncio.get_event_loop()
out2 = torch.tensor(
loop.run_until_complete(engine.async_encode(prompt))["embedding"]
)
engine.shutdown()
print("\n==== Shapes ====")
print(f"sync shape: {out1.shape}")
print(f"async shape: {out2.shape}")
self.assertTrue(
torch.allclose(out1, out2, atol=1e-5, rtol=1e-3),
"Sync and async embeddings are not equal within tolerance",
)
if __name__ == "__main__":
unittest.main()