跳到正文
cccskills
免费在 GitHub 公开

write-sglang-test

Write and register SGLang unit and end-to-end tests, from choosing models and file locations to reusing server fixtures. Follow project rules for meaningful assertions, CI registration, dependency mocking, and safe server cleanup after tests.

中文简介正在准备中。当前显示英语简介。

原文查看原文说明

Guide for writing SGLang CI/UT tests. Covers CustomTestCase, CI registration, server fixtures, model selection, mock testing, and test placement. Always read test/README.md for the full CI layout, how to run tests, and extra tips. Use when creating new tests, adding CI test cases, writing unit tests, or when the user asks to add tests for SGLang features.

查看安装方法

适用场景

  • Adding CPU-only unit tests for component logic
  • Testing features through a running SGLang server
  • Registering tests in an appropriate CI suite
  • Reviewing mock boundaries and server cleanup

包含的文件(1)

  • SKILL.md20.6 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Writing SGLang CI / UT Tests

Apply kernel-organization for the public operator namespace, logical grouping, lazy registry metadata, and test placement. The implementation tutorial below does not replace that API contract.

This skill covers how to write and register tests. For CI pipeline internals (stage ordering, fail-fast, gating, partitioning, debugging CI failures), see the CI workflow guide. Whether a case is worth adding at all is decided by unit-test-admission — read it before writing the case, not after.

Core Rules

  1. Always use CustomTestCase — never raw unittest.TestCase. It ensures tearDownClass runs even when setUpClass fails, preventing resource leaks in CI.
  2. tearDownClass must shut the server down gracefully — call terminate_and_kill_process_tree(cls.process), never a bare kill_process_tree. SIGKILL alone skips the server's userspace cleanup and leaves its GPU memory charged to the dead process; the next class then OOMs while loading weights. Keep it defensive too: hasattr/null checks before accessing resources (e.g. cls.process) that setUpClass may not have finished allocating.
  3. Place non-kernel tests in test/registered/<kind>/<subsystem>/ — <kind> is unit, e2e, accuracy, perf, or stress; kernel tests use test/registered/kernels/{ops,benchmark}/<group>/; hardware belongs in registrations, not directory names
  4. Reuse server fixtures — inherit from DefaultServerBase or write setUpClass/tearDownClass with popen_launch_server
  5. Mock boundaries, not SGLang behavior — mock slow or external dependencies only when the assertion still checks an observable result, state transition, or error. A test whose evidence is only assert_called* mirrors its mock and is not admissible. Launch a real server only when inference results or lifecycle behavior are the contract under test.

Existing files are not the reference. About 290 test files still call the bare kill_process_tree(cls.process.pid), against 43 on the current helper. They predate rule 2 and are being migrated, so grepping the repo for a teardown pattern finds the wrong one roughly seven times out of eight. The same goes for register_cuda_ci(suite=...): four files still pass it, all of them under test/registered/stress/.

# Bad:  kill_process_tree(cls.process.pid)           # SIGKILL only; GPU memory lingers
# Good: terminate_and_kill_process_tree(cls.process)

# Bad:  register_cuda_ci(est_time=80, suite="base-b-test-1-gpu-small")
# Good: register_cuda_ci(est_time=80, stage="base-b", runner_config="1-gpu-small")

JIT kernel notes:

  • If the task is adding or updating code under python/sglang/kernels/jit/, prefer the add-jit-kernel skill first.
  • JIT kernel correctness tests use test/registered/kernels/ops/<group>/test_*.py.
  • JIT kernel benchmarks use test/registered/kernels/benchmark/<group>/bench_*.py.
  • Those files are executed by test/run_suite.py through dedicated kernel suites (base-b-kernel-*); a register_*_ci(...) call placed under python/sglang/ is rejected by the check-no-registered-tests-in-package pre-commit hook.

Model & Backend Selection

ScenarioModelCI RegistrationSuite
Unit tests (no server / engine launch)Noneregister_cpu_cibase-a-test-cpu
Common / backend-independent (middleware, abort, routing, config, arg parsing)DEFAULT_SMALL_MODEL_NAME_FOR_TEST (1B)register_cuda_ci onlybase-b-test-1-gpu-small
Model-agnostic functionality (sampling, session, OpenAI API features)DEFAULT_SMALL_MODEL_NAME_FOR_TEST (1B)register_cuda_ci (+ AMD if relevant)base-b-test-1-gpu-small
General performance (single node, no spec/DP/parallelism)DEFAULT_MODEL_NAME_FOR_TEST (8B)register_cuda_cibase-b-test-1-gpu-large
Bigger features (spec, DP, TP, disaggregation)Case by caseCase by caseSee Choosing a Suite below

