[Test] Consolidate kernel tests under plural kernels tree (#39966)

This commit is contained in:
Xiaoyu Zhang
2026-09-18 07:37:48 +08:00
committed by GitHub
parent b98a2d1096
commit 7bc9152447
53 changed files with 113 additions and 53 deletions
+9 -10
View File
@@ -11,7 +11,7 @@ This skill covers **how to write and register tests**. For CI pipeline internals
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 tests in `test/registered/<kind>/<subsystem>/`**`<kind>` is `unit`, `kernel`, `e2e`, `accuracy`, `perf`, or `stress`; hardware belongs in registrations, not directory names
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.
@@ -31,10 +31,8 @@ This skill covers **how to write and register tests**. For CI pipeline internals
JIT kernel notes:
- If the task is adding or updating code under `python/sglang/kernels/jit/`, prefer the `add-jit-kernel` skill first.
- New JIT kernel correctness tests use `test/registered/kernel/jit/**/test_*.py`.
- New JIT kernel benchmarks use `test/registered/kernel/jit/benchmark/**/bench_*.py`.
- `test/registered/jit/` also exists and still runs. It is a leftover from the kernel
reclassification (RFC #29630) that was never finished; do not add files there.
- 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.
---
@@ -336,12 +334,12 @@ They are ordinary registered tests; only their stage differs:
```python
from sglang.test.ci.ci_register import register_cuda_ci
# Correctness tests in test/registered/kernel/jit/
# 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/kernel/jit/benchmark/
# 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"
@@ -375,8 +373,9 @@ A `register_*_ci(...)` under `python/sglang/` is rejected by the
**Decision rule** (see also `test/registered/README.md`):
- CPU component logic, no server → `registered/unit/<subsystem>/`
- JIT kernel correctness / benchmarks `registered/kernel/jit/`
- Other accelerator operator correctness → `registered/kernel/<group>/`
- 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/`
@@ -422,7 +421,7 @@ 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: test files live in `test/registered/kernel/jit/`; only test-only helpers stay under `python/sglang/kernels/jit/`
- [ ] 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
@@ -112,7 +112,6 @@ jobs:
- "python/sglang/kernels/ops/diffusion/**"
- "test/registered/kernels/ops/diffusion/**"
- "test/registered/kernels/benchmark/diffusion/**"
- "test/registered/kernel/diffusion/**"
- "test/registered/unit/diffusion/**"
- "python/sglang/cli/**"
jit_kernel:
@@ -120,7 +119,6 @@ jobs:
- ".github/workflows/pr-test-jit-kernel.yml"
- "python/pyproject.toml"
- "test/registered/kernels/**"
- "test/registered/kernel/diffusion/**"
# sglang.kernels is the migrated kernel namespace (RFC #29630 / #30044); the
# base-b-kernel suites import it directly, so kernel edits must run them.
- "python/sglang/kernels/!(*.md)"
@@ -191,7 +191,7 @@ are comparable.
| `../subblock_sparse_attn.py` | the `AttentionBackend`: schedule, gating, dense fallback |
Tests: `test/unit/test_subblock_sparse_attention.py` and
`test/registered/kernel/attention/test_subblock_sage_fp8_sm90.py`. The GPU
`test/registered/kernels/ops/attention/test_subblock_sage_fp8_sm90.py`. The GPU
test covers the native production dispatch. Running at a full block budget must
reproduce dense attention up to the expected quantization error, pinning routing
indices, ragged tails, scale domains and the softmax scale in one check.
@@ -1,9 +1,9 @@
"""Hand-tuned dispatch configs for the JIT custom all-reduce (v2).
Thresholds and block counts come from sweeps of
``test/registered/jit/benchmark/bench_custom_all_reduce.py`` on the listed
GPUs; ``get_all_reduce_config`` picks the table for the current arch and
world size.
``test/registered/kernels/benchmark/communication/bench_custom_all_reduce.py``
on the listed GPUs; ``get_all_reduce_config`` picks the table for the current
arch and world size.
"""
from functools import cache
@@ -4,11 +4,12 @@ Pre-commit hook: reject CI-registered tests that live inside the importable
`sglang` package (python/sglang/).
Registered tests and benchmarks must live under test/registered/ (e.g.
test/registered/jit/ for JIT kernel tests and test/registered/jit/benchmark/
for JIT kernel benchmarks) so they are not shipped in the wheel and are
collected by run_suite.py's registered glob. A registered file placed inside
the package would be shipped to users AND silently dropped by run_suite.py
(which no longer globs the package) -- it would never run in CI. This guard
test/registered/kernels/ops/ for kernel tests and
test/registered/kernels/benchmark/ for kernel benchmarks) so they are not
shipped in the wheel and are collected by run_suite.py's registered glob. A
registered file placed inside the package would be shipped to users AND
silently dropped by run_suite.py (which no longer globs the package) -- it
would never run in CI. This guard
turns that silent skip into a hard failure.
Reuses ut_parse_one_file() from ci_register.py (AST-based) so the registry
@@ -65,8 +66,8 @@ def main() -> int:
)
print(
" Registered tests and benchmarks must live under test/registered/\n"
" (e.g. test/registered/jit/ for JIT kernel tests and\n"
" test/registered/jit/benchmark/ for JIT kernel benchmarks) so they\n"
" (e.g. test/registered/kernels/ops/ for kernel tests and\n"
" test/registered/kernels/benchmark/ for kernel benchmarks) so they\n"
" are not shipped in the wheel and are collected by run_suite.py.\n"
)
for f in offenders:
+14 -5
View File
@@ -39,10 +39,11 @@ _MODERN_SHAPE = re.compile(r"^(.+)-test-(.+)$")
# no suite any workflow invokes and the test silently never runs.
_LEGACY_CUDA_PREFIXES = ("stress",)
_TEST_KINDS = {"unit", "kernel", "e2e", "accuracy", "perf", "stress"}
_TEST_KINDS = {"unit", "e2e", "accuracy", "perf", "stress"}
_KERNEL_ROOT = "kernels"
# Flat vendor trees. Vendor-only coverage fits no kind above: no XPU/NPU suite
# carries the `-kernel-` infix `kernel` needs, and these launch device work.
# carries the `-kernel-` infix the kernel tree needs, and these launch device work.
_VENDOR_DIRS = {"amd", "mlx", "musa", "npu", "xpu"}
@@ -132,11 +133,22 @@ def taxonomy_errors(path: str, registries: list, tree: ast.AST) -> list[str]:
relative_parts = parts[2:] if parts[:2] == ["test", "registered"] else []
if relative_parts and relative_parts[0] in _VENDOR_DIRS:
return []
if relative_parts and relative_parts[0] == _KERNEL_ROOT:
errors = []
if len(relative_parts) < 4 or relative_parts[1] not in {"ops", "benchmark"}:
errors.append(
f"{path}: kernel tests must live under "
"test/registered/kernels/{ops,benchmark}/<group>/"
)
if any("-kernel-" not in (r.effective_suite or "") for r in registries):
errors.append(f"{path}: kernel tests must use a *-kernel-* suite")
return errors
if len(relative_parts) < 3 or relative_parts[0] not in _TEST_KINDS:
return [
f"{path}: registered tests must live under "
"test/registered/<kind>/<subsystem>/; kind must be one of "
+ ", ".join(sorted(_TEST_KINDS))
+ "; kernel tests use test/registered/kernels/{ops,benchmark}/<group>/"
]
kind = relative_parts[0]
@@ -153,9 +165,6 @@ def taxonomy_errors(path: str, registries: list, tree: ast.AST) -> list[str]:
errors.append(f"{path}: unit test est_time must be <= 60 seconds")
if _contains_call(tree, "popen_launch_server"):
errors.append(f"{path}: unit tests may not launch a server")
elif kind == "kernel":
if any("-kernel-" not in (r.effective_suite or "") for r in registries):
errors.append(f"{path}: kernel tests must use a *-kernel-* suite")
elif kind in {"accuracy", "perf"}:
invalid = [
r
@@ -0,0 +1,51 @@
import ast
import unittest
from types import SimpleNamespace
from scripts.lint.check_registered_tests import taxonomy_errors
def _registry(suite: str):
return SimpleNamespace(effective_suite=suite, est_time=1)
class TestRegisteredTestTaxonomy(unittest.TestCase):
def setUp(self):
self.tree = ast.parse("")
self.kernel_registry = [_registry("base-b-kernel-unit-test-1-gpu-large")]
def test_plural_kernel_ops_layout_is_accepted(self):
errors = taxonomy_errors(
"test/registered/kernels/ops/attention/test_example.py",
self.kernel_registry,
self.tree,
)
self.assertEqual(errors, [])
def test_plural_kernel_benchmark_layout_is_accepted(self):
errors = taxonomy_errors(
"test/registered/kernels/benchmark/attention/bench_example.py",
[_registry("base-b-kernel-benchmark-test-1-gpu-large")],
self.tree,
)
self.assertEqual(errors, [])
def test_singular_kernel_root_is_rejected(self):
errors = taxonomy_errors(
"test/registered/kernel/attention/test_example.py",
self.kernel_registry,
self.tree,
)
self.assertTrue(errors)
def test_kernel_group_is_required(self):
errors = taxonomy_errors(
"test/registered/kernels/ops/test_example.py",
self.kernel_registry,
self.tree,
)
self.assertTrue(errors)
if __name__ == "__main__":
unittest.main()
+13 -11
View File
@@ -11,9 +11,9 @@ The CI pipeline runs in three sequential stages: **A** (pre-flight, ~3 min) →
## Folder Organization
- `registered/`: CI test files, auto-discovered by `run_suite.py`. Most tests live here. JIT kernel tests are an exception (see below).
- `registered/`: CI test files, including kernel tests and benchmarks, auto-discovered by `run_suite.py`.
- `manual/`: Non-CI tests for local debugging or special setups.
- `run_suite.py`: CI runner — scans `registered/` and JIT kernel directories.
- `run_suite.py`: CI runner — scans `registered/` recursively.
The system supports both [unittest](https://docs.python.org/3/library/unittest.html) and [pytest](https://docs.pytest.org/en/stable/). The launcher runs `python filename.py -f` with **failfast enabled by default**.
@@ -44,7 +44,7 @@ python3 test/registered/core/test_srt_endpoint.py
python3 test/registered/core/test_srt_endpoint.py TestSRTEndpoint.test_simple_decode
# Single JIT kernel test
python3 test/registered/jit/test_add_constant.py
python3 test/registered/kernels/ops/elementwise/test_add_constant.py
# Run a suite
python3 test/run_suite.py --hw cpu --suite base-a-test-cpu
@@ -72,13 +72,15 @@ Parameters: `est_time` (seconds), `stage` + `runner_config` (target stage and ru
Keep `est_time`, `stage`, `runner_config` as **literal values**`run_suite.py` collects them by AST parsing.
New and renamed tests use this layout:
New and renamed non-kernel tests use this layout:
```text
test/registered/<kind>/<subsystem>/test_*.py
```
`<kind>` is one of `unit`, `kernel`, `e2e`, `accuracy`, `perf`, or `stress`.
`<kind>` is one of `unit`, `e2e`, `accuracy`, `perf`, or `stress`. Kernel tests
use `test/registered/kernels/{ops,benchmark}/<group>/`, retaining the established
plural `kernels` root.
Hardware is expressed by one or more `register_*_ci` calls, never by creating a
new top-level hardware directory. The admission checker applies the layout and
kind/suite contract incrementally while legacy paths are migrated.
@@ -87,13 +89,13 @@ Diffusion workflows also enter through `test/run_suite.py`; registered bridge
files preserve their case-level pytest partitioning until the remaining
diffusion cases are moved out of the package test-support tree.
New JIT kernel correctness tests and benchmarks live under
`test/registered/kernel/jit/`; legacy `test/registered/jit/` files are migrated
incrementally. Helpers stay alongside the kernel source under
`python/sglang/kernels/jit/` and are imported by absolute path:
Kernel correctness tests and benchmarks use the established plural `kernels`
root and mirror the operator group under `python/sglang/kernels/ops/`. Helpers
stay alongside the kernel source under `python/sglang/kernels/jit/` and are
imported by absolute path:
- Correctness tests: `test/registered/kernel/jit/test_*.py``base-b-kernel-unit-test-1-gpu-large`
- Benchmarks: `test/registered/kernel/jit/benchmark/bench_*.py``base-b-kernel-benchmark-test-1-gpu-large`
- Correctness tests: `test/registered/kernels/ops/<group>/test_*.py``base-b-kernel-unit-test-1-gpu-large`
- Benchmarks: `test/registered/kernels/benchmark/<group>/bench_*.py``base-b-kernel-benchmark-test-1-gpu-large`
## Choosing a Suite
@@ -5,7 +5,7 @@ Measures throughput (µs) for fused_qk_norm_rope across typical
LLM configurations (head_dim × num_heads × num_tokens).
Run:
python test/registered/jit/benchmark/bench_fused_qknorm_rope.py
python test/registered/kernels/benchmark/attention/bench_fused_qknorm_rope.py
"""
import itertools
@@ -11,9 +11,9 @@ Providers:
Usage::
# Benchmark on the default world sizes (2, 4, 8 GPUs):
python test/registered/jit/benchmark/bench_symm_mem_all_gather.py
python test/registered/kernels/benchmark/communication/bench_symm_mem_all_gather.py
# Pick a specific world size (or comma-separated list):
python test/registered/jit/benchmark/bench_symm_mem_all_gather.py --num-gpu 8
python test/registered/kernels/benchmark/communication/bench_symm_mem_all_gather.py --num-gpu 8
"""
from __future__ import annotations
@@ -1,7 +1,7 @@
"""Benchmark for DeepSeek V3 fused QKV-A GEMM: CuTe DSL vs CUDA JIT vs torch.
Run on SM90+ (Hopper or later):
python test/registered/jit/benchmark/bench_dsv3_fused_a_gemm.py
python test/registered/kernels/benchmark/gemm/bench_dsv3_fused_a_gemm.py
"""
import torch
@@ -11,10 +11,10 @@
#
# Test command:
# python3 -m pytest -q \
# test/registered/jit/test_deepseek_v4_compress_state_runtime_shapes.py
# test/registered/kernels/ops/attention/test_deepseek_v4_compress_state_runtime_shapes.py
#
# Runtime-shape benchmark command:
# python3 test/registered/jit/test_deepseek_v4_compress_state_runtime_shapes.py \
# python3 test/registered/kernels/ops/attention/test_deepseek_v4_compress_state_runtime_shapes.py \
# --benchmark \
# --shape-source runtime \
# --warmup 20 \
@@ -22,7 +22,7 @@
# --csv /data00/eval_results/operator_bench/runtime_shape_bench.csv
#
# Synthetic Flash/Pro shape benchmark command:
# python3 test/registered/jit/test_deepseek_v4_compress_state_runtime_shapes.py \
# python3 test/registered/kernels/ops/attention/test_deepseek_v4_compress_state_runtime_shapes.py \
# --benchmark \
# --shape-source preset \
# --shape-presets all \
@@ -7,12 +7,12 @@ NCCL all-gather for a sweep of token counts, hidden widths, and the
Usage::
# Run on the default world sizes (2, 4, 8 GPUs):
python test/registered/jit/test_symm_mem_all_gather.py
python test/registered/kernels/ops/communication/test_symm_mem_all_gather.py
# Pick a specific world size (or comma-separated list):
python test/registered/jit/test_symm_mem_all_gather.py --num-gpu 4
python test/registered/jit/test_symm_mem_all_gather.py --num-gpu 2,4,8
python test/registered/kernels/ops/communication/test_symm_mem_all_gather.py --num-gpu 4
python test/registered/kernels/ops/communication/test_symm_mem_all_gather.py --num-gpu 2,4,8
# Extra pytest args (forwarded to each torchrun worker):
python test/registered/jit/test_symm_mem_all_gather.py -k 16384
python test/registered/kernels/ops/communication/test_symm_mem_all_gather.py -k 16384
"""
from __future__ import annotations
@@ -10,7 +10,7 @@ semaphore window cycling.
Usage::
python test/registered/jit/kimi_k3/test_ar_fusion.py # relaunches under torchrun (8 GPUs)
python test/registered/kernels/ops/kimi_k3/test_ar_fusion.py # relaunches under torchrun (8 GPUs)
"""
from __future__ import annotations
@@ -4,7 +4,7 @@ Poison the packed scratch with NaN, gather with the strided layout used by
`_forward_trtllm_sparse`, and require that (a) valid rows are copied exactly and
(b) every slot in [valid_count, stride) is zero, so the paged decode kernel can never
multiply masked probabilities into stale NaN/Inf bytes. Also checks the compact
(FA2 fallback) layout is unchanged. Intended for test/registered/kernel/qsa/.
(FA2 fallback) layout is unchanged. Intended for test/registered/kernels/ops/qsa/.
"""
import sys
+1 -1
View File
@@ -2,7 +2,7 @@
CPU-only component tests that do **not** launch a server, load model weights,
or require an accelerator. GPU operator correctness belongs under
`test/registered/kernel/<subsystem>/`.
`test/registered/kernels/ops/<group>/`.
## Quick Start