[misc] CI hygiene: enforce __main__ entry, drop silent-skipped tests, fix rerun-test protoc (#23305)

This commit is contained in:
Liangsheng Yin
2026-04-20 21:16:24 -07:00
committed by GitHub
parent 6b19e8a452
commit 6cc2eee50d
19 changed files with 101 additions and 3168 deletions
+8
View File
@@ -138,6 +138,14 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v5
# Needed by setuptools-rust to build the bundled native gRPC extension
# (rust/sglang-grpc) when installing the main `sglang` wheel from source.
- name: Install protoc
run: sudo bash scripts/ci/utils/install_protoc.sh
- name: Install Rust toolchain
run: bash scripts/ci/utils/install_rustup.sh
- name: Install dependencies
timeout-minutes: 20
env:
@@ -1,13 +0,0 @@
import pytest
from sglang.jit_kernel.utils import _REGISTERED_DEPENDENCIES
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=30, suite="stage-b-kernel-unit-1-gpu-large")
register_cuda_ci(est_time=30, suite="nightly-kernel-1-gpu", nightly=True)
@pytest.mark.parametrize("name", _REGISTERED_DEPENDENCIES.keys())
def test_availability(name: str) -> None:
# NOTE: the path resolution should not fail
_REGISTERED_DEPENDENCIES[name]()
+53 -10
View File
@@ -2,7 +2,7 @@ import ast
import warnings
from dataclasses import dataclass
from enum import Enum, auto
from typing import List, Optional
from typing import List, Optional, Tuple
__all__ = [
"HWBackend",
@@ -87,6 +87,7 @@ class RegistryVisitor(ast.NodeVisitor):
def __init__(self, filename: str):
self.filename = filename
self.registries: list[CIRegistry] = []
self.has_main_entry: bool = False
def _constant_value(self, node: ast.AST) -> object:
if isinstance(node, ast.Constant):
@@ -180,25 +181,53 @@ class RegistryVisitor(ast.NodeVisitor):
disabled=disabled,
)
@staticmethod
def _is_main_block_with_call(stmt: ast.If) -> bool:
"""True iff `stmt` is `if __name__ == "__main__":` with a body that
contains at least one call (i.e. actually runs something, not just
`pass`). This is what makes `python3 file.py` execute tests."""
test = stmt.test
if not isinstance(test, ast.Compare):
return False
if not (isinstance(test.left, ast.Name) and test.left.id == "__name__"):
return False
if len(test.ops) != 1 or not isinstance(test.ops[0], ast.Eq):
return False
if len(test.comparators) != 1:
return False
rhs = test.comparators[0]
if not (isinstance(rhs, ast.Constant) and rhs.value == "__main__"):
return False
for child in ast.walk(ast.Module(body=stmt.body, type_ignores=[])):
if isinstance(child, ast.Call):
return True
return False
def visit_Module(self, node):
for stmt in node.body:
if not isinstance(stmt, ast.Expr) or not isinstance(stmt.value, ast.Call):
continue
cr = self._collect_ci_registry(stmt.value)
if cr is not None:
self.registries.append(cr)
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
cr = self._collect_ci_registry(stmt.value)
if cr is not None:
self.registries.append(cr)
elif isinstance(stmt, ast.If) and self._is_main_block_with_call(stmt):
self.has_main_entry = True
self.generic_visit(node)
def ut_parse_one_file(filename: str) -> List[CIRegistry]:
def ut_parse_one_file(filename: str) -> Tuple[List[CIRegistry], bool]:
"""Parse a test file and return (registries, has_main_entry).
`has_main_entry` is True iff the file has `if __name__ == "__main__":`
with a call in its body -- required for `python3 file.py` to actually
run tests (the CI runner's invocation pattern).
"""
with open(filename, "r") as f:
file_content = f.read()
tree = ast.parse(file_content, filename=filename)
visitor = RegistryVisitor(filename=filename)
visitor.visit(tree)
return visitor.registries
return visitor.registries, visitor.has_main_entry
def auto_partition(files: List[CIRegistry], rank: int, size: int) -> List[CIRegistry]:
@@ -230,7 +259,7 @@ def auto_partition(files: List[CIRegistry], rank: int, size: int) -> List[CIRegi
def collect_tests(files: list[str], sanity_check: bool = True) -> List[CIRegistry]:
ci_tests = []
for file in files:
registries = ut_parse_one_file(file)
registries, has_main_entry = ut_parse_one_file(file)
if len(registries) == 0:
msg = f"No CI registry found in {file}"
if sanity_check:
@@ -239,6 +268,20 @@ def collect_tests(files: list[str], sanity_check: bool = True) -> List[CIRegistr
warnings.warn(msg)
continue
# Every file with at least one enabled registry must have an
# executable `if __name__ == "__main__":` block; otherwise
# `python3 file.py -f` (how run_unittest_files invokes tests)
# silently exits and the file shows green without running.
has_enabled = any(r.disabled is None for r in registries)
if sanity_check and has_enabled and not has_main_entry:
raise ValueError(
f'{file}: missing `if __name__ == "__main__":` entry. '
f"Pytest-style tests in this file will silently skip under "
f"`python3 file.py -f`. Add `unittest.main()` (for "
f"unittest.TestCase) or `sys.exit(pytest.main([__file__, "
f'"-v"]))` (for pytest-style).'
)
ci_tests.extend(registries)
return ci_tests
+1 -1
View File
@@ -34,7 +34,7 @@ def main() -> int:
errors = []
for f in files:
try:
registries = ci_register.ut_parse_one_file(f)
registries, _has_main_entry = ci_register.ut_parse_one_file(f)
if len(registries) == 0:
errors.append(f)
except Exception:
+1 -1
View File
@@ -35,7 +35,7 @@ def collect_all_tests(registered_dir: str) -> list[CIRegistry]:
for file in sorted(files):
try:
registries = ut_parse_one_file(file)
registries, _ = ut_parse_one_file(file)
all_tests.extend(registries)
except Exception as e:
print(f"Warning: Failed to parse {file}: {e}", file=sys.stderr)
-150
View File
@@ -1,150 +0,0 @@
"""AMD nightly test for Z-Image-Turbo diffusion model (text-to-image)."""
import io
import logging
import os
import pytest
from sglang.multimodal_gen.test.server.test_server_common import ( # noqa: F401
DiffusionServerBase,
diffusion_server,
)
from sglang.multimodal_gen.test.server.test_server_utils import (
ServerContext,
get_generate_fn,
)
from sglang.multimodal_gen.test.server.testcase_configs import (
DiffusionSamplingParams,
DiffusionServerArgs,
DiffusionTestCase,
)
from sglang.test.ci.ci_register import register_amd_ci
logger = logging.getLogger(__name__)
register_amd_ci(est_time=1800, suite="nightly-amd-1-gpu-zimage-turbo", nightly=True)
AMD_ZIMAGE_CASES = [
DiffusionTestCase(
"zimage_image_t2i",
DiffusionServerArgs(model_path="Tongyi-MAI/Z-Image-Turbo", modality="image"),
DiffusionSamplingParams(
prompt="Doraemon is eating dorayaki",
output_size="1024x1024",
),
),
]
CLIP_SCORE_THRESHOLD = 0.20
ARTIFACT_DIR = os.environ.get(
"SGLANG_DIFFUSION_ARTIFACT_DIR", "/tmp/diffusion-artifacts"
)
def _save_image_and_write_summary(
case_id: str, prompt: str, image_bytes: bytes, clip_score: float | None = None
):
"""Save generated image to artifact dir and write summary."""
ext = "jpg" if image_bytes[:2] == b"\xff\xd8" else "png"
os.makedirs(ARTIFACT_DIR, exist_ok=True)
img_path = os.path.join(ARTIFACT_DIR, f"{case_id}.{ext}")
with open(img_path, "wb") as f:
f.write(image_bytes)
logger.info("Saved image artifact: %s (%d bytes)", img_path, len(image_bytes))
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
if not summary_file:
return
clip_line = ""
if clip_score is not None:
status = "PASS" if clip_score >= CLIP_SCORE_THRESHOLD else "FAIL"
clip_line = f"| CLIP Score | {clip_score:.4f} ({status}, threshold: {CLIP_SCORE_THRESHOLD}) |\n"
md = (
f"### Z-Image-Turbo — `{case_id}`\n\n"
f"| | |\n|---|---|\n"
f"| Prompt | {prompt} |\n"
f"| Size | {len(image_bytes):,} bytes |\n"
f"{clip_line}"
f"| Artifact | `{case_id}.{ext}` (download from Artifacts section above) |\n\n"
)
with open(summary_file, "a") as f:
f.write(md)
def _compute_clip_score(image_bytes: bytes, prompt: str) -> float | None:
"""Compute CLIP cosine similarity between the image and prompt."""
try:
import torch
from PIL import Image
from transformers import CLIPModel, CLIPProcessor
model_name = "openai/clip-vit-base-patch32"
processor = CLIPProcessor.from_pretrained(model_name)
model = CLIPModel.from_pretrained(model_name)
model.eval()
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
inputs = processor(text=[prompt], images=image, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
score = outputs.logits_per_image.item() / 100.0
logger.info("CLIP score for '%s': %.4f", prompt, score)
return score
except Exception as e:
logger.warning("CLIP score computation failed: %s", e)
return None
class TestZImageTurboAMD(DiffusionServerBase):
"""AMD nightly test for Z-Image-Turbo text-to-image generation."""
@classmethod
def teardown_class(cls):
try:
super().teardown_class()
except AttributeError:
pass
@pytest.fixture(params=AMD_ZIMAGE_CASES, ids=lambda c: c.id)
def case(self, request) -> DiffusionTestCase:
return request.param
def test_diffusion_generation(
self,
case: DiffusionTestCase,
diffusion_server: ServerContext,
):
generate_fn = get_generate_fn(
model_path=case.server_args.model_path,
modality=case.server_args.modality,
sampling_params=case.sampling_params,
)
perf_record, content = self.run_and_collect(
diffusion_server, case.id, generate_fn
)
self._validate_and_record(case, perf_record)
self._test_v1_models_endpoint(diffusion_server, case)
prompt = case.sampling_params.prompt or ""
clip_score = _compute_clip_score(content, prompt)
if clip_score is not None:
logger.info(
"CLIP score: %.4f (threshold: %.2f)", clip_score, CLIP_SCORE_THRESHOLD
)
assert clip_score >= CLIP_SCORE_THRESHOLD, (
f"CLIP score {clip_score:.4f} below threshold {CLIP_SCORE_THRESHOLD} "
f"for prompt '{prompt}'"
)
_save_image_and_write_summary(case.id, prompt, content, clip_score)
@@ -42,3 +42,9 @@ class TestExpandPreset:
"""Unknown preset name raises ValueError."""
with pytest.raises(ValueError, match="Unknown value for --preset"):
expand_preset(["--preset", "nonexistent"], presets=PRESETS)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -254,3 +254,9 @@ class TestCodePatcher:
raise RuntimeError("test error")
assert obj.greet("world") == "hello world"
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -55,3 +55,11 @@ class TestDumperApplySourcePatches:
cls.greet.__code__ = original_code
assert obj.greet("world") == "hello world"
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main([__file__, "-v"]))
@@ -290,3 +290,9 @@ class TestApplyEdits:
]
result = apply_edits(source=source, edits=edits)
assert result == ("def foo():\n" " x = 1\n" " y = 20\n" " return x\n")
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -1,6 +1,3 @@
from argparse import Namespace
from pathlib import Path
import pytest
import torch
@@ -9,9 +6,7 @@ from sglang.srt.debug_utils.dump_comparator import (
_calc_rel_diff,
_compute_smaller_dtype,
_try_unify_shape,
main,
)
from sglang.srt.debug_utils.dumper import DumperConfig, _Dumper
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=30, suite="stage-a-test-cpu", nightly=True)
@@ -59,101 +54,7 @@ class TestComputeSmallerDtype:
assert _compute_smaller_dtype(torch.float32, torch.float32) is None
# ----------------------------- Integration tests -----------------------------
if __name__ == "__main__":
import sys
def _make_dumper(directory: Path) -> _Dumper:
return _Dumper(
config=DumperConfig(
enable=True,
dir=str(directory),
)
)
def _create_dumps(
tmp_path: Path,
tensor_names: list[str],
*,
baseline_names: list[str] | None = None,
) -> tuple[Path, Path]:
if baseline_names is None:
baseline_names = tensor_names
d_baseline: Path = tmp_path / "baseline"
d_target: Path = tmp_path / "target"
d_baseline.mkdir()
d_target.mkdir()
torch.manual_seed(42)
baseline_tensor: torch.Tensor = torch.randn(10, 10)
target_tensor: torch.Tensor = baseline_tensor + torch.randn(10, 10) * 0.01
exp_paths: list[Path] = []
for d, names, tensor in [
(d_baseline, baseline_names, baseline_tensor),
(d_target, tensor_names, target_tensor),
]:
dumper: _Dumper = _make_dumper(d)
for name in names:
dumper.dump(name, tensor)
dumper.step()
exp_paths.append(d / dumper._config.exp_name)
return exp_paths[0], exp_paths[1]
def _make_args(
baseline_path: Path,
target_path: Path,
*,
filter_pattern: str | None = None,
) -> Namespace:
return Namespace(
baseline_path=str(baseline_path),
target_path=str(target_path),
start_step=0,
end_step=1000000,
diff_threshold=1e-3,
filter=filter_pattern,
)
class TestMainBasic:
def test_matching_tensors(
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
baseline_path, target_path = _create_dumps(tmp_path, ["tensor_a", "tensor_b"])
args: Namespace = _make_args(baseline_path, target_path)
main(args)
captured: str = capsys.readouterr().out
assert "✅" in captured
def test_with_filter(self, tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
baseline_path, target_path = _create_dumps(tmp_path, ["tensor_a", "tensor_b"])
args: Namespace = _make_args(
baseline_path, target_path, filter_pattern="tensor_a"
)
main(args)
captured: str = capsys.readouterr().out
assert "tensor_a" in captured
assert "Check:" in captured
def test_no_match_skips(
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
baseline_path, target_path = _create_dumps(
tmp_path,
["only_in_target"],
baseline_names=["only_in_baseline"],
)
args: Namespace = _make_args(baseline_path, target_path)
main(args)
captured: str = capsys.readouterr().out
assert "Skip" in captured
sys.exit(pytest.main([__file__, "-v"]))
@@ -1,148 +0,0 @@
import time
from typing import Tuple
import numpy as np
import pytest
import torch
from sglang.srt.models.utils import compute_cu_seqlens_from_grid_numpy as cpu_numpy_impl
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
# Ops - Repeat Interleave tests (1-GPU)
register_cuda_ci(est_time=7, suite="stage-b-test-1-gpu-small")
register_amd_ci(est_time=75, suite="stage-b-test-1-gpu-small-amd")
def torch_ref_impl(grid_thw: torch.Tensor) -> torch.Tensor:
"""
Pure PyTorch implementation of cu_seqlens computation.
Assumes grid_thw is already on the correct device (CPU here).
Shape: [T, 3], columns: [repeat_count, H, W]
"""
cu_seqlens = torch.repeat_interleave(
grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
).cumsum(dim=0)
cu_seqlens = torch.cat(
[
torch.zeros(1, dtype=torch.int32, device=cu_seqlens.device),
cu_seqlens.to(torch.int32),
]
)
return cu_seqlens
def benchmark_once(fn, grid_thw, iters: int = 1000):
"""
Run a function `fn` on the same input `grid_thw` for `iters` times
and measure total elapsed time.
"""
start = time.perf_counter()
for _ in range(iters):
out = fn(grid_thw)
end = time.perf_counter()
return (end - start), out
# (T, repeat_min, repeat_max)
GRID_TEST_CONFIGS: list[Tuple[int, int, int]] = [
(16, 1, 4), # small T, small repeat counts
(128, 0, 4), # allow repeat=0 to test edge cases
(512, 1, 8),
(1024, 1, 16),
]
NUM_CASES_PER_CONFIG = 10
def _generate_random_grid(T: int, repeat_min: int, repeat_max: int) -> torch.Tensor:
"""
grid_thw: [T, 3]
col0: repeat count
col1, col2: arbitrary positive integers (here 1..16)
"""
repeats = torch.randint(repeat_min, repeat_max + 1, (T, 1), dtype=torch.int32)
th = torch.randint(1, 17, (T, 1), dtype=torch.int32)
tw = torch.randint(1, 17, (T, 1), dtype=torch.int32)
grid_thw = torch.cat([repeats, th, tw], dim=1)
return grid_thw
class TestRepeatInterleave:
@classmethod
def setup_class(cls):
torch.set_num_threads(1)
def setup_method(self, method):
torch.manual_seed(0)
np.random.seed(0)
@pytest.mark.parametrize(
"T,repeat_min,repeat_max",
GRID_TEST_CONFIGS,
)
@pytest.mark.parametrize("case_idx", range(NUM_CASES_PER_CONFIG))
def test_cpu_correctness_random_cases(
self,
T: int,
repeat_min: int,
repeat_max: int,
case_idx: int,
):
torch.manual_seed(case_idx)
np.random.seed(case_idx)
grid_thw = _generate_random_grid(T, repeat_min, repeat_max)
grid_clone = grid_thw.clone()
out_torch = torch_ref_impl(grid_thw)
out_numpy = cpu_numpy_impl(grid_thw)
assert torch.equal(grid_thw, grid_clone), "Function modified input grid_thw!"
assert (
out_torch.shape == out_numpy.shape
), f"Shape mismatch: torch={out_torch.shape}, numpy={out_numpy.shape}"
assert (
out_torch.dtype == torch.int32
), f"Unexpected torch dtype: {out_torch.dtype}"
assert (
out_numpy.dtype == torch.int32
), f"Unexpected numpy impl dtype: {out_numpy.dtype}"
if not torch.equal(out_torch.cpu(), out_numpy.cpu()):
diff_idx = (out_torch.cpu() != out_numpy.cpu()).nonzero(as_tuple=False)
idx0 = diff_idx[0].item()
pytest.fail(
f"Value mismatch, T={T}, case_idx={case_idx}, first differing index={idx0}, "
f"torch={out_torch[idx0].item()}, "
f"numpy={out_numpy[idx0].item()}"
)
def test_zero_repeat_edge_case(self):
T = 4
grid_thw = torch.tensor(
[
[0, 4, 4],
[1, 2, 3], # 6
[2, 1, 5], # 5, 5
[0, 7, 7], # 0
],
dtype=torch.int32,
)
grid_clone = grid_thw.clone()
out_torch = torch_ref_impl(grid_thw)
out_numpy = cpu_numpy_impl(grid_thw)
assert torch.equal(
grid_thw, grid_clone
), "Function modified input grid_thw with zero repeats!"
assert torch.equal(
out_torch.cpu(), out_numpy.cpu()
), f"Zero-repeat case mismatch: torch={out_torch}, numpy={out_numpy}"
-310
View File
@@ -1,310 +0,0 @@
"""
Usage:
python3 -m unittest test_bnb.TestVisionModel.test_vlm
python3 -m unittest test_bnb.TestLanguageModel.test_mmlu
"""
import multiprocessing as mp
import random
from concurrent.futures import ThreadPoolExecutor
from types import SimpleNamespace
import openai
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_cuda_ci
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,
)
register_cuda_ci(est_time=6, suite="stage-b-test-1-gpu-small")
VISION_MODELS = [
"unsloth/Qwen2.5-VL-7B-Instruct-bnb-4bit",
"unsloth/Qwen2-VL-7B-Instruct-bnb-4bit",
"unsloth/Llama-3.2-11B-Vision-Instruct-bnb-4bit",
"unsloth/Llama-3.2-11B-Vision-bnb-4bit",
"unsloth/gemma-3-4b-it-bnb-4bit",
"unsloth/gemma-3-4b-it-unsloth-bnb-4bit",
]
LANGUAGE_MODELS = [
"unsloth/Qwen2.5-7B-Instruct-bnb-4bit",
"unsloth/Qwen2-7B-Instruct-bnb-4bit",
"unsloth/Llama-3.2-3B-Instruct-bnb-4bit",
"unsloth/gemma-3-1b-it-bnb-4bit",
]
# image
IMAGE_MAN_IRONING_URL = "https://raw.githubusercontent.com/sgl-project/sgl-test-files/refs/heads/main/images/man_ironing_on_back_of_suv.png"
IMAGE_SGL_LOGO_URL = "https://raw.githubusercontent.com/sgl-project/sgl-test-files/refs/heads/main/images/sgl_logo.png"
# video
VIDEO_JOBS_URL = "https://raw.githubusercontent.com/sgl-project/sgl-test-files/refs/heads/main/videos/jobs_presenting_ipod.mp4"
# audio
AUDIO_TRUMP_SPEECH_URL = "https://raw.githubusercontent.com/sgl-project/sgl-test-files/refs/heads/main/audios/Trump_WEF_2018_10s.mp3"
AUDIO_BIRD_SONG_URL = "https://raw.githubusercontent.com/sgl-project/sgl-test-files/refs/heads/main/audios/bird_song.mp3"
def popen_launch_server_wrapper(base_url, model, other_args):
process = popen_launch_server(
model,
base_url,
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
other_args=other_args,
)
return process
class TestVisionModel(CustomTestCase):
@classmethod
def setUpClass(cls):
mp.set_start_method("spawn", force=True)
cls.base_url = DEFAULT_URL_FOR_TEST
cls.base_url += "/v1"
cls.api_key = "sk-123456"
def _run_single_image_chat_completion(self):
client = openai.Client(api_key=self.api_key, base_url=self.base_url)
response = client.chat.completions.create(
model="default",
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {"url": IMAGE_MAN_IRONING_URL},
},
{
"type": "text",
"text": "Describe this image in a very short sentence.",
},
],
},
],
temperature=0,
)
assert response.choices[0].message.role == "assistant"
text = response.choices[0].message.content
assert isinstance(text, str)
# `driver` is for gemma-3-it
assert "man" in text or "person" or "driver" in text, text
assert "cab" in text or "taxi" in text or "SUV" in text, text
# MiniCPMO fails to recognize `iron`, but `hanging`
assert "iron" in text or "hang" in text, text
assert response.id
assert response.created
assert response.usage.prompt_tokens > 0
assert response.usage.completion_tokens > 0
assert response.usage.total_tokens > 0
def _run_multi_turn_chat_completion(self):
client = openai.Client(api_key=self.api_key, base_url=self.base_url)
response = client.chat.completions.create(
model="default",
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {"url": IMAGE_MAN_IRONING_URL},
},
{
"type": "text",
"text": "Describe this image in a very short sentence.",
},
],
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "There is a man at the back of a yellow cab ironing his clothes.",
}
],
},
{
"role": "user",
"content": [
{"type": "text", "text": "Repeat your previous answer."}
],
},
],
temperature=0,
)
assert response.choices[0].message.role == "assistant"
text = response.choices[0].message.content
assert isinstance(text, str)
assert "man" in text or "cab" in text, text
assert response.id
assert response.created
assert response.usage.prompt_tokens > 0
assert response.usage.completion_tokens > 0
assert response.usage.total_tokens > 0
def _run_multi_images_chat_completion(self):
client = openai.Client(api_key=self.api_key, base_url=self.base_url)
response = client.chat.completions.create(
model="default",
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {"url": IMAGE_MAN_IRONING_URL},
"modalities": "multi-images",
},
{
"type": "image_url",
"image_url": {"url": IMAGE_SGL_LOGO_URL},
"modalities": "multi-images",
},
{
"type": "text",
"text": "I have two very different images. They are not related at all. "
"Please describe the first image in one sentence, and then describe the second image in another sentence.",
},
],
},
],
temperature=0,
)
assert response.choices[0].message.role == "assistant"
text = response.choices[0].message.content
assert isinstance(text, str)
print("-" * 30)
print(f"Multi images response:\n{text}")
print("-" * 30)
assert "man" in text or "cab" in text or "SUV" in text or "taxi" in text, text
assert "logo" in text or '"S"' in text or "SG" in text, text
assert response.id
assert response.created
assert response.usage.prompt_tokens > 0
assert response.usage.completion_tokens > 0
assert response.usage.total_tokens > 0
def run_decode_with_image(self, image_id):
client = openai.Client(api_key=self.api_key, base_url=self.base_url)
content = []
if image_id == 0:
content.append(
{
"type": "image_url",
"image_url": {"url": IMAGE_MAN_IRONING_URL},
}
)
elif image_id == 1:
content.append(
{
"type": "image_url",
"image_url": {"url": IMAGE_SGL_LOGO_URL},
}
)
else:
pass
content.append(
{
"type": "text",
"text": "Describe this image in a very short sentence.",
}
)
response = client.chat.completions.create(
model="default",
messages=[
{"role": "user", "content": content},
],
temperature=0,
)
assert response.choices[0].message.role == "assistant"
text = response.choices[0].message.content
assert isinstance(text, str)
def _run_test_mixed_batch(self):
image_ids = [0, 1, 2] * 4
with ThreadPoolExecutor(4) as executor:
list(executor.map(self.run_decode_with_image, image_ids))
def test_vlm(self):
models_to_test = VISION_MODELS
if is_in_ci():
models_to_test = [random.choice(VISION_MODELS)]
for model in models_to_test:
with self.subTest(model=model):
other_args = [
"--mem-fraction-static",
"0.6",
"--load-format",
"bitsandbytes",
"--enable-multimodal",
]
try:
process = popen_launch_server_wrapper(
DEFAULT_URL_FOR_TEST, model, other_args
)
self._run_test_mixed_batch()
self._run_multi_images_chat_completion()
self._run_multi_turn_chat_completion()
self._run_single_image_chat_completion()
finally:
kill_process_tree(process.pid)
class TestLanguageModel(CustomTestCase):
@classmethod
def setUpClass(cls):
mp.set_start_method("spawn", force=True)
cls.base_url = DEFAULT_URL_FOR_TEST
# cls.base_url += "/v1"
cls.api_key = "sk-123456"
def test_mmlu(self):
models_to_test = LANGUAGE_MODELS
if is_in_ci():
models_to_test = [random.choice(LANGUAGE_MODELS)]
for model in models_to_test:
with self.subTest(model=model):
other_args = [
"--mem-fraction-static",
"0.6",
"--load-format",
"bitsandbytes",
]
try:
process = popen_launch_server_wrapper(
DEFAULT_URL_FOR_TEST, model, other_args
)
args = SimpleNamespace(
base_url=self.base_url,
model=model,
eval_name="mmlu",
num_examples=32,
num_threads=16,
)
metrics = run_eval(args)
print(f"{metrics=}")
self.assertGreater(metrics["score"], 0.3)
finally:
kill_process_tree(process.pid)
-59
View File
@@ -1,59 +0,0 @@
from types import SimpleNamespace
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_amd_ci
from sglang.test.run_eval import run_eval
from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST,
CustomTestCase,
popen_launch_server,
)
register_amd_ci(est_time=313, suite="stage-b-test-1-gpu-small-amd")
class TestMixtralAccuracy(CustomTestCase):
@classmethod
def setUpClass(cls):
cls.model = "mistralai/Mixtral-8x7B-Instruct-v0.1"
cls.base_url = DEFAULT_URL_FOR_TEST
other_args = [
"--tp",
"2",
"--mem-fraction-static",
"0.9",
"--context-length",
"38768",
"--quantization",
"quark_int4fp8_moe",
# The default aiter attention backend raises segmentation faults and other errors - as quark_int4fp8_moe is not related to attention, let's just use triton here.
"--attention-backend",
"triton",
]
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=45 * 60,
other_args=other_args,
)
@classmethod
def tearDownClass(cls):
kill_process_tree(cls.process.pid)
def test_gsm8k(self):
args = SimpleNamespace(
base_url=self.base_url,
model=self.model,
eval_name="gsm8k",
api="completion",
max_tokens=512,
num_examples=1400,
num_threads=128,
num_shots=8,
)
metrics = run_eval(args)
print(f"{metrics=}")
self.assertGreater(metrics["score"], 0.56)
-174
View File
@@ -1,174 +0,0 @@
# Rotary Embedding - MRoPE tests (1-GPU)
from typing import NamedTuple
import pytest
import torch
from packaging.version import Version
from transformers import AutoConfig
from transformers import __version__ as TRANSFORMERS_VERSION
from sglang.srt.layers.rotary_embedding import get_rope
from sglang.srt.server_args import ServerArgs, set_global_server_args_for_scheduler
from sglang.srt.utils import (
cpu_has_amx_support,
is_cpu,
is_cuda,
is_hip,
is_npu,
is_xpu,
)
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(est_time=7, suite="stage-b-test-1-gpu-large")
register_amd_ci(est_time=15, suite="stage-b-test-1-gpu-small-amd")
_is_cuda = is_cuda()
_is_hip = is_hip()
_is_cpu = is_cpu()
_is_cpu_amx_available = cpu_has_amx_support()
_is_npu = is_npu()
_is_xpu = is_xpu()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def generate_test_data(
num_tokens: int,
num_q_heads: int,
num_kv_heads: int,
head_size: int,
max_position_embeddings: int,
dtype: torch.dtype,
device: torch.device,
):
"""Generate test data for given configuration."""
torch.manual_seed(42)
# Create 2D positions (3, num_tokens) for multimodal case
positions = torch.randint(
0, max_position_embeddings // 4, (3, num_tokens), device=device
)
# Create query and key tensors
query = torch.randn(num_tokens, num_q_heads * head_size, dtype=dtype, device=device)
key = torch.randn(num_tokens, num_kv_heads * head_size, dtype=dtype, device=device)
return positions, query, key
class MRoPETestInfo(NamedTuple):
model_name: str
atol: float = 1e-2
rtol: float = 1.6e-2
marks: list[pytest.MarkDecorator] = []
TRANSFORMERS_BASE_VERSION = Version(TRANSFORMERS_VERSION).base_version
MODELS_TO_TEST = [
MRoPETestInfo(model_name="Qwen/Qwen2-VL-7B-Instruct"),
MRoPETestInfo(model_name="Qwen/Qwen2-VL-72B-Instruct"),
MRoPETestInfo(model_name="Qwen/Qwen2.5-VL-72B-Instruct"),
]
num_tokens_list = [11, 8192]
def create_yarn_rope_scaling(original_config, scaling_factor=2.0):
yarn_config = {
"rope_type": "yarn",
"factor": scaling_factor,
"original_max_position_embeddings": original_config.max_position_embeddings,
}
if hasattr(original_config, "rope_scaling") and original_config.rope_scaling:
if "mrope_section" in original_config.rope_scaling:
yarn_config["mrope_section"] = original_config.rope_scaling["mrope_section"]
if "mrope_interleaved" in original_config.rope_scaling:
yarn_config["mrope_interleaved"] = original_config.rope_scaling[
"mrope_interleaved"
]
return yarn_config
@pytest.mark.skipif(not (_is_cuda or _is_hip), reason="Skipping CUDA/ROCm only tests.")
@pytest.mark.parametrize(
"model_info, model_name",
[
pytest.param(test_config, test_config.model_name, marks=test_config.marks)
for test_config in MODELS_TO_TEST
],
)
@pytest.mark.parametrize("tp_size", [1, 2])
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float32])
@pytest.mark.parametrize("num_tokens", num_tokens_list)
@pytest.mark.parametrize(
"rope_scaling_type", ["default", "yarn"], ids=["mrope_default", "mrope_yarn"]
)
def test_mrope(
model_name: str,
model_info: MRoPETestInfo,
tp_size: int,
dtype: torch.dtype,
num_tokens: int,
rope_scaling_type: str,
):
set_global_server_args_for_scheduler(ServerArgs(model_path="dummy"))
atol = model_info.atol
rtol = model_info.rtol
config = AutoConfig.from_pretrained(model_name)
config = config.get_text_config()
# get the model config
total_num_kv_heads = config.num_key_value_heads
total_num_heads = config.num_attention_heads
num_heads = total_num_heads // tp_size
num_kv_heads = max(1, total_num_kv_heads // tp_size)
head_dim = (
config.head_dim
if hasattr(config, "head_dim")
else config.hidden_size // total_num_heads
)
is_neox_style = True
rope_theta = config.rope_theta
max_position = config.max_position_embeddings
partial_rotary_factor = getattr(config, "partial_rotary_factor", 1.0)
rotary_dim = int(head_dim * partial_rotary_factor)
if rope_scaling_type == "yarn":
rope_scaling_config = create_yarn_rope_scaling(config, scaling_factor=2.0)
else:
rope_scaling_config = config.rope_scaling
mrope_helper_class = get_rope(
head_size=head_dim,
rotary_dim=rotary_dim,
max_position=max_position,
base=rope_theta,
is_neox_style=is_neox_style,
rope_scaling=rope_scaling_config,
dtype=dtype,
).to(device=device)
# create q k v input tensors
# create rotary pos emb input tensors
positions, query, key = generate_test_data(
num_tokens, num_heads, num_kv_heads, head_dim, max_position, dtype, device
)
query_native, key_native = mrope_helper_class.forward_native(
positions,
query.clone(),
key.clone(),
)
query_cuda, key_cuda = mrope_helper_class.forward(
positions,
query.clone(),
key.clone(),
)
torch.testing.assert_close(query_native, query_cuda, atol=atol, rtol=rtol)
torch.testing.assert_close(key_native, key_cuda, atol=atol, rtol=rtol)
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,6 @@ import torch
from sglang.srt.managers.schedule_batch import FINISH_ABORT
from sglang.srt.mem_cache.base_prefix_cache import MatchResult
from sglang.srt.mem_cache.common import release_kv_cache
from sglang.srt.session.streaming_session import SessionSlot, StreamingSession
from sglang.test.ci.ci_register import register_cpu_ci
@@ -76,6 +75,7 @@ class _FakeReq:
self.pop_overallocated_calls = 0
self.to_finish = None
self.finished_reason = None
self.finished_len = None
def pop_committed_kv_cache(self):
assert not self.kv_committed_freed
@@ -89,36 +89,6 @@ class _FakeReq:
return self.kv_committed_len, self.kv_allocated_len
def test_streaming_release_kv_cache_defers_tail_free(monkeypatch):
"""Spec tail is NOT trimmed in cache_finished_req; it is deferred to
match_prefix's orphan tail free on the next turn. cache_finished_req
only sets bookkeeping flags and saves the slot as-is."""
page_size = 16
req_to_token = torch.arange(128, dtype=torch.int32).reshape(1, 128)
req_to_token_pool = SimpleNamespace(req_to_token=req_to_token, free_slots=[])
allocator = _FakeAllocator()
tree_cache = StreamingSession(
_FakeInnerCache(req_to_token_pool, allocator, page_size)
)
req = _FakeReq("session-a", req_pool_idx=0, committed=17, allocated=40)
monkeypatch.setattr(
"sglang.srt.mem_cache.common.get_global_server_args",
lambda: SimpleNamespace(page_size=page_size, speculative_algorithm="eagle"),
)
release_kv_cache(req, tree_cache)
slot = tree_cache.slots["session-a"]
assert req.kv_committed_freed is True
assert req.kv_overallocated_freed is True
assert req.req_pool_idx is None
# Slot keeps the full allocation — tail free is deferred to match_prefix.
assert slot.kv_committed_len == 17
assert slot.kv_allocated_len == 40
assert len(allocator.freed) == 0
def test_preabort_detaches_session_and_preserves_slot():
"""Pre-aborted req (to_finish set before match_prefix) is detached from
the session: session=None, abort_req() called. Slot stays intact."""
@@ -271,3 +241,11 @@ def test_trim_overshoot_postcondition():
# Tail [38, 44) freed by _free_kv_aligned.
assert len(allocator.freed) == 1
assert allocator.freed[0].tolist() == list(range(38, 44))
if __name__ == "__main__":
import sys
import pytest
sys.exit(pytest.main([__file__, "-v"]))
@@ -1,156 +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.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=6, suite="stage-b-test-1-gpu-small")
register_amd_ci(est_time=10, suite="stage-b-test-1-gpu-small-amd")
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()
@@ -1,166 +0,0 @@
import os
import statistics
import pytest
import torch
import torch.nn as nn
from sglang.srt.models.glm4v import Glm4vVisionPatchEmbed
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=8, suite="stage-b-test-1-gpu-large")
PATCH_SIZE = 14
TEMPORAL_PATCH_SIZE = 2
IN_CHANNELS = 3
HIDDEN_SIZE = 1536
FLAT_DIM = IN_CHANNELS * TEMPORAL_PATCH_SIZE * PATCH_SIZE * PATCH_SIZE
class ReferenceConv3dPatchEmbed(nn.Module):
def __init__(
self,
patch_size=PATCH_SIZE,
temporal_patch_size=TEMPORAL_PATCH_SIZE,
in_channels=IN_CHANNELS,
hidden_size=HIDDEN_SIZE,
):
super().__init__()
self.patch_size = patch_size
self.temporal_patch_size = temporal_patch_size
self.in_channels = in_channels
self.hidden_size = hidden_size
kernel_size = (temporal_patch_size, patch_size, patch_size)
self.proj = nn.Conv3d(
in_channels,
hidden_size,
kernel_size=kernel_size,
stride=kernel_size,
bias=True,
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x.view(
-1,
self.in_channels,
self.temporal_patch_size,
self.patch_size,
self.patch_size,
)
x = self.proj(x).view(-1, self.hidden_size)
return x
def _build_modules(device: str, dtype: torch.dtype):
conv_mod = ReferenceConv3dPatchEmbed().to(device=device, dtype=dtype).eval()
linear_mod = (
Glm4vVisionPatchEmbed(
patch_size=PATCH_SIZE,
temporal_patch_size=TEMPORAL_PATCH_SIZE,
in_channels=IN_CHANNELS,
hidden_size=HIDDEN_SIZE,
)
.to(device=device, dtype=dtype)
.eval()
)
with torch.no_grad():
linear_mod.proj.weight.copy_(conv_mod.proj.weight)
linear_mod.proj.bias.copy_(conv_mod.proj.bias)
linear_mod.copy_conv3d_weight_to_linear()
return conv_mod, linear_mod
def _benchmark_cuda_module(
module: nn.Module,
x: torch.Tensor,
warmup: int = 50,
inner_iters: int = 200,
repeats: int = 10,
) -> float:
assert x.is_cuda
module.eval()
with torch.inference_mode():
for _ in range(warmup):
module(x)
torch.cuda.synchronize()
samples = []
for _ in range(repeats):
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
start.record()
for _ in range(inner_iters):
module(x)
end.record()
torch.cuda.synchronize()
samples.append(start.elapsed_time(end) / inner_iters)
return statistics.median(samples)
def test_patch_embed_linear_matches_conv3d():
torch.manual_seed(0)
device = "cpu"
dtype = torch.float32
conv_mod, linear_mod = _build_modules(device=device, dtype=dtype)
x = torch.randn(512, FLAT_DIM, device=device, dtype=dtype)
with torch.inference_mode():
y_conv = conv_mod(x)
y_linear = linear_mod(x)
torch.testing.assert_close(
y_conv,
y_linear,
rtol=1e-5,
atol=1e-5,
)
@pytest.mark.skipif(
not torch.cuda.is_available(), reason="CUDA is required for perf benchmark"
)
def test_patch_embed_linear_conv3d():
torch.manual_seed(0)
torch.backends.cudnn.benchmark = True
device = "cuda"
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
conv_mod, linear_mod = _build_modules(device=device, dtype=dtype)
num_patches = int(os.getenv("GLM4V_NUM_PATCHES", "4096"))
warmup = int(os.getenv("GLM4V_WARMUP", "50"))
inner_iters = int(os.getenv("GLM4V_INNER_ITERS", "200"))
repeats = int(os.getenv("GLM4V_REPEATS", "10"))
x = torch.randn(num_patches, FLAT_DIM, device=device, dtype=dtype).contiguous()
conv_ms = _benchmark_cuda_module(
conv_mod, x, warmup=warmup, inner_iters=inner_iters, repeats=repeats
)
linear_ms = _benchmark_cuda_module(
linear_mod, x, warmup=warmup, inner_iters=inner_iters, repeats=repeats
)
speedup = conv_ms / linear_ms
print(
f"\n[patch_embed perf] conv3d={conv_ms:.4f} ms | "
f"linear={linear_ms:.4f} ms | speedup={speedup:.3f}x"
)
min_speedup = float(os.getenv("GLM4V_MIN_SPEEDUP", "1.00"))
assert speedup >= min_speedup, (
f"Expected speedup >= {min_speedup:.3f}x, but got {speedup:.3f}x "
f"(conv3d={conv_ms:.4f} ms, linear={linear_ms:.4f} ms)"
)