Key principle for E2E tests: Do NOT add register_amd_ci unless the test specifically exercises AMD/ROCm code paths. Common E2E tests just need any GPU to run — duplicating across backends wastes CI time with no extra coverage.

All model constants

Defined in python/sglang/test/test_utils.py:

ConstantModelWhen to use
DEFAULT_SMALL_MODEL_NAME_FOR_TESTLlama-3.2-1B-InstructCommon features, model-agnostic tests
DEFAULT_SMALL_MODEL_NAME_FOR_TEST_BASELlama-3.2-1BBase (non-instruct) model tests
DEFAULT_MODEL_NAME_FOR_TESTLlama-3.1-8B-InstructGeneral performance (single node)
DEFAULT_MOE_MODEL_NAME_FOR_TESTMixtral-8x7B-InstructMoE-specific tests
DEFAULT_SMALL_EMBEDDING_MODEL_NAME_FOR_TEST—Embedding tests
DEFAULT_SMALL_VLM_MODEL_NAME_FOR_TEST—Vision-language tests

Naming Conventions

A per-commit suite name is generated from registration metadata as {stage}-test-{runner_config} — you don't hand-write it:

  • stage — the CI stage (e.g. base-b, base-b-kernel-unit, base-c).
  • runner_config — a runner-pool key from scripts/ci/runner_configs.yml, which maps it to the physical runner label (so 1-gpu-large runs on 1-gpu-h100). AMD/NPU use their own keys (e.g. amd).
  • Suite — register_cuda_ci(stage="base-b", runner_config="1-gpu-small") → base-b-test-1-gpu-small, the name you pass to run_suite.py --suite. The -test- is just the connector; never put it in register_*_ci.

CUDA nightly uses the same shape with stage="nightly" (e.g. stage="nightly", runner_config="1-gpu-large" → nightly-test-1-gpu-large) and no nightly=True — the stage name carries the cadence, and setting the flag makes the test silently never run. Legacy single-string suite= is left only for stress and some AMD/CPU/NPU pools.

All CI Suites

Do not work from a list copied into this file; it goes stale silently. Read the current one:

grep -n "_SUITES = {" test/run_suite.py   # PER_COMMIT_SUITES, NIGHTLY_SUITES, OTHER_SUITES
cat scripts/ci/runner_configs.yml         # runner_config -> physical runner label

scripts/ci/runner_configs.yml calls itself the single source of truth for the runner_config field, and run_suite.py is what actually dispatches, so those two files settle any disagreement with prose anywhere else.

Nightly suites live in NIGHTLY_SUITES and run via nightly-test-nvidia.yml, nightly-test-amd.yml, and nightly-test-npu.yml, not pr-test.yml. CUDA nightly is named nightly-test-{runner_config} — one suite per machine type, holding everything that runs nightly on it, with auto_partition splitting the work. There is no per-purpose split; kernel, eval, perf, and precision all share their machine's suite.

Note: Multimodal diffusion uses python/sglang/multimodal_gen/test/run_suite.py, not test/run_suite.py.

Choosing a Suite

Use the lightest suite that meets your test's needs:

  • No GPU required → base-a-test-cpu
  • Most small GPU tests → base-b-test-1-gpu-small (default choice)
  • Need H100 memory or Hopper features → base-b-test-1-gpu-large
  • JIT kernel correctness → base-b-kernel-unit-test-1-gpu-large
  • JIT kernel correctness for B200 / SM100 paths → base-b-kernel-unit-test-4-gpu-b200
  • JIT kernel benchmarks → base-b-kernel-benchmark-test-1-gpu-large
  • Multi-GPU → only when the test actually needs multiple GPUs

Test File Templates

Unit Tests (no server / engine launch)

See test/registered/unit/README.md for quick-start and rules. Unit tests live in test/registered/unit/, mirroring python/sglang/srt/:

"""Unit tests for srt/<module>"""

import unittest
from unittest.mock import MagicMock, patch

from sglang.srt.<module> import TargetClass
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase

register_cpu_ci(est_time=5, suite="base-a-test-cpu")
# Unit tests are CPU-only. GPU operator tests belong under `kernel/`.

