Migrate sampling tests to test/registered/sampling/ (#16455)

This commit is contained in:
Alison Shao
2026-01-05 23:43:20 -08:00
committed by GitHub
parent 5e5b1183ed
commit 176266f358
4 changed files with 13 additions and 6 deletions
@@ -0,0 +1,200 @@
"""Test original log probability alignment between SGLang and Hugging Face.
This test suite verifies the correctness of the `origin_logprobs` output (temperature=1)
and the `logprobs` output (temperature=0.5) in SGLang by comparing it against
raw logit-based probabilities computed directly from a reference Hugging Face model.
The test covers the following scenarios:
- Next-token prediction: Verifies that the log probability of the next token from
SGLang matches the Hugging Face model.
- Top-k logprobs: Ensures that the top-k original logprobs returned by SGLang are
consistent with Hugging Face outputs.
- Specified token IDs: Confirms that the original logprobs for specific token IDs
match the values computed from Hugging Face logits.
"""
import os
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(est_time=41, suite="stage-b-test-small-1-gpu")
register_amd_ci(est_time=60, suite="stage-b-test-small-1-gpu")
import random
import unittest
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
import sglang as sgl
from sglang.test.test_utils import DEFAULT_SMALL_MODEL_NAME_FOR_TEST
# ------------------------- Configurable via env ------------------------- #
MODEL_ID = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
PROMPTS = [
"Hello, my name is",
"The future of AI is",
"The president of the United States is",
"The capital of France is ",
]
TOP_LOGPROBS_NUM = 50
NUM_RANDOM_TOKEN_IDS = 10
RTOL = 0.20
ATOL = 0.00
# ------------------------------------------------
torch.manual_seed(1234)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(1234)
torch.backends.cuda.matmul.allow_tf32 = False
torch.backends.cudnn.allow_tf32 = False
class TestOriginalLogprob(unittest.TestCase):
def setUp(self):
# ----- HF side (float32 weights) -----
self.tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, padding_side="right")
self.hf_model = AutoModelForCausalLM.from_pretrained(
MODEL_ID, torch_dtype=torch.float32, device_map="auto"
)
# Shared sampling parameters
self.sampling_params = {
"temperature": 0.5, # SGLang uses 0.5, but original logprobs are used 1.0
"top_p": 1.0,
"top_k": 10,
"max_new_tokens": 1,
}
# ---------------------------------------------------------------------
# Helper: compare one SGLang block (token_logprobs / top_logprobs / ids_logprobs)
# against a reference HF logprob vector.
# ---------------------------------------------------------------------
def assert_logprobs_block_equal(
self,
hf_log_probs: torch.Tensor, # [V]
token_log_probs: list,
top_log_probs: list,
ids_log_probs: list,
random_token_ids: list,
tag: str = "",
):
vals, idxs, _ = zip(*token_log_probs)
sgl_vals = torch.tensor(vals, device=self.hf_model.device, dtype=torch.float32)
sgl_idxs = torch.tensor(idxs, device=self.hf_model.device, dtype=torch.long)
hf_vals = hf_log_probs[sgl_idxs]
self.assertTrue(
torch.allclose(hf_vals, sgl_vals, rtol=RTOL, atol=ATOL),
msg=f"[{tag}] tokenlevel mismatch at indices {sgl_idxs.tolist()}",
)
hf_topk, _ = torch.topk(hf_log_probs, k=TOP_LOGPROBS_NUM, dim=-1)
sgl_topk = torch.tensor(
[float(t[0]) for t in top_log_probs[0] if t and t[0] is not None][
:TOP_LOGPROBS_NUM
],
dtype=torch.float32,
device=self.hf_model.device,
)
k = min(hf_topk.numel(), sgl_topk.numel())
self.assertTrue(
torch.allclose(hf_topk[:k], sgl_topk[:k], rtol=RTOL, atol=ATOL),
msg=f"[{tag}] topk mismatch",
)
indices = torch.tensor(
random_token_ids, dtype=torch.long, device=hf_log_probs.device
)
hf_token_ids = hf_log_probs[indices]
sgl_token_ids = torch.tensor(
[v for v, _, _ in ids_log_probs[0]],
device=self.hf_model.device,
dtype=torch.float32,
)
self.assertTrue(
torch.allclose(hf_token_ids, sgl_token_ids, rtol=RTOL, atol=ATOL),
msg=f"[{tag}] tokenIDs mismatch",
)
# Optional: print max abs diff for quick diagnostics
max_diff = torch.max(torch.abs(hf_vals - sgl_vals)).item()
print(f"[{tag}] max|diff| tokenlevel = {max_diff:.4f}")
def test_logprob_match(self):
vocab_size = self.tokenizer.vocab_size
for env_val in ["True", "False"]:
with self.subTest(SGLANG_RETURN_ORIGINAL_LOGPROB=env_val):
os.environ["SGLANG_RETURN_ORIGINAL_LOGPROB"] = env_val
# ----- SGLang side -----
sgl_engine = sgl.Engine(
model_path=MODEL_ID,
skip_tokenizer_init=True,
trust_remote_code=True,
mem_fraction_static=0.60,
)
for prompt in PROMPTS:
random_token_ids = sorted(
random.sample(range(vocab_size), NUM_RANDOM_TOKEN_IDS)
)
enc = self.tokenizer(prompt, return_tensors="pt")
input_ids = enc["input_ids"].to(self.hf_model.device)
attn_mask = enc["attention_mask"].to(self.hf_model.device)
with torch.inference_mode():
hf_out = self.hf_model(
input_ids=input_ids,
attention_mask=attn_mask,
return_dict=True,
)
logits = hf_out.logits[:, -1, :] # [1, V]
hf_log_probs = F.log_softmax(
logits.float() / self.sampling_params["temperature"], dim=-1
)[0]
hf_original_log_probs = F.log_softmax(logits.float(), dim=-1)[0]
outputs = sgl_engine.generate(
input_ids=input_ids[0].tolist(),
sampling_params=self.sampling_params,
return_logprob=True,
top_logprobs_num=TOP_LOGPROBS_NUM,
token_ids_logprob=random_token_ids,
)
if isinstance(outputs, list):
outputs = outputs[0]
meta = outputs["meta_info"]
# Check original logprobs only if enabled
if env_val.lower() == "true":
self.assert_logprobs_block_equal(
hf_log_probs=hf_original_log_probs,
token_log_probs=meta["output_token_logprobs"],
top_log_probs=meta["output_top_logprobs"],
ids_log_probs=meta["output_token_ids_logprobs"],
random_token_ids=random_token_ids,
tag=f"Original logprobs SGLang vs HF: {prompt} ({env_val})",
)
else:
# Always check regular logprobs
self.assert_logprobs_block_equal(
hf_log_probs=hf_log_probs,
token_log_probs=meta["output_token_logprobs"],
top_log_probs=meta["output_top_logprobs"],
ids_log_probs=meta["output_token_ids_logprobs"],
random_token_ids=random_token_ids,
tag=f"logprobs SGLang vs HF: {prompt} ({env_val})",
)
sgl_engine.shutdown()
if __name__ == "__main__":
unittest.main()
+250
View File
@@ -0,0 +1,250 @@
import json
import random
import re
import unittest
from concurrent.futures import ThreadPoolExecutor
import requests
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(est_time=82, suite="stage-b-test-small-1-gpu")
register_amd_ci(est_time=180, suite="stage-b-test-small-1-gpu")
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 TestPenalty(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,
)
@classmethod
def tearDownClass(cls):
kill_process_tree(cls.process.pid)
def run_decode(self, sampling_params):
"""Helper method for basic decode tests."""
return_logprob = True
top_logprobs_num = 5
return_text = True
n = 1
response = requests.post(
self.base_url + "/generate",
json={
# prompt that is supposed to generate < 32 tokens
"text": "<|start_header_id|>user<|end_header_id|>\n\nWhat is the answer for 1 + 1 = ?<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n",
"sampling_params": {
"max_new_tokens": 48,
"n": n,
**sampling_params,
},
"return_logprob": return_logprob,
"top_logprobs_num": top_logprobs_num,
"return_text_in_logprobs": return_text,
"logprob_start_len": 0,
},
)
self.assertEqual(response.status_code, 200)
print(json.dumps(response.json()))
print("=" * 100)
def run_generate_with_prompt(self, prompt, sampling_params, max_tokens=100):
"""Helper method to generate text with a specific prompt and parameters."""
sampling_params.setdefault("temperature", 0.05)
sampling_params.setdefault("top_p", 1.0)
response = requests.post(
self.base_url + "/v1/chat/completions",
json={
"model": self.model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": max_tokens,
**sampling_params,
},
)
self.assertEqual(response.status_code, 200)
result = response.json()
content = result["choices"][0]["message"]["content"]
return content
def count_word_repetitions(self, text, word):
"""Count how many times a specific word appears in the text."""
return len(re.findall(r"\b" + re.escape(word) + r"\b", text.lower()))
def _test_penalty_effect(
self,
prompt,
baseline_params,
penalty_params,
target_word,
expected_reduction=True,
max_tokens=50,
):
"""Generic test for penalty effects."""
# Run multiple iterations to get more reliable results
baseline_counts = []
penalty_counts = []
for i in range(5):
baseline_output = self.run_generate_with_prompt(
prompt, baseline_params, max_tokens
)
penalty_output = self.run_generate_with_prompt(
prompt, penalty_params, max_tokens
)
baseline_count = self.count_word_repetitions(baseline_output, target_word)
penalty_count = self.count_word_repetitions(penalty_output, target_word)
baseline_counts.append(baseline_count)
penalty_counts.append(penalty_count)
# Calculate averages
avg_baseline = sum(baseline_counts) / len(baseline_counts)
avg_penalty = sum(penalty_counts) / len(penalty_counts)
if expected_reduction:
# Simple check: penalty should reduce repetition
self.assertLess(
avg_penalty,
avg_baseline,
f"Penalty should reduce '{target_word}' repetition: {avg_baseline:.1f}{avg_penalty:.1f}",
)
else:
self.assertGreater(
avg_penalty,
avg_baseline,
f"Negative penalty should increase '{target_word}' repetition",
)
def test_default_values(self):
self.run_decode({})
def test_frequency_penalty(self):
self.run_decode({"frequency_penalty": 2})
def test_min_new_tokens(self):
self.run_decode({"min_new_tokens": 16})
def test_presence_penalty(self):
self.run_decode({"presence_penalty": 2})
def test_penalty_mixed(self):
args = [
{},
{},
{},
{"frequency_penalty": 2},
{"presence_penalty": 1},
{"min_new_tokens": 16},
{"frequency_penalty": 0.2},
{"presence_penalty": 0.4},
{"min_new_tokens": 8},
{"frequency_penalty": 0.4, "presence_penalty": 0.8},
{"frequency_penalty": 0.4, "min_new_tokens": 12},
{"presence_penalty": 0.8, "min_new_tokens": 12},
{"presence_penalty": -0.3, "frequency_penalty": 1.3, "min_new_tokens": 32},
{"presence_penalty": 0.3, "frequency_penalty": -1.3, "min_new_tokens": 32},
]
random.shuffle(args * 5)
with ThreadPoolExecutor(8) as executor:
list(executor.map(self.run_decode, args))
def test_frequency_penalty_reduces_word_repetition(self):
"""Test frequency penalty using word repetition."""
prompt = "Write exactly 10 very small sentences, each containing the word 'data'. Use the word 'data' as much as possible."
baseline_params = {"frequency_penalty": 0.0, "repetition_penalty": 1.0}
penalty_params = {"frequency_penalty": 1.99, "repetition_penalty": 1.0}
self._test_penalty_effect(prompt, baseline_params, penalty_params, "data")
def test_presence_penalty_reduces_topic_repetition(self):
"""Test presence penalty using topic repetition."""
prompt = "Write the word 'machine learning' exactly 20 times in a row, separated by spaces."
baseline_params = {"presence_penalty": 0.0, "repetition_penalty": 1.0}
penalty_params = {"presence_penalty": 1.99, "repetition_penalty": 1.0}
self._test_penalty_effect(
prompt, baseline_params, penalty_params, "machine learning"
)
def test_combined_penalties_reduce_repetition(self):
"""Test combined penalty effects."""
prompt = "Write exactly 10 short sentences, each containing the word 'data'. Use the word 'data' as much as possible."
baseline_params = {
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
"repetition_penalty": 1.0,
}
penalty_params = {
"frequency_penalty": 1.99,
"presence_penalty": 1.99,
"repetition_penalty": 1.99,
}
self._test_penalty_effect(
prompt, baseline_params, penalty_params, "data", max_tokens=100
)
def test_penalty_edge_cases_negative_penalty_values(self):
"""Test edge cases with negative penalty values."""
prompt = "Write the word 'test' exactly 15 times in a row, separated by spaces."
baseline_params = {
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
"repetition_penalty": 1.0,
}
negative_penalty_params = {
"frequency_penalty": -0.5,
"presence_penalty": -0.25,
"repetition_penalty": 1.0,
}
# Negative penalties should increase repetition (expected_reduction=False)
self._test_penalty_effect(
prompt,
baseline_params,
negative_penalty_params,
"test",
expected_reduction=False,
max_tokens=60,
)
def test_penalty_edge_cases_extreme_penalty_values(self):
"""Test edge cases with extreme penalty values."""
prompt = (
"Write the word 'extreme' exactly 20 times in a row, separated by spaces."
)
baseline_params = {
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
"repetition_penalty": 1.0,
}
extreme_penalty_params = {
"frequency_penalty": 2.0,
"presence_penalty": 2.0,
"repetition_penalty": 2.0,
}
# Extreme penalties should strongly reduce repetition
self._test_penalty_effect(
prompt,
baseline_params,
extreme_penalty_params,
"extreme",
expected_reduction=True,
max_tokens=80,
)
if __name__ == "__main__":
unittest.main(verbosity=3)
@@ -0,0 +1,94 @@
import unittest
from types import SimpleNamespace
import requests
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(est_time=66, suite="stage-b-test-small-1-gpu")
register_amd_ci(est_time=66, suite="stage-b-test-small-1-gpu")
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 TestPyTorchSamplingBackend(CustomTestCase):
@classmethod
def setUpClass(cls):
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=["--sampling-backend", "pytorch", "--disable-radix-cache"],
)
@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,
temperature=0.1,
)
metrics = run_eval(args)
self.assertGreaterEqual(metrics["score"], 0.65)
def test_greedy(self):
first_text = None
# ensure the answer is identical across single response
for _ in range(5):
response_single = requests.post(
self.base_url + "/generate",
json={
"text": "The capital of Germany is",
"sampling_params": {
"temperature": 0,
"max_new_tokens": 32,
},
},
).json()
text = response_single["text"]
if first_text is None:
first_text = text
self.assertEqual(text, first_text)
first_text = None
response_batch = requests.post(
self.base_url + "/generate",
json={
"text": ["The capital of Germany is"] * 10,
"sampling_params": {
"temperature": 0,
"max_new_tokens": 32,
},
},
).json()
# ensure the answer is identical among the batch
for i in range(10):
text = response_batch[i]["text"]
if first_text is None:
first_text = text
self.assertEqual(text, first_text)
if __name__ == "__main__":
unittest.main()