docs: flesh out MAINTAINER.md oncall lists and link GitHub profiles (#21575)

This commit is contained in:
Lianmin Zheng
2026-03-27 17:39:16 -07:00
committed by GitHub
parent c37200f5e4
commit 83997080a6
7 changed files with 952 additions and 408 deletions
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -9,8 +9,8 @@
/python/sglang/multimodal_gen/runtime/layers @mickqian @yhyang201 @BBuf @yingluosanqian @ping1jing2
/python/sglang/multimodal_gen/runtime/models/dits @mickqian @yhyang201 @BBuf @yingluosanqian @ping1jing2
/python/sglang/srt/batch_invariant_ops @Fridge003 @hebiao064
/python/sglang/srt/compilation @hebiao064 @Oasis-Git
/python/sglang/srt/constrained @hnyls2002 @DarkSharpness
/python/sglang/srt/compilation @hebiao064
/python/sglang/srt/disaggregation @ByronHsu @hnyls2002 @ShangmingCai
/python/sglang/srt/disaggregation/ascend @ping1jing2 @iforgetmyname
/python/sglang/srt/distributed @yizhang2077 @merrymercy @ch-wan
@@ -49,6 +49,7 @@
/sgl-model-gateway/benches @slin1237
/sgl-model-gateway/bindings/python @CatherineSue @key4ng @slin1237
/sgl-model-gateway/e2e_test @CatherineSue @key4ng
/sgl-model-gateway/examples/wasm @slin1237
/sgl-model-gateway/src/config @slin1237
/sgl-model-gateway/src/core @slin1237
/sgl-model-gateway/src/data_connector @key4ng
@@ -62,6 +63,5 @@
/sgl-model-gateway/src/tokenizer @slin1237 @CatherineSue
/sgl-model-gateway/src/tool_parser @slin1237 @CatherineSue
/sgl-model-gateway/src/wasm @slin1237
/sgl-model-gateway/examples/wasm @slin1237
/test/srt/ascend @ping1jing2 @iforgetmyname
/test/srt/test_modelopt* @Edwardf0t1
+87 -10
View File
@@ -37,34 +37,111 @@ __Note__: The permissions to trigger CI tests are defined separately according t
- **Ideal case:** For each modified file, one Codeowner has approved the PR. The PR has also passed the required CI tests. Then, anyone with write permission can merge the PR.
- **Exception:** In cases where it is difficult to meet all requirements (due to flaky CI or slow responses), a Merge Oncall can bypass branch protection to merge the PR.
If you meet any issues during the merge, you can discuss in [slack channels](https://slack.sglang.io/): #dev, #pull-request, and #ci-cd-build-release.
If you meet any issues during the merge, you can discuss in [slack channels](https://slack.sglang.io/): #pull-request, #ci-cd-build-release, #dev.
## The List of Merge Oncalls and Reviewers
This section lists the oncalls for each module or feature.
The format is @github-username (Slack username).
TODO: fill in the list.
### Scheduler
[@merrymercy](https://github.com/merrymercy) (Lianmin Zheng), [@hnyls2002](https://github.com/hnyls2002) (Liangsheng Yin), [@cctry](https://github.com/cctry) (Shiyang Chen)
related files
- python/sglang/srt/managers
- python/sglang/srt/model_executor
### Diffusion
[@mickqian](https://github.com/mickqian) (Mick), [@BBuf](https://github.com/BBuf) (BBuf)
related files
- python/sglang/multimodal_gen
### PD disaggregation
[@ByronHsu](https://github.com/ByronHsu) (Byron Hsu), [@cctry](https://github.com/cctry) (Shiyang Chen), [@ShangmingCai](https://github.com/ShangmingCai) (Shangming Cai)
related files
- python/sglang/srt/disaggregation
### KV Cache
[@ispobock](https://github.com/ispobock) (Ke Bao), [@xiezhq-hermann](https://github.com/xiezhq-hermann) (Zhiqiang Xie)
related files
- python/sglang/srt/mem_cache
### Parallelism
[@ch-wan](https://github.com/ch-wan) (Cheng Wan), [@fzyzcjy](https://github.com/fzyzcjy) (Tom)
related files
- python/sglang/srt/eplb
- python/sglang/srt/distributed
- python/sglang/srt/layers/dp_attention.py
### Kernel
[@BBuf](https://github.com/BBuf) (BBuf)
related files
- python/sglang/jit_kernel
- sgl-kernel
### Speculative decoding
[@hnyls2002](https://github.com/hnyls2002) (Liangsheng Yin), [@Qiaolin-Yu](https://github.com/Qiaolin-Yu) (Qiaolin Yu)
related files
- python/sglang/srt/speculative
### NV and model-specific optimizations
[@Fridge003](https://github.com/Fridge003) (Baizhou Zhang), [@ishandhanani](https://github.com/ishandhanani) (Ishan Dhanani), [@Qiaolin-Yu](https://github.com/Qiaolin-Yu) (Qiaolin Yu)
related files
- python/sglang/srt/models
- python/sglang/srt/layers/attention
### AMD optimizations
[@HaiShaw](https://github.com/HaiShaw) (Henry HAI)
### NPU optimizations
[@iforgetmyname](https://github.com/iforgetmyname) (Even Zhou)
related files
- python/sglang/srt/hardware_backend/npu
### CI, Release, Package
[@Kangyan-Zhou](https://github.com/Kangyan-Zhou) (Kangyan Zhou), [@Fridge003](https://github.com/Fridge003) (Baizhou Zhang)
related files
- .github/workflows
### Router, API
[@slin1237](https://github.com/slin1237) (Simo Lin)
related files
- sgl-model-gateway
- python/sglang/srt/grpc
- python/sglang/srt/entrypoints
### Other Notes
Now we have many Merge Oncalls mainly because the CI is flaky and the CODEOWNERS is too coarse-grained.
In the future, we hope the CI can be improved and we only need bypass rarely. After that, most Merge Oncalls can be converted back to Write and CODEOWNERS.
This list is based on the current situation. If you or someone you know would like to take on more responsibility and are qualified, please ping @Lianmin Zheng and @Ying Sheng in the Slack channel. They will start a nomination and internal review process.
This list is based on the current situation. If you or someone you know would like to take on more responsibility and are qualified, please ping [Lianmin Zheng](https://github.com/merrymercy) and [Ying Sheng](https://github.com/Ying1123) in the Slack channel. They will start a nomination and internal review process.
## The List of CI Oncalls
The format is @github-username (Slack username).
This section lists the oncalls for each hardware platform. The format is @github-username (Slack username).
### NVIDIA GPUs
@merrymercy (Lianmin Zheng), @Kangyan-Zhou (Kangyan Zhou), @ch-wan (Cheng Wan), @HanHan009527 (hanhan), @ishandhanani (Ishan Dhanani), @key4ng (Keyang Ru), @slin1237 (Simo Lin), @ShangmingCai (Shangming Cai)
[@Kangyan-Zhou](https://github.com/Kangyan-Zhou) (Kangyan Zhou), [@ch-wan](https://github.com/ch-wan) (Cheng Wan), [@HanHan009527](https://github.com/HanHan009527) (hanhan), [@ishandhanani](https://github.com/ishandhanani) (Ishan Dhanani), [@ShangmingCai](https://github.com/ShangmingCai) (Shangming Cai), [@alisonshao](https://github.com/alisonshao) (Alison Shao).
### AMD GPUs
@saienduri (Sai Enduri), @HaiShaw (Henry HAI)
[@saienduri](https://github.com/saienduri) (Sai Enduri), [@HaiShaw](https://github.com/HaiShaw) (Henry HAI)
### Intel CPU and XPU
@mingfeima (Mingfei Ma), @DiweiSun (Diwei Sun)
[@mingfeima](https://github.com/mingfeima) (Mingfei Ma), [@DiweiSun](https://github.com/DiweiSun) (Diwei Sun)
### Ascend NPUs
@iforgetmyname (Even Zhou)
[@iforgetmyname](https://github.com/iforgetmyname) (Even Zhou)
This list is based on the current situation. If you or someone you know would like to donate machines for CI, they can serve as the CI oncalls for their machines. Please ping @Lianmin Zheng and @Ying Sheng in the Slack channel. They will start a nomination and internal review process.
This list is based on the current situation. If you or someone you know would like to donate machines for CI, they can serve as the CI oncalls for their machines. Please ping [Lianmin Zheng](https://github.com/merrymercy) and [Ying Sheng](https://github.com/Ying1123) in the Slack channel. They will start a nomination and internal review process.
## Suspending Permissions
If the merge oncall bypasses checks to merge a PR that breaks the `main` branch, or if they repeatedly break the CI due to various reasons, their privileges will be suspended for at least three days, depending on the severity of the incident.
If a Merge Oncall bypasses checks to merge a PR that breaks the `main` branch, or if they repeatedly break the CI due to various reasons, their privileges will be suspended for at least two days, depending on the severity of the incident.
+411
View File
@@ -0,0 +1,411 @@
"""
Audit GitHub repository collaborators with elevated access.
This script will:
1. Fetch all collaborators with write permission to this repo.
2. Show their github username, Nickname and the role (e.g., admin, maintain,
custom org role, write, triage).
3. Show their last activity related to this repo (last commit, last issue,
last pull request). Put the data in YYYY-MM-DD format. Add a column "last activity date" to the CSV, before the above three breakdown columns.
4. Show activity on other repos: repos touched via public events in the last 90 days (Push, PR, Issues, etc.). Sort the repos by the number of activities.
5. Write results to a CSV sorted by the roles (admin, maintain, custom org role, write, triage) and the last activity date (most recent first).
Usage:
export GH_TOKEN="your_github_token"
python3 audit_permission.py [--output path] [--repo owner/name]
Requires: requests, and a token with permission to list collaborators (push+
access to the repo).
"""
from __future__ import annotations
import argparse
import csv
import os
import sys
import time
from collections import Counter
from datetime import datetime, timedelta, timezone
from typing import Any
try:
import requests
except ImportError:
requests = None # type: ignore
DEFAULT_OWNER = "sgl-project"
DEFAULT_NAME = "sglang"
HEADERS: dict[str, str] = {}
def _request(
method: str,
url: str,
*,
params: dict[str, Any] | None = None,
max_retries: int = 3,
) -> requests.Response:
if requests is None:
raise RuntimeError("Install the requests package: pip install requests")
for attempt in range(max_retries):
r = requests.request(method, url, headers=HEADERS, params=params, timeout=60)
if r.status_code == 403 and "rate limit" in (r.text or "").lower():
reset = r.headers.get("X-RateLimit-Reset")
wait = 60
if reset:
try:
wait = max(1, int(reset) - int(time.time()) + 2)
except ValueError:
pass
print(f"Rate limited; sleeping {wait}s...", file=sys.stderr)
time.sleep(min(wait, 3600))
continue
return r
return r
def paginate_list(url: str, params: dict[str, Any] | None = None) -> list[Any]:
out: list[Any] = []
next_url: str | None = url
next_params = params
while next_url:
r = _request("GET", next_url, params=next_params)
next_params = None
if r.status_code != 200:
print(
f"Error {r.status_code} GET {next_url}: {r.text[:500]}",
file=sys.stderr,
)
break
data = r.json()
if isinstance(data, list):
out.extend(data)
else:
break
next_url = None
link = r.headers.get("Link", "")
for part in link.split(", "):
if 'rel="next"' in part:
start = part.find("<") + 1
end = part.find(">")
if start > 0 and end > start:
next_url = part[start:end]
break
return out
def collaborator_role(collab: dict[str, Any]) -> str:
role_name = collab.get("role_name")
if isinstance(role_name, str) and role_name.strip():
return role_name.strip()
perms = collab.get("permissions") or {}
if perms.get("admin"):
return "admin"
if perms.get("maintain"):
return "maintain"
if perms.get("push"):
return "write"
if perms.get("triage"):
return "triage"
return "read"
def has_write_plus(collab: dict[str, Any]) -> bool:
perms = collab.get("permissions") or {}
return bool(
perms.get("admin")
or perms.get("maintain")
or perms.get("push")
or perms.get("triage")
)
def role_sort_tier(collab: dict[str, Any]) -> int:
"""Sort order: admin (0), maintain (1), custom org role (2), write (3), triage (4)."""
rn = collab.get("role_name")
if isinstance(rn, str) and rn.strip():
k = rn.strip().lower()
if k == "admin":
return 0
if k == "maintain":
return 1
if k == "write":
return 3
if k == "triage":
return 4
if k == "read":
return 5
return 2
perms = collab.get("permissions") or {}
if perms.get("admin"):
return 0
if perms.get("maintain"):
return 1
if perms.get("push"):
return 3
if perms.get("triage"):
return 4
return 5
def fetch_display_name(login: str) -> str:
url = f"https://api.github.com/users/{login}"
r = _request("GET", url)
if r.status_code != 200:
return ""
data = r.json()
if not isinstance(data, dict):
return ""
n = data.get("name")
return n.strip() if isinstance(n, str) else ""
def parse_github_ts(s: str) -> datetime | None:
if not s:
return None
s = s.replace("Z", "+00:00")
try:
return datetime.fromisoformat(s)
except ValueError:
return None
def iso_timestamp_to_ymd(iso: str | None) -> str:
if not iso:
return ""
p = parse_github_ts(iso)
if not p:
return ""
return p.date().isoformat()
def max_date_ymd(*iso_dates: str | None) -> str:
best: datetime | None = None
for d in iso_dates:
p = parse_github_ts(d or "")
if p and (best is None or p > best):
best = p
return best.date().isoformat() if best else ""
def parse_ymd(s: str) -> datetime | None:
if not s:
return None
try:
return datetime.strptime(s, "%Y-%m-%d").replace(tzinfo=timezone.utc)
except ValueError:
return None
def last_commit_date(owner: str, repo: str, login: str) -> str | None:
url = f"https://api.github.com/repos/{owner}/{repo}/commits"
r = _request("GET", url, params={"author": login, "per_page": 1})
if r.status_code != 200:
return None
data = r.json()
if not isinstance(data, list) or not data:
return None
commit = data[0].get("commit") or {}
c = commit.get("committer") or commit.get("author") or {}
d = c.get("date")
return d if isinstance(d, str) else None
def search_repo_item(
owner: str, repo: str, login: str, kind: str
) -> dict[str, Any] | None:
q = f"repo:{owner}/{repo} is:{kind} author:{login}"
url = "https://api.github.com/search/issues"
r = _request(
"GET",
url,
params={"q": q, "sort": "updated", "order": "desc", "per_page": 1},
)
if r.status_code != 200:
return None
payload = r.json()
items = payload.get("items")
if not items:
return None
return items[0] if isinstance(items[0], dict) else None
def last_issue_pr_dates(
owner: str, repo: str, login: str
) -> tuple[str | None, str | None]:
issue = search_repo_item(owner, repo, login, "issue")
pr = search_repo_item(owner, repo, login, "pr")
issue_dt = None
pr_dt = None
if issue:
issue_dt = issue.get("updated_at") or issue.get("created_at")
if not isinstance(issue_dt, str):
issue_dt = None
if pr:
pr_dt = pr.get("updated_at") or pr.get("created_at")
if not isinstance(pr_dt, str):
pr_dt = None
return issue_dt, pr_dt
def other_repos_activity_column(
login: str, owner: str, repo: str, days: int = 90
) -> str:
"""Repos other than this one touched in the window, sorted by event count (desc)."""
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
full = f"{owner}/{repo}"
counts: Counter[str] = Counter()
url: str | None = f"https://api.github.com/users/{login}/events/public"
params: dict[str, Any] = {"per_page": 100}
while url:
r = _request("GET", url, params=params)
params = {}
if r.status_code != 200:
break
events = r.json()
if not isinstance(events, list):
break
oldest_in_page: datetime | None = None
for ev in events:
if not isinstance(ev, dict):
continue
created = parse_github_ts(ev.get("created_at") or "")
if created:
if oldest_in_page is None or created < oldest_in_page:
oldest_in_page = created
if created and created < cutoff:
continue
rinfo = ev.get("repo")
name = None
if isinstance(rinfo, dict):
name = rinfo.get("name")
if isinstance(name, str) and name and name != full:
counts[name] += 1
next_url = None
link = r.headers.get("Link", "")
for part in link.split(", "):
if 'rel="next"' in part:
s, e = part.find("<") + 1, part.find(">")
if s > 0 and e > s:
next_url = part[s:e]
break
if oldest_in_page and oldest_in_page < cutoff:
break
url = next_url
if not events:
break
ordered = sorted(counts.items(), key=lambda x: (-x[1], x[0]))
return ";".join(f"{n}:{c}" for n, c in ordered)
def main() -> None:
parser = argparse.ArgumentParser(description="Audit repo collaborator permissions.")
parser.add_argument(
"--repo",
default=f"{DEFAULT_OWNER}/{DEFAULT_NAME}",
help=f"owner/name (default: {DEFAULT_OWNER}/{DEFAULT_NAME})",
)
parser.add_argument(
"--output",
"-o",
default=os.path.join(os.path.dirname(__file__), "permission_audit.csv"),
help="Output CSV path",
)
parser.add_argument(
"--events-days",
type=int,
default=90,
help="Window for other-repo activity via public events",
)
args = parser.parse_args()
if "/" not in args.repo:
print("Error: --repo must be owner/name", file=sys.stderr)
sys.exit(1)
owner, name = args.repo.split("/", 1)
gh_token = os.getenv("GH_TOKEN")
if not gh_token:
print("Error: GH_TOKEN environment variable is not set.", file=sys.stderr)
sys.exit(1)
global HEADERS
HEADERS = {
"Authorization": f"Bearer {gh_token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
collab_url = f"https://api.github.com/repos/{owner}/{name}/collaborators"
print(f"Fetching collaborators for {owner}/{name}...", file=sys.stderr)
collaborators = paginate_list(
collab_url, params={"per_page": 100, "affiliation": "all"}
)
rows: list[dict[str, Any]] = []
elevated = [c for c in collaborators if isinstance(c, dict) and has_write_plus(c)]
print(
f"Found {len(elevated)} collaborators with admin/maintain/write/triage.",
file=sys.stderr,
)
for i, col in enumerate(elevated, start=1):
login = col.get("login")
if not isinstance(login, str):
continue
print(f" [{i}/{len(elevated)}] {login}", file=sys.stderr)
role = collaborator_role(col)
nickname = fetch_display_name(login)
cd = last_commit_date(owner, name, login)
issue_dt, pr_dt = last_issue_pr_dates(owner, name, login)
last_act_ymd = max_date_ymd(cd, issue_dt, pr_dt)
others = other_repos_activity_column(login, owner, name, days=args.events_days)
rows.append(
{
"_role_tier": role_sort_tier(col),
"github_username": login,
"nickname": nickname,
"role": role,
"last_activity_date": last_act_ymd,
"last_commit_date": iso_timestamp_to_ymd(cd),
"last_issue_date": iso_timestamp_to_ymd(issue_dt),
"last_pr_date": iso_timestamp_to_ymd(pr_dt),
"other_repos_90d": others,
}
)
def sort_key(r: dict[str, Any]) -> tuple[int, float]:
tier = r["_role_tier"]
act = parse_ymd(r.get("last_activity_date") or "")
ts = act.timestamp() if act else 0.0
return (tier, -ts)
rows.sort(key=sort_key)
fieldnames = [
"github_username",
"nickname",
"role",
"last_activity_date",
"last_commit_date",
"last_issue_date",
"last_pr_date",
"other_repos_90d",
]
for r in rows:
del r["_role_tier"]
with open(args.output, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=fieldnames)
w.writeheader()
w.writerows(rows)
print(f"Wrote {len(rows)} rows to {args.output}", file=sys.stderr)
if __name__ == "__main__":
main()
+5 -5
View File
@@ -12,7 +12,7 @@
<!-- If this pull request affects model outputs (e.g., changes to the kernel or model forward code), provide accuracy test results. -->
## Benchmarking and Profiling
## Speed Tests and Profiling
<!-- If this pull request impacts inference speed, provide benchmarking and profiling results. -->
@@ -24,10 +24,10 @@
- [ ] Provide accuracy and speed benchmark results according to [Test the accuracy](https://docs.sglang.io/developer_guide/contribution_guide.html#test-the-accuracy) and [Benchmark the speed](https://docs.sglang.io/developer_guide/contribution_guide.html#benchmark-the-speed).
- [ ] Follow the SGLang code style [guidance](https://docs.sglang.io/developer_guide/contribution_guide.html#code-style-guidance).
## Review Process
## Review and Merge Process
1. Ping Merge Oncalls to start the PR flow. See the [PR Merge Process](https://github.com/sgl-project/sglang/blob/main/.github/MAINTAINER.md#pull-request-merge-process).
1. Ping Merge Oncalls to start the process. See the [PR Merge Process](https://github.com/sgl-project/sglang/blob/main/.github/MAINTAINER.md#pull-request-merge-process).
2. Get approvals from [CODEOWNERS](https://github.com/sgl-project/sglang/blob/main/.github/CODEOWNERS) and other reviewers.
3. Trigger CI tests with [comments](https://docs.sglang.io/developer_guide/contribution_guide.html#how-to-trigger-ci-tests) or contact authorized users to do so.
- `/tag-run-ci-label`, `/rerun-failed-ci`, `/tag-and-rerun-ci`
4. After green CI and required approvals, ask Merge Oncalls to merge.
- Common commands include `/tag-and-rerun-ci`, `/tag-run-ci-label`, `/rerun-failed-ci`
4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.
+6 -4
View File
@@ -22,7 +22,7 @@ The format of `CI_PERMISSIONS.json` is as follows:
Permissions are assigned according to the following rules:
1. Add the top 50 contributors from the last 90 days with full permissions, no cooldown, and the reason "top contributor".
1. Add the top 50 contributors from the last 120 days with full permissions, no cooldown, and the reason "top contributor".
2. Load all users from the existing `CI_PERMISSIONS.json` file and update their entries as follows:
- If a user is already covered by rule 1, skip that user.
- If the old reason of a user is "top contributor" but they are not in the current top contributors list, change their configuration to:
@@ -117,7 +117,7 @@ def get_write_access_users():
return writers
def get_top_contributors(days=90, limit=50):
def get_top_contributors(days, limit):
"""Fetches top contributors based on commit count in the last N days."""
print(f"Fetching commits from the last {days} days...")
since_date = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
@@ -132,7 +132,7 @@ def get_top_contributors(days=90, limit=50):
author_counts[commit["author"]["login"]] += 1
top_users = [user for user, _ in author_counts.most_common(limit)]
print(f"Found {len(top_users)} active contributors in the last {days} days.")
print(f"Found {len(top_users)} top contributors in the last {days} days.")
return set(top_users)
@@ -193,7 +193,7 @@ def main():
print(f"Warning: Could not fetch collaborators (check token scope). Error: {e}")
write_access_users = set()
top_contributors = get_top_contributors(days=90, limit=50)
top_contributors = get_top_contributors(days=120, limit=50)
old_permissions = load_existing_permissions()
new_permissions = {}
@@ -203,6 +203,7 @@ def main():
new_permissions[user] = {
"can_tag_run_ci_label": True,
"can_rerun_failed_ci": True,
"can_rerun_stage": True,
"cooldown_interval_minutes": 0,
"reason": "top contributor",
}
@@ -220,6 +221,7 @@ def main():
new_permissions[user] = {
"can_tag_run_ci_label": True,
"can_rerun_failed_ci": True,
"can_rerun_stage": True,
"cooldown_interval_minutes": 60,
"reason": "custom override",
}
+1 -17
View File
@@ -31,28 +31,14 @@ pre-commit run --all-files
- Link checking with lychee is **enforced in CI**. By default, it is not blocking local commits.
- To run local link checks manually, use: `pre-commit run --hook-stage manual lychee --all-files`.
### Link check guidance (lychee)
- If your PR changes `docs/` or `README.md`, we recommend running local link checks before pushing.
- Local lychee is optional (CI is the source of truth), but if you want a system installation, see the official project: [lycheeverse/lychee](https://github.com/lycheeverse/lychee).
- Recommended local commands:
```bash
# Fast local/offline check (pre-commit config)
pre-commit run --hook-stage manual lychee --all-files
# CI-like online check (external links over network)
lychee --config .github/linters/lychee-ci.toml README.md "docs/**/*.md" "docs/**/*.rst" "docs/**/*.ipynb"
```
## Run and add unit tests
If you add a new feature or fix a bug, please add corresponding unit tests to ensure coverage and prevent regression.
SGLang uses Python's built-in [unittest](https://docs.python.org/3/library/unittest.html) framework with [pytest](https://docs.pytest.org/) as the test runner.
### Unit tests (no server required)
Unit tests live under [`test/registered/unit/`](https://github.com/sgl-project/sglang/tree/main/test/registered/unit), organized to mirror the `python/sglang/srt/` source tree. These tests validate component logic **without** launching a server or loading real model weights.
SGLang uses Python's built-in [unittest](https://docs.python.org/3/library/unittest.html) framework with [pytest](https://docs.pytest.org/) as the test runner.
**When to add a unit test:** If you modify a file under `python/sglang/srt/`, check whether a corresponding test exists in `test/registered/unit/` and add coverage for your changes. For example:
@@ -140,7 +126,6 @@ If you dont have permission and youre not the PR author, please ask mainta
### CI rate limits
Due to CI scheduling and limited resources, higher-priority PRs may preempt running jobs. In such cases, you may need to rerun the tests.
We apply CI rate limits to prevent abuse and ensure fair usage of our CI resources.
Each CI workflow has a default limit defined in its workflow configuration file. For example, in [pr-gate.yml](https://github.com/sgl-project/sglang/blob/main/.github/workflows/pr-gate.yml), the default cooldown period is 120 minutes, and each workflow can override it via the `cool-down-minutes` input parameter:
@@ -154,7 +139,6 @@ cool-down-minutes:
Users listed in [CI_PERMISSIONS.json](https://github.com/sgl-project/sglang/blob/main/.github/CI_PERMISSIONS.json) may have a per-user cooldown interval. In practice, we use the minimum of the workflows default window and the user-specific interval.
## Code style guidance
- Avoid code duplication. If the same code snippet (more than five lines) appears multiple times, extract it into a shared function.
- Minimize device synchronization. Reduce expensive CPU-GPU synchronization operations, such as `tensor.item()` or `tensor.cpu()`, whenever possible. Use vectorized code.