class TestTargetClass(CustomTestCase):
    def test_basic_behavior(self):
        obj = TargetClass(...)
        self.assertEqual(obj.method(), expected)

    @patch("sglang.srt.<module>.some_dependency")
    def test_with_mock(self, mock_dep):
        mock_dep.return_value = MagicMock()
        # test logic with dependency mocked
        ...


if __name__ == "__main__":
    unittest.main()

Use unittest.mock.patch / MagicMock only at dependency boundaries. Assert the resulting value, state, protocol output, or error—not merely that the mock was called. If the module transitively imports GPU-only packages (e.g. sgl_kernel), they can be stubbed so the test runs on CPU CI. Do not modify sys.modules at module level—use patch.dict (as a class decorator or with start/stop) to ensure cleanup and avoid cross-test pollution. See test/registered/unit/README.md for details and examples.

Quality bar — test real logic (validation boundaries, state transitions, error paths, branching, etc.). Skip tests that just verify Python itself works (e.g., "does calling an abstract method raise NotImplementedError?", "does a dataclass store the field I assigned?"). Consolidate repetitive patterns into parameterized tests. No production code changes in test PRs.

E2E test (small model, server needed)

import unittest

import requests

from sglang.test.ci.ci_register import register_cuda_ci
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,
    terminate_and_kill_process_tree,
)

register_cuda_ci(est_time=60, stage="base-b", runner_config="1-gpu-small")


class TestMyFeature(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=["--arg1", "value1"],  # feature-specific args
        )

    @classmethod
    def tearDownClass(cls):
        if hasattr(cls, "process") and cls.process:
            terminate_and_kill_process_tree(cls.process)

    def test_basic_functionality(self):
        response = requests.post(
            self.base_url + "/generate",
            json={"text": "Hello", "sampling_params": {"max_new_tokens": 32}},
        )
        self.assertEqual(response.status_code, 200)


if __name__ == "__main__":
    unittest.main(verbosity=3)

Copy the tearDownClass above verbatim. Most existing E2E files still show the bare kill_process_tree; that form is being migrated out and must not be reproduced.

E2E test (8B model, server needed, performance)

import time
import unittest

import requests

from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import (
    DEFAULT_MODEL_NAME_FOR_TEST,
    DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
    DEFAULT_URL_FOR_TEST,
    CustomTestCase,
    popen_launch_server,
    terminate_and_kill_process_tree,
)

register_cuda_ci(est_time=300, stage="base-b", runner_config="1-gpu-large")


class TestMyFeaturePerf(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,
        )

    @classmethod
    def tearDownClass(cls):
        if hasattr(cls, "process") and cls.process:
            terminate_and_kill_process_tree(cls.process)

    def test_latency(self):
        start = time.perf_counter()
        response = requests.post(
            self.base_url + "/generate",
            json={"text": "Hello", "sampling_params": {"max_new_tokens": 128}},
        )
        elapsed = time.perf_counter() - start
        self.assertEqual(response.status_code, 200)
        self.assertLess(elapsed, 5.0, "Latency exceeded threshold")


if __name__ == "__main__":
    unittest.main(verbosity=3)

Server Fixture Reuse

For tests that only need a standard server, inherit from DefaultServerBase and override class attributes:

from sglang.test.server_fixtures.default_fixture import DefaultServerBase

class TestMyFeature(DefaultServerBase):
    model = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
    other_args = ["--enable-my-feature"]

    def test_something(self):
        ...

Available fixtures in python/sglang/test/server_fixtures/:

FixtureUse case
DefaultServerBaseStandard single-server tests
EagleServerBaseEAGLE speculative decoding
PDDisaggregationServerBaseDisaggregated prefill/decode
MMMUServerBaseMultimodal VLM tests

CI Registration

Every CI-discovered test file must call a registration function at module level:

from sglang.test.ci.ci_register import (
    register_cuda_ci,
    register_amd_ci,
    register_cpu_ci,
    register_npu_ci,
)

# Per-commit test (small 1-gpu, runs on 5090)
register_cuda_ci(est_time=80, stage="base-b", runner_config="1-gpu-small")

# Per-commit test (large 1-gpu, runs on H100)
register_cuda_ci(est_time=120, stage="base-b", runner_config="1-gpu-large")

# Nightly-only test (same shape as per-commit, stage is just "nightly")
register_cuda_ci(est_time=200, stage="nightly", runner_config="1-gpu-large")

# Multi-backend test (only when testing backend-specific code paths)
register_cuda_ci(est_time=80, stage="base-a", runner_config="1-gpu-small")
register_amd_ci(est_time=120, suite="stage-a-test-1-gpu-small-amd")
register_npu_ci(est_time=400, suite="nightly-8-npu-a3", nightly=True)

# Temporarily disabled test
register_cuda_ci(
    est_time=80, stage="base-b", runner_config="1-gpu-small", disabled="flaky - see #12345"
)

Parameters:

  • est_time: estimated runtime in seconds (used for CI partitioning)
  • stage + runner_config: the canonical pair for CUDA; the suite name is generated from them (see Naming Conventions)
  • suite: legacy single-string form. Only stress and some AMD/CPU/NPU pools still take it; register_cpu_ci(suite="base-a-test-cpu") is correct and is not being migrated
  • nightly=True: legacy cadence flag, for non-CUDA nightly suites only. CUDA nightly uses stage="nightly" and must leave this unset
  • disabled="reason": temporarily disable with explanation

Key principle: Only add register_amd_ci / register_npu_ci when the test exercises backend-specific code paths. Common E2E tests just need register_cuda_ci — duplicating across backends wastes CI time.

JIT Kernel Registration

run_suite.py discovers every test/registered/**/*.py, JIT kernel files included. They are ordinary registered tests; only their stage differs:

from sglang.test.ci.ci_register import register_cuda_ci

# Correctness tests in test/registered/kernels/ops/<group>/
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="8-gpu-h200")

# Benchmarks in test/registered/kernels/benchmark/<group>/
register_cuda_ci(est_time=6, stage="base-b-kernel-benchmark", runner_config="1-gpu-large")

# Optional nightly registration — same form, stage is just "nightly"
register_cuda_ci(est_time=120, stage="nightly", runner_config="1-gpu-large")
register_cuda_ci(est_time=120, stage="nightly", runner_config="8-gpu-h200")

Every call generates a suite named {stage}-test-{runner_config}, e.g. base-b-kernel-unit-test-1-gpu-large and nightly-test-1-gpu-large. Keep est_time, stage, runner_config, and suite as literal values — run_suite.py collects them by AST parsing.


Test Placement

test/
├── registered/          # CI tests (auto-discovered by run_suite.py)
│   ├── unit/<subsystem>/      # CPU-only; no server or model weights
│   ├── kernel/<group>/        # accelerator operator correctness/benchmarks
│   ├── e2e/<subsystem>/       # engine/server integration
│   ├── accuracy/<family>/     # scheduled eval floors
│   ├── perf/<family>/         # scheduled latency/throughput contracts
│   └── stress/<subsystem>/    # stress/weekly coverage
├── manual/              # Non-CI: debugging, one-off, manual verification
└── run_suite.py         # CI runner (globs test/registered/**/*.py; nothing outside it)

python/sglang/kernels/jit/   # implementation + test-only helpers, never registered tests

A register_*_ci(...) under python/sglang/ is rejected by the check-no-registered-tests-in-package pre-commit hook.

Decision rule (see also test/registered/README.md):

  • CPU component logic, no server → registered/unit/<subsystem>/
  • JIT kernel correctness → registered/kernels/ops/<group>/
  • JIT kernel benchmarks → registered/kernels/benchmark/<group>/
  • Other accelerator operator correctness → registered/kernels/ops/<group>/
  • Server needed → registered/e2e/<subsystem>/
  • Eval floor / performance contract → registered/{accuracy,perf}/<family>/
  • Local debugging → manual/

Eval Accuracy Mixins

Design philosophy: Most test files don't care about eval logic — they only need a "does this feature break model output quality?" sanity check. The mixin pattern separates what to test (threshold) from how to test (the eval call, assertions, CI summary). Test classes declare thresholds as class attributes; the mixin provides the test_* method. Override when you need extra assertions (e.g. EAGLE accept length).

Available mixins in python/sglang/test/kits/eval_accuracy_kit.py: MMLUMixin, HumanEvalMixin, MGSMEnMixin, GSM8KMixin. Can be combined freely. Read the source for attrs and defaults. MMLUMixin already scores through sgl-eval; GSM8KMixin still defaults to the deprecated run_eval backend for existing tests, so new tests set gsm8k_backend = "sgl_eval". The exception is a base or completion-only model: sgl-eval only sends chat requests, so those stay on the 5-shot completion backend.

class TestMyFeature(CustomTestCase, MMLUMixin):
    mmlu_score_threshold = 0.65
    mmlu_num_examples = 64
    mmlu_num_threads = 32
    # test_mmlu is inherited — no code needed

Key Utilities

from sglang.test.test_utils import (
    CustomTestCase,              # base class with retry logic
    popen_launch_server,         # launch server subprocess
    terminate_and_kill_process_tree,    # SIGTERM, then SIGKILL, then wait for
                                        # the GPU memory to come back
    DEFAULT_URL_FOR_TEST,        # auto-configured base URL
    DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,  # 600s default
    run_bench_serving,           # benchmark helper (launch + bench)
)

Checklist

Before submitting a test:

  • Inherits from CustomTestCase (not unittest.TestCase)
  • Has register_*_ci(...) call at module level
  • Placed in test/registered/<kind>/<subsystem>/
  • JIT kernel work: correctness tests live in test/registered/kernels/ops/<group>/, benchmarks live in test/registered/kernels/benchmark/<group>/, and only test-only helpers stay under python/sglang/kernels/jit/
  • Backend-independent tests: register_cuda_ci only + smallest model
  • Logic that doesn't need a server / engine launch → unit test in registered/unit/ (see Unit Tests section)
  • tearDownClass is defensive — uses hasattr/null checks before accessing resources that may not have been allocated
  • Every case answers "what future diff turns this red?" — see unit-test-admission
  • est_time is reasonable (measure locally)

Run these against the new file and paste the output rather than self-attesting:

f=<your new test file>
grep -n "kill_process_tree" $f          # every hit must be terminate_and_kill_process_tree
grep -n "register_.*_ci(" $f            # CUDA: stage= + runner_config=, never suite=
grep -n "CustomTestCase\|unittest.main" $f   # both must appear

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Develop lightweight CUDA operations for SGLang using JIT, which compiles code on first use. A tensor-scaling example explains implementation conventions, input validation, memory access helpers, and when to choose this approach.

中文简介正在准备中。当前显示英语简介。

  • Add a lightweight CUDA operation to SGLang
  • Choose between JIT and ahead-of-time compilation
  • Validate tensor inputs in a kernel launcher
sgl-project/sglang3.7万2026年10月19日 更新

Add a CUDA/C++ GPU operation to sgl-kernel using AOT, meaning compilation before use. Follow the implementation through PyTorch registration, Python API exposure, SGLang integration, correctness tests, and benchmarks that account for cache reuse.

中文简介正在准备中。当前显示英语简介。

  • Adding a CUDA/C++ operation to sgl-kernel
  • Registering an operation with PyTorch and exposing a Python API
  • Testing numerical results and invalid inputs
sgl-project/sglang3.7万2026年10月19日 更新

Monitor selected GitHub Actions checks on an SGLang pull request until they pass on its latest commit. Diagnose failures, push small validated fixes, and retry only failed jobs within defined limits.

中文简介正在准备中。当前显示英语简介。

  • Monitor selected PR checks until they pass
  • Separate code failures from infrastructure problems
  • Apply small fixes and track the resulting checks
sgl-project/sglang3.7万2026年10月19日 更新

Review existing tests and CI, the automated checks run during development, for redundant coverage, misplaced tests, and opportunities to reduce runtime or cost. Receive file-specific recommendations based on documented improvement patterns, with uncertain findings separated.

中文简介正在准备中。当前显示英语简介。

  • Investigating a slow test suite
  • Finding redundant or misplaced tests
  • Reviewing CI runtime and cost
sgl-project/sglang3.7万2026年10月19日 更新

Understand and modify SGLang's CI pipelines, including test stage order, start conditions, early stopping after failures, and workload splitting. Use the guide to add jobs and investigate failed, skipped, or cancelled checks.

中文简介正在准备中。当前显示英语简介。

  • Add a test job to an SGLang CI stage
  • Investigate failed or skipped CI checks
  • Update test partitions and waiting job counts
sgl-project/sglang3.7万2026年10月19日 更新

Collect and review SGLang startup logs to identify unnecessary output and trace its source. Get saved logs and specific cleanup proposals while preserving useful diagnostics, with runtime changes left for user approval.

中文简介正在准备中。当前显示英语简介。

  • Investigating repeated messages in startup logs
  • Checking which startup warnings need attention
  • Comparing startup logs across models and GPU counts
sgl-project/sglang3.7万2026年10月19日 更新

查看 sgl-project 的全部技能

报告此技能的问题