chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+105
View File
@@ -0,0 +1,105 @@
.github @merrymercy @Fridge003 @ispobock @Kangyan-Zhou @HaiShaw @bingxche
/docker @Fridge003 @ispobock @HaiShaw @ishandhanani @yctseng0211 @sogalin
/docker/npu.Dockerfile @ping1jing2 @iforgetmyname @whybeyoung
/docs @wisclmy0611 @zijiexia @sogalin
/docs_new @wisclmy0611 @zijiexia @Richardczl98 @JustinTong0323 @sogalin
/python/pyproject.toml @merrymercy @Fridge003 @ispobock
/python/sglang/jit_kernel @DarkSharpness @BBuf @celve @HydraQYH @yuan-luo
/python/sglang/jit_kernel/diffusion @yingluosanqian @BBuf @mickqian
/python/sglang/multimodal_gen @mickqian @ping1jing2 @HaiShaw @yichiche @AgainstEntropy @BBuf
/python/sglang/multimodal_gen/runtime/cache @DefTruth
/python/sglang/multimodal_gen/test/server/ascend @ping1jing2 @ssshinigami @Makcum888e @e-martirosian
/python/sglang/srt/batch_invariant_ops @Fridge003 @hebiao064
/python/sglang/srt/compilation @hebiao064 @Oasis-Git
/python/sglang/srt/constrained @hnyls2002 @DarkSharpness @JustinTong0323
/python/sglang/srt/disaggregation @ByronHsu @hnyls2002 @ShangmingCai @HaiShaw @sogalin
/python/sglang/srt/disaggregation/ascend @ping1jing2 @iforgetmyname
/python/sglang/srt/disaggregation/encode_receiver.py @ShangmingCai @liusy58 @ZhengWG @gty111
/python/sglang/srt/disaggregation/encode_server.py @ShangmingCai @liusy58 @ZhengWG @gty111
/python/sglang/srt/disaggregation/mori @Duyi-Wang @kkHuang-amd @billishyahao
/python/sglang/srt/distributed @yizhang2077 @merrymercy @ch-wan
/python/sglang/srt/distributed/device_communicators/mooncake_transfer_engine.py @ShangmingCai @stmatengss
/python/sglang/srt/dllm @ClawSeven @btw616
/python/sglang/srt/entrypoints @ispobock @CatherineSue @slin1237 @merrymercy @JustinTong0323
/python/sglang/srt/entrypoints/anthropic @JustinTong0323
/python/sglang/srt/entrypoints/openai @JustinTong0323
/python/sglang/srt/entrypoints/engine_score_mixin.py @sundar24295s @chanh @fortunecookiee
/python/sglang/srt/entrypoints/grpc_server.py @CatherineSue @slin1237
/python/sglang/srt/entrypoints/openai/serving_score.py @sundar24295s @chanh @fortunecookiee
/python/sglang/srt/eplb @fzyzcjy @ch-wan @xutizhou
/python/sglang/srt/function_call @JustinTong0323
/python/sglang/srt/grpc @CatherineSue @slin1237
/python/sglang/srt/hardware_backend/mlx @yeahdongcn
/python/sglang/srt/hardware_backend/musa @yeahdongcn
/python/sglang/srt/hardware_backend/npu @ping1jing2 @iforgetmyname @whybeyoung
/python/sglang/srt/hardware_backend/gpu/quantization @Alisehen
/python/sglang/srt/hardware_backend/npu/quantization @OrangeRedeng @TamirBaydasov @Alisehen
/python/sglang/srt/layers @merrymercy @Ying1123 @Fridge003 @ispobock @HaiShaw @ch-wan @BBuf @Edwardf0t1
/python/sglang/srt/layers/attention @merrymercy @Fridge003 @ispobock @Qiaolin-Yu @hebiao064 @HaiShaw
/python/sglang/srt/layers/attention/fla @yizhang2077 @hebiao064 @yuan-luo
/python/sglang/srt/layers/attention/hybrid_linear_attn_backend.py @yizhang2077 @hebiao064 @hanming-lu @yuan-luo
/python/sglang/srt/layers/attention/mamba @yizhang2077 @hebiao064
/python/sglang/srt/layers/attention/dsa @1am9trash @hubertlu-tw @kkHuang-amd @HaiShaw @Fridge003 @YAMY1234 @rainj-me
/python/sglang/srt/layers/attention/vision.py @mickqian @yuan-luo @yhyang201
/python/sglang/srt/layers/quantization @ch-wan @BBuf @Edwardf0t1 @FlamingoPg @AniZpZ @HaiShaw @b8zhong @OrangeRedeng @TamirBaydasov @Alisehen
/python/sglang/srt/layers/quantization/quark @kkHuang-amd @yichiche @hubertlu-tw @1am9trash @BowenBao
/python/sglang/srt/lora @Ying1123 @Fridge003 @lifuhuang @yushengsu-thu @jybsuper
/python/sglang/srt/managers @merrymercy @Ying1123 @hnyls2002 @xiezhq-hermann
/python/sglang/srt/managers/scheduler_pp_mixin.py @ShangmingCai @XucSh
/python/sglang/srt/managers/tokenizer_manager_score_mixin.py @sundar24295s @chanh @fortunecookiee
/python/sglang/srt/mem_cache @merrymercy @Ying1123 @hnyls2002 @xiezhq-hermann @hanming-lu @yizhang2077 @hzh0425 @ispobock @alphabetc1
/python/sglang/srt/mem_cache/allocator @hnyls2002 @ispobock @alphabetc1
/python/sglang/srt/mem_cache/storage/mooncake_store @huangtingwei9988
/python/sglang/srt/mem_cache/storage/mooncake_store/embedding_cache_controller.py @liusy58
/python/sglang/srt/mem_cache/storage/mooncake_store/mooncake_embedding_store.py @liusy58
/python/sglang/srt/model_executor @merrymercy @Ying1123 @hnyls2002 @Fridge003 @ispobock
/python/sglang/srt/model_executor/piecewise_cuda_graph_runner.py @hebiao064
/python/sglang/srt/model_loader @b8zhong
/python/sglang/srt/models/deepseek_common @Fridge003 @ispobock @fzyzcjy @ch-wan
/python/sglang/srt/models/deepseek_v2.py @fzyzcjy @zhyncs @ispobock @ch-wan @merrymercy @Fridge003
/python/sglang/srt/models/transformers.py @adarshxs
/python/sglang/srt/multimodal @mickqian @JustinTong0323 @yhyang201 @yuan-luo
/python/sglang/srt/observability @merrymercy @fzyzcjy @sufeng-buaa
/python/sglang/srt/parser @JustinTong0323
/python/sglang/srt/platforms @merrymercy @whybeyoung @alexnails
/python/sglang/srt/ray @Qiaolin-Yu @xyuzh
/python/sglang/srt/speculative @Ying1123 @merrymercy @hnyls2002 @Qiaolin-Yu
/python/sglang/srt/utils/hf_transformers @JustinTong0323
/sgl-kernel @ispobock @BBuf @yizhang2077 @merrymercy @FlamingoPg @HaiShaw
/sgl-kernel/csrc/musa @yeahdongcn
/sgl-model-gateway @slin1237 @CatherineSue
/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
/sgl-model-gateway/src/grpc_client @CatherineSue @slin1237
/sgl-model-gateway/src/mcp @key4ng @slin1237
/sgl-model-gateway/src/policies @slin1237 @ByronHsu
/sgl-model-gateway/src/proto @CatherineSue @slin1237
/sgl-model-gateway/src/protocols @CatherineSue @key4ng
/sgl-model-gateway/src/reasoning_parser @CatherineSue
/sgl-model-gateway/src/routers @CatherineSue @key4ng @slin1237
/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/registered/prefill_only @sundar24295s @chanh @fortunecookiee
/benchmark/prefill_only/bench_score.py @sundar24295s @chanh @fortunecookiee
/test/registered/ascend @ping1jing2 @ssshinigami @e-martirosian
/test/srt/test_modelopt* @Edwardf0t1
/python/sglang/srt/layers/gemma4_fused_ops.py @merrymercy @Ying1123 @Fridge003 @ispobock @HaiShaw @ch-wan @BBuf @Edwardf0t1 @kpham-sgl @pyc96
/python/sglang/srt/function_call/gemma4_detector.py @CatherineSue @JustinTong0323 @kpham-sgl @pyc96
/python/sglang/srt/models/gemma4_*.py @kpham-sgl @pyc96
/python/sglang/srt/multimodal/processors/gemma4.py @kpham-sgl @pyc96
/python/sglang/test/ascend @ping1jing2 @ssshinigami @e-martirosian
/docs_new/cookbook/autoregressive/Google/Gemma4.mdx @wisclmy0611 @zijiexia @Richardczl98 @kpham-sgl @pyc96
/docs_new/src/snippets/autoregressive/gemma4-deployment.jsx @wisclmy0611 @zijiexia @Richardczl98 @kpham-sgl @pyc96
/python/sglang/srt/speculative/ngram_*.py @hnyls2002 @Qiaolin-Yu @kpham-sgl
/python/sglang/srt/speculative/adaptive_*.py @Qiaolin-Yu @alphabetc1
/python/sglang/srt/speculative/cpp_ngram @hnyls2002 @Qiaolin-Yu @kpham-sgl
/python/sglang/srt/speculative/frozen_kv_mtp_*.py @hnyls2002 @Qiaolin-Yu @kpham-sgl @pyc96
/python/sglang/jit_kernel/ngram_*.py @hnyls2002 @Qiaolin-Yu @kpham-sgl
/python/sglang/jit_kernel/csrc/ngram_corpus @hnyls2002 @Qiaolin-Yu @kpham-sgl
+12
View File
@@ -0,0 +1,12 @@
# Maintenance Tools
This folder contains tools and workflows for automating maintenance tasks.
## CI Permissions
`CI_PERMISSIONS.json` defines the CI permissions granted to each user.
Maintainers can directly edit the file to add entries with `"reason": "custom override"`.
Maintainers can also run `update_ci_permission.py` to update it with some auto rules (e.g., top contributors in the last 90 days get full permissions).
## Others
- `MAINTAINER.md` defines the code maintenance model.
+35
View File
@@ -0,0 +1,35 @@
name: 🐞 Bug report
description: Report a bug to help us reproduce and fix it.
title: "[Bug] "
labels: ['Bug']
body:
- type: checkboxes
attributes:
label: Checklist
options:
- label: I searched related issues but found no solution.
- label: The bug persists in the latest version.
- label: Issues without environment info and a minimal reproducible demo are hard to resolve and may receive no feedback.
- label: If this is not a bug report but a general question, please start a discussion at https://github.com/sgl-project/sglang/discussions. Otherwise, it will be closed.
- label: Please use English. Otherwise, it will be closed.
- type: textarea
attributes:
label: Describe the bug
description: A clear, concise description of the bug.
validations:
required: true
- type: textarea
attributes:
label: Reproduction
description: Command/script run and model used.
placeholder: Paste the command here.
validations:
required: true
- type: textarea
attributes:
label: Environment
description: Run `python3 -m sglang.check_env` and paste output here. Issues without this will be closed.
placeholder: Paste environment output here.
validations:
required: true
@@ -0,0 +1,23 @@
name: 🚀 Feature request
description: Suggest an idea for this project
title: "[Feature] "
body:
- type: checkboxes
attributes:
label: Checklist
options:
- label: If this is not a feature request but a general question, please start a discussion at https://github.com/sgl-project/sglang/discussions. Otherwise, it will be closed.
- label: Please use English. Otherwise, it will be closed.
- type: textarea
attributes:
label: Motivation
description: |
Clearly and concisely describe the feature's motivation.
validations:
required: true
- type: textarea
attributes:
label: Related resources
description: |
Provide official releases or third-party implementations if available.
@@ -0,0 +1,108 @@
name: 🧪 Playground - Verified Cell Submission
description: Submit a deployment recipe you verified in the cookbook Playground. A maintainer will review and promote it into the cell catalog via PR.
title: "[Playground] Verified cell: <fill from playground>"
labels: ["cookbook", "playground-submission"]
body:
- type: markdown
attributes:
value: |
Thanks for verifying a deployment recipe!
The cookbook **Playground** section of each model page pre-fills this
form. When your overrides diverge from the verified cell, click
**Submit ↗**, confirm the SGLang version / benchmark / notes in the
**Submit verified cell** dialog and tick the attestations, then
**Open submission on GitHub →**. The model, combination, proposed cell
snippet, and existing cell are filled automatically — please double-check
the snippet matches what you ran.
Once a maintainer with access to the listed hardware reproduces the
recipe, they'll convert this issue into a PR against the cookbook cell
catalog and close this issue.
- type: input
id: model
attributes:
label: Cookbook model
description: Which model's cookbook does this cell belong to? Auto-filled by the playground — use the model id as it appears in the cookbook config.
placeholder: "deepseek-ai/deepseek-v4"
validations:
required: true
- type: input
id: combination
attributes:
label: Combination
description: "Format: hw / variant / quant / strategy / nodes (auto-filled by the playground)."
placeholder: "b200 / flash / fp4 / low-latency / single"
validations:
required: true
- type: textarea
id: cell-snippet
attributes:
label: Proposed cell snippet
description: |
The cell object exactly as it should land in the `cells: [...]` array
of the cookbook config. Auto-generated by the playground — please do not
hand-edit unless you're sure.
render: javascript
validations:
required: true
- type: textarea
id: existing-cell
attributes:
label: Existing cell at this match (for diff)
description: |
The current verified cell at the same `match` tuple, if any. The
playground fills this so the maintainer can see exactly what changed.
Leave empty if no cell yet exists at this match.
render: javascript
validations:
required: false
- type: input
id: sglang-version
attributes:
label: SGLang version
description: Version, tag, or git SHA you tested against.
placeholder: "sglang==0.5.4 (or git SHA abc1234)"
validations:
required: true
- type: textarea
id: bench-result
attributes:
label: Benchmark result (optional but encouraged)
description: |
One-line perf numbers (TTFT, TPOT, tokens/sec, accepted-rate, etc.).
Helps maintainers decide whether this recipe should replace the
existing one or be added as an alternative.
placeholder: "TTFT 95 ms / TPOT 18 ms / 1820 tok/s @ bs=64"
validations:
required: false
- type: textarea
id: notes
attributes:
label: Notes / caveats
description: |
Anything unusual: cluster config, env-var quirks, NIC mappings,
multi-node bootstrap details, etc.
validations:
required: false
- type: checkboxes
id: attestation
attributes:
label: Attestation
description: All three must be ticked. We trust contributors but maintainers will still re-verify before merging.
options:
- label: I ran this exact command on the listed hardware.
required: true
- label: The server reached READY and answered a cURL request successfully.
required: true
- label: Output looked correct on at least one prompt.
required: true
+167
View File
@@ -0,0 +1,167 @@
# SGLang Code Maintenance Model
This document describes the code maintenance model for the SGLang project.
Since SGLang is a large project involving multiple organizations and hardware platforms, we designed this model with the following goals:
- Ensure a responsive and smooth review process.
- Allow for fast iteration, so maintainers can sometimes bypass flaky CI tests for important PRs.
## Role Descriptions
There are four roles in this maintenance model. Some are custom roles, while others are predefined by GitHub.
- **Merge Oncall**: The person who drives the PR merge process. They have strong area-specific expertise and uphold a high bar for code quality.
- Permission: Merge PRs. Bypass branch protection rules if needed.
- Responsibility: Shepherd the merge of PRs assigned to their area. Revert or hotfix any issues related to their merge (especially if they bypass).
- **Codeowner**: The person who protects critical code. Without a bypass, each PR needs at least one Codeowner approval for each modified file protected by [CODEOWNERS](./CODEOWNERS). Please note that this role is not an honor but a significant responsibility because PRs cannot be merged without your approval (except when bypassed by a Merge Oncall).
- Permission: Approve PRs, allowing them to be merged without a bypass.
- Responsibility: Review PRs in a timely manner.
- **Write**: A person with write permission to the SGLang repo.
- Permission: Merge PRs if they have passed required tests and been approved by Codeowners. This role cannot bypass branch protection rules.
- Responsibility: Review and merge PRs in a timely manner.
- **CI Oncall**: A person who manages CI runners for specific hardware platforms.
- Permission: Add CI runners.
- Responsibility: Keep the CI runners up and running.
__Note__: Difference between Merge Oncall and Codeowner
- The Merge Oncall is an active role held by someone who actively tries to help merge PRs and can bypass CI if needed.
- The Codeowner is a passive protection role provided by GitHub; it prevents accidental changes to critical code.
- The list of Merge Oncalls is attached below. The list of Codeowners is in the [CODEOWNERS](./CODEOWNERS) file.
__Note__: The permissions to trigger CI tests are defined separately according to these [rules](https://docs.sglang.io/developer_guide/contribution_guide.html#how-to-trigger-ci-tests).
## Pull Request Merge Process
1. The author submits a pull request (PR) and fills out the PR checklist.
2. A bot assigns this PR to a Merge Oncall and @-mentions them. At the same time, GitHub will automatically request reviews from Codeowners.
3. Someone tags the PR with a `run-ci` label ([help](https://docs.sglang.io/developer_guide/contribution_guide.html#how-to-trigger-ci-tests)). Then the author can trigger CI by pushing new commits.
4. The Merge Oncall coordinates the review (e.g., asking people to review) and approves the PR; the Codeowners also approve the PR. If the assigned Merge Oncall is not responsive, the author can ping other related Merge Oncalls and Reviewers in the list below.
5. The code can now be merged:
- **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/): #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).
### 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](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
This section lists the oncalls for each hardware platform. The format is @github-username (Slack username).
### NVIDIA GPUs
[@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](https://github.com/saienduri) (Sai Enduri), [@HaiShaw](https://github.com/HaiShaw) (Henry HAI)
### Intel CPU and XPU
[@mingfeima](https://github.com/mingfeima) (Mingfei Ma), [@DiweiSun](https://github.com/DiweiSun) (Diwei Sun)
### Ascend NPUs
[@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](https://github.com/merrymercy) and [Ying Sheng](https://github.com/Ying1123) in the Slack channel. They will start a nomination and internal review process.
## CI Maintenance Mode
When the CI is unhealthy (e.g., the scheduled pr-test on `main` is broken for consecutive runs), the project enters **CI Maintenance Mode** by opening [issue #21065](https://github.com/sgl-project/sglang/issues/21065). While active:
- All PR CI runs are paused. Resources are allocated to PRs that fix the CI.
- **Merging non-CI-fix PRs is prohibited.** Only PRs that fix the CI may be merged. In severe cases, merge permissions may be revoked.
Maintenance mode ends when `pr-test.yml` is all green on `main` and the issue is closed.
### Rebase-Required Mode
When a major update lands on `main` and all open PRs must rebase before CI can run (without fully pausing CI), add a line of the form `MIN_BASE_SHA: <sha>` to the body of issue #21065. **The rebase check is enforced regardless of whether the issue is open or closed** — you do not need to enter full maintenance mode (open the issue) to use this gate; just editing the body to include the directive is enough. While the directive is present:
- CI is allowed to run only for PRs whose branch already contains `<sha>` (GitHub compare API status `ahead` or `identical` — i.e., the PR has `<sha>` in its history).
- PRs that are `behind` or `diverged` from `<sha>` are blocked with a "rebase required" error until they rebase onto the latest `main`.
- The `bypass-maintenance` label still bypasses this check for CI-fix PRs.
Notes:
- Only the **first** `MIN_BASE_SHA:` line in the issue body is read.
- The SHA must be 7-40 hex characters; malformed values are ignored (with a warning in the job summary).
- Avoid pasting the directive inside a fenced code block in the issue body — the parser does not skip code fences and may match example snippets.
Remove the directive from the issue body to lift the rebase requirement (closing the issue does NOT lift it on its own).
## Suspending Permissions
If a Merge Oncall bypasses checks to merge a PR that breaks the `main` branch, merges a non-CI-fix PR during CI Maintenance Mode, or repeatedly breaks the CI due to various reasons, their privileges will be suspended for at least two days, depending on the severity of the incident.
@@ -0,0 +1,168 @@
name: Check Maintenance Mode
description: Blocks CI in two independent modes driven by issue #21065. (1) Full-pause: when the issue is open. (2) Rebase-required: whenever the issue body contains a `MIN_BASE_SHA: <sha>` directive — enforced regardless of whether the issue is open or closed, so maintainers can require all PRs to rebase past a specific commit without having to open the maintenance issue. Both modes are bypassed by the `bypass-maintenance` label on the PR, or by env PR_TEST_BYPASS_MAINTENANCE_ON_MAIN=true (PR Test workflow on main only). Merging non-CI-fix PRs is prohibited during full-pause; in severe cases, merge permissions may be revoked.
inputs:
github-token:
description: GitHub token for API access
required: false
default: ${{ github.token }}
runs:
using: composite
steps:
- name: Check maintenance mode
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
run: |
MAINTENANCE_ISSUE=21065
REPO="${{ github.repository }}"
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_HEAD_SHA="${{ github.event.pull_request.head.sha }}"
# PR Test workflow only: scheduled runs and runs on main (dispatch / workflow_call) set this env
if [[ "${PR_TEST_BYPASS_MAINTENANCE_ON_MAIN:-}" == "true" ]]; then
echo "✅ PR Test on main branch; bypassing maintenance gate."
exit 0
fi
# Use curl + jq instead of `gh` because self-hosted GPU runners
# don't have the gh CLI installed. Without this, the action
# silently fell through to "Proceeding with CI" on every GPU
# job — the rebase gate only fired on ubuntu-latest jobs that
# had gh pre-installed. curl+jq are reliably available on every
# Linux runner.
gh_api() {
local path="$1"
local err_file="$2"
curl --silent --show-error --fail \
--max-time 30 \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/$path" 2>"$err_file"
}
# Fetch issue state and body. Fail-open: if we can't read the issue
# (network blip, missing token scope), CI proceeds.
ERR_FILE=$(mktemp)
ISSUE_JSON=$(gh_api "repos/$REPO/issues/$MAINTENANCE_ISSUE" "$ERR_FILE" || true)
if [[ -z "$ISSUE_JSON" ]]; then
echo "⚠️ Issue fetch returned empty. curl stderr was:"
cat "$ERR_FILE" || true
fi
rm -f "$ERR_FILE"
ISSUE_STATE=$(printf '%s' "$ISSUE_JSON" | jq -r '.state // "UNKNOWN"' 2>/dev/null || echo "UNKNOWN")
# Issues API returns state in lowercase ("open"/"closed"); normalize
# to uppercase so existing comparisons against "OPEN" still work.
ISSUE_STATE=$(printf '%s' "$ISSUE_STATE" | tr '[:lower:]' '[:upper:]')
ISSUE_BODY=$(printf '%s' "$ISSUE_JSON" | jq -r '.body // ""' 2>/dev/null || echo "")
echo "DEBUG: ISSUE_STATE=$ISSUE_STATE body_length=${#ISSUE_BODY}"
# Parse optional `MIN_BASE_SHA: <sha>` directive from the issue body
# (first occurrence wins). Whenever this directive is present, the
# rebase check is enforced regardless of whether the issue is open
# or closed — so maintainers can require all PRs to rebase past a
# specific commit without having to open the maintenance issue.
# `grep` exits 1 on no-match, which under `set -eo pipefail` (the
# default for `shell: bash` composite steps) would abort the whole
# script — silently failing every PR whose issue body has no
# MIN_BASE_SHA line. Wrap grep in a brace group with `|| true` so
# an empty match falls through cleanly to "no directive set".
MIN_BASE_SHA=$(
{ printf '%s' "$ISSUE_BODY" | tr -d '\r' | grep -iE '^[[:space:]]*`?MIN_BASE_SHA`?[[:space:]]*[:=]' || true; } \
| head -n1 | sed -E 's/.*[:=][[:space:]]*//; s/`//g' | awk '{print $1}'
)
if [[ -n "$MIN_BASE_SHA" ]] && ! [[ "$MIN_BASE_SHA" =~ ^[a-fA-F0-9]{7,40}$ ]]; then
WARN="⚠️ Ignoring malformed MIN_BASE_SHA directive in issue #$MAINTENANCE_ISSUE: '$MIN_BASE_SHA' (must be 7-40 hex chars)"
echo "$WARN"
echo "$WARN" >> "$GITHUB_STEP_SUMMARY"
MIN_BASE_SHA=""
fi
# If neither gate is active (no MIN_BASE_SHA, issue not open), nothing to do.
if [[ -z "$MIN_BASE_SHA" && "$ISSUE_STATE" != "OPEN" ]]; then
echo "✅ Maintenance mode is OFF and no MIN_BASE_SHA directive. Proceeding with CI."
exit 0
fi
# bypass-maintenance label bypasses both gates. PR labels live on
# the issue resource (PRs are issues with extra metadata in the
# GH API), so we hit the same /issues/{n} endpoint.
if [[ -n "$PR_NUMBER" ]]; then
ERR_FILE=$(mktemp)
PR_JSON=$(gh_api "repos/$REPO/issues/$PR_NUMBER" "$ERR_FILE" || true)
rm -f "$ERR_FILE"
HAS_BYPASS=$(printf '%s' "$PR_JSON" | jq -r '[.labels[]?.name] | map(select(. == "bypass-maintenance")) | length' 2>/dev/null || echo "0")
if [[ "${HAS_BYPASS:-0}" -gt 0 ]]; then
echo "✅ PR #$PR_NUMBER has 'bypass-maintenance' label. Bypassing maintenance + rebase checks."
exit 0
fi
fi
# Rebase-required gate (independent of issue open/closed state).
if [[ -n "$MIN_BASE_SHA" ]]; then
if [[ -z "$PR_NUMBER" || -z "$PR_HEAD_SHA" ]]; then
echo "✅ Not a PR context; skipping rebase check."
else
# Use GitHub compare API: status is "ahead"/"identical" when MIN_BASE_SHA is reachable from PR head.
ERR_FILE=$(mktemp)
COMPARE_JSON=$(gh_api "repos/$REPO/compare/$MIN_BASE_SHA...$PR_HEAD_SHA" "$ERR_FILE" || true)
if [[ -z "$COMPARE_JSON" ]]; then
echo "⚠️ Compare API failed. curl stderr was:"
cat "$ERR_FILE" || true
fi
rm -f "$ERR_FILE"
COMPARE_STATUS=$(printf '%s' "$COMPARE_JSON" | jq -r '.status // "UNKNOWN"' 2>/dev/null || echo "UNKNOWN")
COMPARE_STATUS="${COMPARE_STATUS:-UNKNOWN}"
case "$COMPARE_STATUS" in
ahead|identical)
echo "✅ PR #$PR_NUMBER contains required base ${MIN_BASE_SHA:0:12} ($COMPARE_STATUS)."
;;
UNKNOWN)
echo "⚠️ Could not determine rebase status via GitHub API; fail-open, allowing rebase check to pass."
;;
*)
MSG=$(printf "%s\n" \
"## ⚠️ Rebase Required Before CI Can Run" \
"A major update has landed on \`main\`. All PRs must rebase onto the latest \`main\` before CI will run." \
"Required base commit: \`${MIN_BASE_SHA:0:12}\` (your PR is \`$COMPARE_STATUS\` relative to this commit)." \
"" \
"What should you do?" \
"- Rebase your branch onto the latest \`main\` and push again" \
"- Follow CI Maintenance Mode issue: https://github.com/$REPO/issues/$MAINTENANCE_ISSUE for context" \
"- CI-fix PRs may request the \`bypass-maintenance\` label to skip this check")
echo "$MSG" >> "$GITHUB_STEP_SUMMARY"
while IFS= read -r line; do
echo "::error::$line"
done <<< "$MSG"
exit 1
;;
esac
fi
fi
# Full-pause maintenance gate (only when issue is open).
if [[ "$ISSUE_STATE" == "OPEN" ]]; then
MSG=$(printf "%s\n" \
"## ⚠️ CI Maintenance Mode is Active" \
"The CI infrastructure is currently under maintenance." \
"All PR CI runs are paused until maintenance is complete." \
"**Merging non-CI-fix PRs is prohibited during maintenance mode.** In severe cases, merge permissions may be revoked." \
"You might also experience unexpected failures during this period." \
"The team is working on the issue and will update the status as soon as possible." \
"" \
"What should you do?" \
"- **Do NOT merge non-CI-fix PRs** until maintenance mode is lifted" \
"- Check back later (~12 hours)" \
"- Follow CI Maintenance Mode issue: https://github.com/$REPO/issues/$MAINTENANCE_ISSUE for status updates")
echo "$MSG" >> "$GITHUB_STEP_SUMMARY"
while IFS= read -r line; do
echo "::error::$line"
done <<< "$MSG"
exit 1
fi
echo "✅ Rebase check passed; full-pause not active. Proceeding with CI."
@@ -0,0 +1,98 @@
name: Check PR Test Health
description: Fail fast if any job in the current workflow run has already failed, or if the lint check (from lint.yml) has failed. Auto-skips for scheduled runs. The jobs-failed check (but not the lint check) is bypassed when the PR carries the `bypass-fastfail` label.
inputs:
github-token:
description: 'GitHub token for API calls'
required: false
default: ${{ github.token }}
runs:
using: composite
steps:
- name: Check PR test health
uses: actions/github-script@v8
env:
SKIP_PR_TEST_HEALTH_CHECK: ${{ env.SKIP_PR_TEST_HEALTH_CHECK }}
with:
github-token: ${{ inputs.github-token }}
script: |
// Skip when explicitly requested via env var (e.g. release branch cut)
if (process.env.SKIP_PR_TEST_HEALTH_CHECK === 'true') {
core.info('Skipping health check (SKIP_PR_TEST_HEALTH_CHECK=true)');
return;
}
// Skip for scheduled runs — they should collect all failures, not fast-fail
if (context.eventName === 'schedule') {
core.info('Skipping health check for scheduled run');
return;
}
// Check lint status from the separate Lint workflow (lint.yml).
// listJobsForWorkflowRun only sees jobs within the SAME run, so we use
// checks.listForRef which queries by commit SHA across ALL workflows.
const ref = context.payload.pull_request?.head?.sha || context.sha;
const { data } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: ref,
check_name: 'lint',
});
const lintRun = data.check_runs.find(
cr => cr.app?.slug === 'github-actions'
);
if (lintRun?.status === 'completed' && lintRun?.conclusion === 'failure') {
core.setFailed('Fast-fail: lint check failed');
return;
}
// Skip the jobs-failed check when the PR carries the bypass-fastfail label.
// Lint check above still runs.
let labels = [];
if (context.payload.pull_request?.labels) {
labels = context.payload.pull_request.labels.map(l => l.name);
} else {
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: ref,
});
if (prs.length > 0) {
labels = prs[0].labels.map(l => l.name);
}
}
if (labels.includes('bypass-fastfail')) {
core.info('Skipping jobs-failed check (bypass-fastfail label present)');
return;
}
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
per_page: 100,
});
// Find jobs that failed from a real error, not from fast-fail cascade
const rootCauseFailures = jobs.filter(j => {
if (j.status !== 'completed' || j.conclusion !== 'failure') return false;
// h20 runners are flaky (dirty GPU state from prior runs); their failures
// should not cascade fast-fail to other stages. j.name shape from
// listJobsForWorkflowRun: "<job-key>" + optional " / <reusable-job>"
// + optional " (<matrix>)". Split off the base job key before exact
// match so we cover both inline + reusable forms without confusing
// 'h20' with the 'h200' prefix.
const baseName = j.name.split(/[ /]/)[0];
if (baseName === 'base-c-test-8-gpu-h20') {
return false;
}
// If the failing step is the health check, it's a cascade — skip it
const failedStep = (j.steps || []).find(s => s.conclusion === 'failure');
if (failedStep && (failedStep.name.includes('check-pr-test-health') || failedStep.name.includes('Check PR test health'))) {
return false;
}
return true;
});
if (rootCauseFailures.length > 0) {
core.setFailed(`Fast-fail: skipping — root cause job(s): ${rootCauseFailures.map(j => j.name).join(', ')}`);
}
@@ -0,0 +1,101 @@
name: Upload CUDA Coredumps
description: Upload CUDA coredump files as artifacts, optionally signal to a tracker issue, and clean up.
inputs:
artifact-suffix:
description: Suffix appended to the artifact name (e.g. matrix partition id)
required: false
default: ""
retention-days:
description: Number of days to retain the artifact
required: false
default: "7"
tracker-issue:
description: |
If set, post a one-line comment to sgl-project/sglang issue
#<tracker-issue> when at least one coredump is detected. Requires
`bot-token` with issues:write on sgl-project/sglang.
required: false
default: ""
bot-token:
description: PAT with issues:write on sgl-project/sglang. Required when tracker-issue is set.
required: false
default: ""
runs:
using: composite
steps:
- name: Check for coredumps
id: check
shell: bash
run: |
# Mirror get_dump_dir() in python/sglang/srt/debug_utils/cuda_coredump.py:
# explicit override > per-job RUNNER_TEMP > /tmp default, then a
# per-(run, attempt) subdir so dumps are never mis-attributed across CI
# jobs that share a self-hosted runner's filesystem.
if [ -n "$SGLANG_CUDA_COREDUMP_DIR" ]; then
base="$SGLANG_CUDA_COREDUMP_DIR"
elif [ -n "$RUNNER_TEMP" ]; then
base="$RUNNER_TEMP/sglang_cuda_coredumps"
else
base="/tmp/sglang_cuda_coredumps"
fi
if [ -n "$GITHUB_RUN_ID" ]; then
dir="$base/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT:-1}"
else
dir="$base"
fi
echo "dump_dir=$dir" >> "$GITHUB_OUTPUT"
if [ -d "$dir" ] && [ -n "$(ls -A "$dir" 2>/dev/null)" ]; then
echo "has_dumps=true" >> "$GITHUB_OUTPUT"
else
echo "has_dumps=false" >> "$GITHUB_OUTPUT"
fi
- name: Upload CUDA coredumps
if: steps.check.outputs.has_dumps == 'true'
uses: actions/upload-artifact@v4
with:
name: cuda-coredumps-${{ github.job }}${{ inputs.artifact-suffix && format('-{0}', inputs.artifact-suffix) }}
path: ${{ steps.check.outputs.dump_dir }}/
retention-days: ${{ inputs.retention-days }}
- name: Signal coredump to tracker issue
if: steps.check.outputs.has_dumps == 'true' && inputs.tracker-issue != '' && inputs.bot-token != ''
shell: bash
env:
BOT_TOKEN: ${{ inputs.bot-token }}
PR_NUM: ${{ github.event.pull_request.number }}
EVENT_NAME: ${{ github.event_name }}
TRACKER_ISSUE: ${{ inputs.tracker-issue }}
run: |
if [ -n "$PR_NUM" ]; then
ref_label="PR #${PR_NUM}"
else
ref_label="$EVENT_NAME"
fi
# Resolve own job_id via REST API: match by runner_name + status
# in_progress (the current job is the only one in_progress on this
# runner). Robust across matrix expansions and artifact-suffix shapes.
job_id=$(curl -sS \
-H "Authorization: Bearer ${BOT_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}/jobs?per_page=100" \
| python3 -c 'import json,sys,os; print(next((j["id"] for j in json.load(sys.stdin)["jobs"] if j.get("runner_name")==os.environ["RUNNER_NAME"] and j.get("status")=="in_progress"), ""))')
if [ -n "$job_id" ]; then
run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/job/${job_id}"
else
# Fallback to run-attempt URL if job_id lookup failed
run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}"
fi
body_json=$(printf '{"body":"@hnyls2002 [Coredump Tracker] %s - %s"}' "${ref_label}" "${run_url}")
curl -sS -X POST \
-H "Authorization: Bearer ${BOT_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/sgl-project/sglang/issues/${TRACKER_ISSUE}/comments" \
-d "${body_json}"
- name: Cleanup CUDA coredumps
shell: bash
run: rm -rf "${{ steps.check.outputs.dump_dir }}"
+229
View File
@@ -0,0 +1,229 @@
name: Wait for Jobs
description: Poll and wait for specified jobs in the current workflow run to complete. Returns success immediately when the PR carries the `bypass-fastfail` label, letting downstream stages dispatch in parallel (same effect as scheduled runs).
inputs:
stage-name:
description: 'Human-readable stage name for log messages (e.g. "base-a")'
required: true
jobs:
description: |
JSON array of job specs to wait for. Each element is either:
- a string: exact job name (e.g. "base-a-test-1-gpu-small")
- an object { "prefix": "...", "expected_count": N }: for matrix jobs
required: true
max-wait-minutes:
description: 'Maximum time to wait before timing out'
required: false
default: '240'
poll-interval-seconds:
description: 'Seconds between polling attempts'
required: false
default: '60'
github-token:
description: 'GitHub token for API calls'
required: false
default: ${{ github.token }}
outputs:
result:
description: 'Overall result: success, failure, or timeout'
value: ${{ steps.wait.outputs.result }}
runs:
using: composite
steps:
- name: Wait for jobs to complete
id: wait
uses: actions/github-script@v8
env:
INPUT_STAGE_NAME: ${{ inputs.stage-name }}
INPUT_JOBS: ${{ inputs.jobs }}
INPUT_MAX_WAIT_MINUTES: ${{ inputs.max-wait-minutes }}
INPUT_POLL_INTERVAL_SECONDS: ${{ inputs.poll-interval-seconds }}
with:
github-token: ${{ inputs.github-token }}
script: |
const stageName = process.env.INPUT_STAGE_NAME;
const jobSpecs = JSON.parse(process.env.INPUT_JOBS);
const maxWaitMinutes = parseInt(process.env.INPUT_MAX_WAIT_MINUTES);
const pollIntervalSeconds = parseInt(process.env.INPUT_POLL_INTERVAL_SECONDS);
const maxAttempts = (maxWaitMinutes * 60) / pollIntervalSeconds;
// bypass-fastfail label opts the PR out of stage-to-stage waiting,
// letting all stages dispatch in parallel like scheduled runs do.
let labels = [];
if (context.payload.pull_request?.labels) {
labels = context.payload.pull_request.labels.map(l => l.name);
} else {
const ref = context.payload.pull_request?.head?.sha || context.sha;
try {
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: ref,
});
if (prs.length > 0) {
labels = prs[0].labels.map(l => l.name);
}
} catch (e) {
console.log(`Could not fetch PR labels for ${ref}: ${e.message}`);
}
}
if (labels.includes('bypass-fastfail')) {
console.log(`Skipping ${stageName} wait (bypass-fastfail label present)`);
core.setOutput('result', 'success');
return;
}
// Normalize job specs into a uniform format
const normalizedSpecs = jobSpecs.map(spec => {
if (typeof spec === 'string') {
return { prefix: spec, expected_count: 1, exact: true };
}
return { ...spec, exact: false };
});
const totalExpectedJobs = normalizedSpecs.reduce((sum, s) => sum + s.expected_count, 0);
const matchesSpec = (jobName, spec) => {
if (spec.exact) {
return jobName === spec.prefix;
}
// Match bare prefix or any GHA-rendered shard suffix. Inline matrix
// produces `<prefix> (<shard>)`; reusable-workflow callers produce
// `<prefix> / <called-job> (<shard>)`. Both have a space after the
// prefix, so a single ' '-delimited check covers both.
return jobName === spec.prefix || jobName.startsWith(spec.prefix + ' ');
};
// Use ETag conditional requests to avoid consuming rate limit when nothing changed.
// GitHub returns 304 Not Modified for unchanged data, which is FREE (no rate limit cost).
let lastEtag = '';
let lastJobs = null;
let apiCalls = 0;
let cachedCalls = 0;
async function fetchJobs() {
const url = `GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs`;
const params = {
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
per_page: 100,
headers: {},
};
if (lastEtag) {
params.headers['if-none-match'] = lastEtag;
}
try {
const response = await github.request(url, params);
apiCalls++;
const rateRemaining = response.headers['x-ratelimit-remaining'] || '?';
const rateLimit = response.headers['x-ratelimit-limit'] || '?';
console.log(`[rate-limit] ${rateRemaining}/${rateLimit} remaining (ETag: ${lastEtag ? 'sent' : 'none'}) | this session: ${apiCalls} paid, ${cachedCalls} free`);
lastEtag = response.headers.etag || '';
const jobs = response.data.jobs;
// Handle pagination if >100 jobs
// ETag only covers page 1, so invalidate it to avoid stale cache
// when later pages change but page 1 doesn't.
if (response.data.total_count > 100) {
lastEtag = '';
for (let page = 2; page <= Math.ceil(response.data.total_count / 100); page++) {
const { data: pageData } = await github.request(url, {
...params,
page,
headers: {},
});
jobs.push(...pageData.jobs);
}
}
lastJobs = jobs;
return { jobs, cached: false };
} catch (err) {
if (err.status === 304 && lastJobs) {
cachedCalls++;
console.log(`[rate-limit] 304 Not Modified | this session: ${apiCalls} paid, ${cachedCalls} free`);
return { jobs: lastJobs, cached: true };
}
throw err;
}
}
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const { jobs, cached } = await fetchJobs();
let allCompleted = true;
let failedJobs = [];
let completedCount = 0;
let totalCount = 0;
for (const spec of normalizedSpecs) {
const matchingJobs = jobs.filter(job => matchesSpec(job.name, spec));
for (const job of matchingJobs) {
totalCount++;
if (!cached) {
console.log(`${job.name}: status=${job.status}, conclusion=${job.conclusion}`);
}
if (job.status === 'completed') {
completedCount++;
if (job.conclusion !== 'success' && job.conclusion !== 'skipped') {
failedJobs.push(job.name);
}
} else {
allCompleted = false;
}
}
if (matchingJobs.length < spec.expected_count) {
// Job-level `if:` is evaluated before matrix expansion. When it
// evaluates false, GitHub emits exactly one "skipped" entry
// without a "(shard)" suffix instead of N matrix entries. Two
// shapes are possible:
// - inline matrix: `<prefix>`
// - reusable workflow: `<prefix> / <called-job>`
// Both lack the ` (` shard marker. Detect that precise shape so
// we don't poll forever — and so we don't mistake a partially
// materialized dynamic/reusable matrix for a skipped one.
const unexpandedSkip = matchingJobs.length === 1 &&
!matchingJobs[0].name.includes(' (') &&
matchingJobs[0].status === 'completed' &&
matchingJobs[0].conclusion === 'skipped';
if (unexpandedSkip) {
const missing = spec.expected_count - 1;
totalCount += missing;
completedCount += missing;
if (!cached) {
console.log(`${spec.prefix}: job-level skip (bare entry, conclusion=skipped); treating as all ${spec.expected_count} skipped`);
}
} else {
console.log(`${spec.prefix}: found ${matchingJobs.length}/${spec.expected_count} jobs (waiting for more)`);
allCompleted = false;
}
}
}
console.log(`[${stageName}] Progress: ${completedCount}/${totalCount} jobs completed (expected ${totalExpectedJobs})${cached ? ' (cached, no rate limit cost)' : ''}`);
// Fail fast if any jobs failed
if (failedJobs.length > 0) {
core.setOutput('result', 'failure');
core.setFailed(`${stageName} jobs failed: ${failedJobs.join(', ')}`);
return;
}
if (allCompleted && totalCount >= totalExpectedJobs) {
core.setOutput('result', 'success');
return;
}
console.log(`Waiting ${pollIntervalSeconds}s... (attempt ${attempt + 1}/${maxAttempts})`);
await new Promise(resolve => setTimeout(resolve, pollIntervalSeconds * 1000));
}
core.setFailed(`Timeout waiting for ${stageName} jobs`);
core.setOutput('result', 'timeout');
+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()
+134
View File
@@ -0,0 +1,134 @@
# Configuration for the GitHub Labeler action
# Automatically adds labels to PRs based on the files changed
# Router specific (Rust code in sgl-model-gateway)
model-gateway:
- changed-files:
- any-glob-to-any-file: 'sgl-model-gateway/**/*'
# Kernel specific
sgl-kernel:
- changed-files:
- any-glob-to-any-file: 'sgl-kernel/**/*'
# JIT kernel specific
jit-kernel:
- changed-files:
- any-glob-to-any-file: 'python/sglang/jit_kernel/**/*'
# Documentation
documentation:
- changed-files:
- any-glob-to-any-file:
- '**/*.md'
- '**/*.mdx'
- 'docs/**/*'
- 'README*'
- 'docs_new/**/*'
# Dependencies
dependencies:
- changed-files:
- any-glob-to-any-file:
- '**/requirements*.txt'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/pyproject*.toml'
- '**/setup.py'
- '**/poetry.lock'
- '**/package.json'
- '**/package-lock.json'
# Multi-modal
Multi-modal:
- changed-files:
- any-glob-to-any-file:
- '**/*multimodal*'
- '**/*vision*'
- '**/*vlm*'
# Diffusion
diffusion:
- changed-files:
- any-glob-to-any-file: 'python/sglang/multimodal_gen/**/*'
# LoRA
lora:
- changed-files:
- any-glob-to-any-file:
- '**/*lora*'
# Quantization
quant:
- changed-files:
- any-glob-to-any-file:
- '**/*quant*'
- '**/*quantization*'
# Speculative decoding
speculative-decoding:
- changed-files:
- any-glob-to-any-file:
- '**/*speculative*'
# AMD specific
amd:
- changed-files:
- any-glob-to-any-file:
- '**/*amd*'
- '**/*rocm*'
# NPU specific
npu:
- changed-files:
- any-glob-to-any-file:
- '**/*npu*'
- '**/*ascend*'
# Blackwell
blackwell:
- changed-files:
- any-glob-to-any-file:
- '**/*nvfp4*'
- 'sgl-kernel/csrc/attention/cutlass_sm100_mla/**/*'
- 'python/sglang/srt/layers/attention/trtllm_mla_backend.py'
- 'python/sglang/srt/layers/attention/trtllm_mha_backend.py'
# DeepSeek specific
deepseek:
- changed-files:
- any-glob-to-any-file:
- '**/*deepseek*'
# HiCache
hicache:
- changed-files:
- any-glob-to-any-file:
- '**/*hicache*'
# Deterministic
deterministic:
- changed-files:
- any-glob-to-any-file: 'python/sglang/srt/batch_invariant_ops/**/*'
# Piecewise CUDA Graph
piecewise-cuda-graph:
- changed-files:
- any-glob-to-any-file: 'python/sglang/srt/compilation/**/*'
# Moore Threads specific
mthreads:
- changed-files:
- any-glob-to-any-file:
- '**/*mthreads*'
- '**/*musa*'
- '**/musa/**'
# Apple Silicon specific
apple-silicon:
- changed-files:
- any-glob-to-any-file:
- '**/*metal*'
- '**/*mlx*'
- '**/mlx/**'
- '**/*mps*'
+42
View File
@@ -0,0 +1,42 @@
no_progress = true
verbose = "warn"
timeout = 20
max_concurrency = 8
retry_wait_time = 2
max_retries = 2
# CI should validate external links over the network.
offline = false
scheme = ["http", "https"]
exclude_path = [
# Exclude generated Sphinx build artifacts.
# - "(\\./)?" allows both "docs/..." and "./docs/..."
# - "[/\\\\]" supports both slash styles in CI environments
"^(\\./)?docs[/\\\\]_build[/\\\\]",
]
exclude = [
# Local-only endpoints referenced in docs/examples.
# These are expected to be unreachable in GitHub-hosted CI.
"^https?://localhost(:[0-9]+)?(/|$)",
"^http://127\\.0\\.0\\.1(:[0-9]+)?(/|$)",
# Vendor pages that frequently block/deny CI user-agents (transient 403/anti-bot).
"^https://www\\.intel\\.com/content/www/us/en/ark/products/series/240391/intel-arc-b-series-graphics\\.html$",
"^https://www\\.intel\\.com/content/www/us/en/ark/products/series/242616/intel-arc-pro-b-series-graphics\\.html$",
"^https://www\\.intel\\.com/content/www/us/en/products/sku/241598/intel-arc-b580-graphics/specifications\\.html$",
# Non-routable bind address used in examples, never externally reachable.
"^http://0\\.0\\.0\\.0(/|$)",
# Large doc portals with anti-bot/rate-limit behavior in CI.
# We keep API docs references in content but do not fail CI on access policy.
"^https://platform\\.openai\\.com/docs/",
"^https://gamma\\.app/docs/Optimizing-RL-with-SGLang-y0kqgj877k34779$",
"^https://aflah02\\.substack\\.com/p/multi-node-llm-inference-with-sglang/?$",
# Known noisy image URLs used in notebook-rendered examples.
"^https://github\\.com/sgl-project/sglang/blob/main/examples/assets/example_image\\.png\\?raw=true$",
"^https://raw\\.githubusercontent\\.com/sgl-project/sglang/main/examples/assets/example_image\\.png/?$",
"^https://raw\\.githubusercontent\\.com/sgl-project/sglang/main/assets/logo\\.png/?$",
]
+18
View File
@@ -0,0 +1,18 @@
# .github/linters/lychee.toml
no_progress = true
verbose = "warn"
timeout = 20
max_concurrency = 8
offline = true
# Ignore generated docs output; check source docs only.
exclude_path = [
"^(\\./)?docs[/\\\\]_build[/\\\\]",
]
exclude = [
"^https?://localhost(:[0-9]+)?(/|$)",
"^http://127\\.0\\.0\\.1(:[0-9]+)?(/|$)",
"^http://0\\.0\\.0\\.0(/|$)",
]
+33
View File
@@ -0,0 +1,33 @@
<!-- Thank you for your contribution! Please follow these guidelines to enhance your pull request. If anything is unclear, submit your PR and reach out to maintainers for assistance. Join our Slack community at https://slack.sglang.io to discuss further. -->
## Motivation
<!-- Describe the purpose and goals of this pull request. -->
## Modifications
<!-- Detail the changes made in this pull request. -->
## Accuracy Tests
<!-- If this pull request affects model outputs (e.g., changes to the kernel or model forward code), provide accuracy test results. -->
## Speed Tests and Profiling
<!-- If this pull request impacts inference speed, provide benchmarking and profiling results. -->
## Checklist
- [ ] Format your code according to the [Format code with pre-commit](https://docs.sglang.io/developer_guide/contribution_guide.html#format-code-with-pre-commit).
- [ ] Add unit tests according to the [Run and add unit tests](https://docs.sglang.io/developer_guide/contribution_guide.html#run-and-add-unit-tests).
- [ ] Update documentation according to [Write documentations](https://docs.sglang.io/developer_guide/contribution_guide.html#write-documentations).
- [ ] 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 and 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.
- 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.
+244
View File
@@ -0,0 +1,244 @@
"""
Update the CI permissions configuration file.
This script updates the `CI_PERMISSIONS.json` file, which defines the CI permissions granted to each user.
The format of `CI_PERMISSIONS.json` is as follows:
{
"username1": {
"can_tag_run_ci_label": true,
"can_rerun_failed_ci": true,
"cooldown_interval_minutes": 0,
"reason": "top contributor"
},
"username2": {
"can_tag_run_ci_label": true,
"can_rerun_failed_ci": true,
"cooldown_interval_minutes": 60,
"reason": "custom override"
}
}
Permissions are assigned according to the following rules:
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:
{
"can_tag_run_ci_label": true,
"can_rerun_failed_ci": true,
"cooldown_interval_minutes": 60,
"reason": "custom override"
}
- For all other cases, preserve the original configuration unchanged.
3. All other users receive no permissions and a 120-minute cooldown (they are omitted from the file).
Usage:
export GH_TOKEN="your_github_token"
python3 update_ci_permission.py
# Sort-only mode (no network calls, no GH_TOKEN required)
python3 update_ci_permission.py --sort-only
"""
import argparse
import json
import os
from collections import Counter
from datetime import datetime, timedelta, timezone
try:
import requests
except ImportError:
requests = None # Only needed for non-sort-only runs
# Configuration
REPO_OWNER = "sgl-project"
REPO_NAME = "sglang"
FILE_NAME = os.path.join(os.path.dirname(__file__), "CI_PERMISSIONS.json")
HEADERS = {}
def github_api_get(endpoint, params=None):
"""Helper to make paginated GitHub API requests."""
if requests is None:
raise RuntimeError(
"The requests package is required. Install it or use --sort-only."
)
if not HEADERS:
raise RuntimeError(
"GitHub headers not initialized. Set GH_TOKEN or use --sort-only."
)
results = []
url = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/{endpoint}"
while url:
response = requests.get(url, headers=HEADERS, params=params)
if response.status_code != 200:
print(f"Error fetching {url}: {response.status_code} {response.text}")
# If we fail to fetch, strictly return what we have or empty to avoid crashing logic
break
data = response.json()
if isinstance(data, list):
results.extend(data)
else:
return data # Non-list response (not paginated usually)
# Handle pagination
url = None
if "link" in response.headers:
links = response.headers["link"].split(", ")
for link in links:
if 'rel="next"' in link:
url = link[link.find("<") + 1 : link.find(">")]
params = None # Params are included in the next link
break
return results
def get_write_access_users():
"""Fetches users with push (write) or admin access."""
print("Fetching collaborators with write access...")
# Note: This endpoint usually requires admin rights on the token.
collaborators = github_api_get("collaborators", params={"per_page": 100})
writers = set()
for col in collaborators:
perms = col.get("permissions", {})
# Check for admin, maintain, or push rights
if perms.get("admin") or perms.get("maintain") or perms.get("push"):
writers.add(col["login"])
print(f"Found {len(writers)} users with write access.")
return writers
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()
# Fetch commits
commits = github_api_get("commits", params={"since": since_date, "per_page": 100})
author_counts = Counter()
for commit in commits:
# commit['author'] contains the GitHub user object (can be None if not linked)
if commit.get("author") and "login" in commit["author"]:
author_counts[commit["author"]["login"]] += 1
top_users = [user for user, _ in author_counts.most_common(limit)]
print(f"Found {len(top_users)} top contributors in the last {days} days.")
return set(top_users)
def load_existing_permissions():
if os.path.exists(FILE_NAME):
try:
with open(FILE_NAME, "r") as f:
return json.load(f)
except json.JSONDecodeError:
print(f"Warning: {FILE_NAME} is invalid JSON. Starting fresh.")
return {}
def sort_permissions_file():
"""Sort the existing CI permissions file alphabetically and exit."""
if not os.path.exists(FILE_NAME):
print(f"{FILE_NAME} not found. Nothing to sort.")
return
old_permissions = load_existing_permissions()
sorted_permissions = dict(sorted(old_permissions.items()))
with open(FILE_NAME, "w") as f:
json.dump(sorted_permissions, f, indent=4)
f.write("\n")
print(f"Sorted {FILE_NAME}. Total users: {len(sorted_permissions)}")
def main():
parser = argparse.ArgumentParser(description="Update or sort CI permissions.")
parser.add_argument(
"--sort-only",
action="store_true",
help="Only sort CI_PERMISSIONS.json alphabetically without fetching data.",
)
args = parser.parse_args()
if args.sort_only:
sort_permissions_file()
return
gh_token = os.getenv("GH_TOKEN")
if not gh_token:
raise ValueError("Error: GH_TOKEN environment variable is not set.")
global HEADERS
HEADERS = {
"Authorization": f"Bearer {gh_token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
# Gather Data
try:
write_access_users = get_write_access_users()
except Exception as e:
print(f"Warning: Could not fetch collaborators (check token scope). Error: {e}")
write_access_users = set()
top_contributors = get_top_contributors(days=120, limit=50)
old_permissions = load_existing_permissions()
new_permissions = {}
# Rule 1: Add Top 50 Contributors
for user in top_contributors:
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",
}
# Rule 2: Process Existing Users (Merge Logic)
for user, config in old_permissions.items():
if user in new_permissions:
# Already handled by Rule 1 or 2
continue
old_reason = config.get("reason", "")
# If they fell off the top contributor list
if old_reason in ["top contributor"]:
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",
}
else:
# Preserve custom overrides
new_permissions[user] = config
# Save and Sort
# Sorting keys for cleaner diffs
sorted_permissions = dict(sorted(new_permissions.items()))
with open(FILE_NAME, "w") as f:
json.dump(sorted_permissions, f, indent=4)
f.write("\n") # Add trailing newline
print(f"Successfully updated {FILE_NAME}. Total users: {len(sorted_permissions)}")
if __name__ == "__main__":
main()
@@ -0,0 +1,331 @@
name: Build and Publish Multi-Arch Docker Images
# Reusable workflow: builds CUDA 12 + CUDA 13 images for amd64 and arm64,
# then creates multi-arch manifests with caller-specified tags.
on:
workflow_call:
inputs:
docker_target:
description: "Dockerfile target stage (framework or runtime)"
required: true
type: string
sgl_version:
description: "Version string passed as SGL_VERSION build arg (empty to skip)"
required: false
type: string
default: ""
extra_build_args:
description: "Additional --build-arg flags appended to docker buildx build"
required: false
type: string
default: ""
checkout_ref:
description: "Git ref to checkout (empty for default)"
required: false
type: string
default: ""
tag_config:
description: 'JSON array of {"cuda":"cu129|cu130","tags":["tag1","tag2"]}. Tags support {version} substitution.'
required: true
type: string
use_environment:
description: "GitHub environment name (e.g. prod) or empty for none"
required: false
type: string
default: ""
image_repo:
description: "Docker Hub repo to push to (e.g. lmsysorg/sglang-staging for testing)"
required: false
type: string
default: "lmsysorg/sglang"
jobs:
build-x86:
if: github.repository == 'sgl-project/sglang'
environment: ${{ inputs.use_environment || null }}
runs-on: x64-docker-build-node
env:
TAG_CONFIG: ${{ inputs.tag_config }}
SGL_VERSION: ${{ inputs.sgl_version }}
IMAGE_REPO: ${{ inputs.image_repo }}
outputs:
digest-cu129: ${{ steps.build-cu129.outputs.digest }}
digest-cu130: ${{ steps.build-cu130.outputs.digest }}
steps:
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache
- name: Cleanup workspace (remove root-owned files from prior runs)
run: sudo rm -rf "$GITHUB_WORKSPACE"/* || true
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Compute Docker build metadata args
run: |
set -euo pipefail
BUILD_COMMIT="$(git rev-parse HEAD)"
BUILD_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
for CUDA_VARIANT in cu129 cu130; do
python3 scripts/ci/utils/docker_build_metadata_args.py \
--cuda "${CUDA_VARIANT}" \
--tag-config "${TAG_CONFIG}" \
--image-repo "${IMAGE_REPO}" \
--sgl-version "${SGL_VERSION}" \
--build-commit "${BUILD_COMMIT}" \
--build-url "${BUILD_URL}" \
> "/tmp/docker-metadata-${CUDA_VARIANT}.args"
done
- name: Free disk space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
docker-images: true
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: Prune Docker to reclaim disk space
run: |
docker buildx prune --filter "until=72h" -f
docker system prune -af --filter "until=72h"
docker volume prune -af
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push AMD64 image (CUDA 12)
id: build-cu129
run: |
VERSION_ARG=""
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu129.args
docker buildx build \
--target ${{ inputs.docker_target }} \
--platform linux/amd64 \
--output type=image,name=${{ inputs.image_repo }},push-by-digest=true,name-canonical=true,push=true \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=12.9.1 \
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=0 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu129.json \
--no-cache \
.
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata-cu129.json'))['containerimage.digest'])")
echo "Pushed digest: ${DIGEST}"
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
- name: Build and push AMD64 image (CUDA 13)
id: build-cu130
run: |
VERSION_ARG=""
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu130.args
docker buildx build \
--target ${{ inputs.docker_target }} \
--platform linux/amd64 \
--output type=image,name=${{ inputs.image_repo }},push-by-digest=true,name-canonical=true,push=true \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=13.0.1 \
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=0 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu130.json \
--no-cache \
.
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata-cu130.json'))['containerimage.digest'])")
echo "Pushed digest: ${DIGEST}"
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
build-arm64:
if: github.repository == 'sgl-project/sglang'
environment: ${{ inputs.use_environment || null }}
runs-on: arm-docker-build-node
env:
TAG_CONFIG: ${{ inputs.tag_config }}
SGL_VERSION: ${{ inputs.sgl_version }}
IMAGE_REPO: ${{ inputs.image_repo }}
outputs:
digest-cu129: ${{ steps.build-cu129.outputs.digest }}
digest-cu130: ${{ steps.build-cu130.outputs.digest }}
steps:
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache
- name: Cleanup workspace (remove root-owned files from prior runs)
run: sudo rm -rf "$GITHUB_WORKSPACE"/* || true
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Compute Docker build metadata args
run: |
set -euo pipefail
BUILD_COMMIT="$(git rev-parse HEAD)"
BUILD_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
for CUDA_VARIANT in cu129 cu130; do
python3 scripts/ci/utils/docker_build_metadata_args.py \
--cuda "${CUDA_VARIANT}" \
--tag-config "${TAG_CONFIG}" \
--image-repo "${IMAGE_REPO}" \
--sgl-version "${SGL_VERSION}" \
--build-commit "${BUILD_COMMIT}" \
--build-url "${BUILD_URL}" \
> "/tmp/docker-metadata-${CUDA_VARIANT}.args"
done
- name: Prune Docker to reclaim disk space
run: |
docker buildx prune --filter "until=72h" -f
docker system prune -af --filter "until=72h"
docker volume prune -af
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push ARM64 image (CUDA 12)
id: build-cu129
run: |
VERSION_ARG=""
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu129.args
docker buildx build \
--target ${{ inputs.docker_target }} \
--platform linux/arm64 \
--output type=image,name=${{ inputs.image_repo }},push-by-digest=true,name-canonical=true,push=true \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=12.9.1 \
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=1 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu129.json \
--no-cache \
.
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata-cu129.json'))['containerimage.digest'])")
echo "Pushed digest: ${DIGEST}"
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
- name: Build and push ARM64 image (CUDA 13)
id: build-cu130
run: |
VERSION_ARG=""
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu130.args
docker buildx build \
--target ${{ inputs.docker_target }} \
--platform linux/arm64 \
--output type=image,name=${{ inputs.image_repo }},push-by-digest=true,name-canonical=true,push=true \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=13.0.1 \
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=1 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu130.json \
--no-cache \
.
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata-cu130.json'))['containerimage.digest'])")
echo "Pushed digest: ${DIGEST}"
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
create-manifests:
runs-on: ubuntu-latest
needs: [build-x86, build-arm64]
if: github.repository == 'sgl-project/sglang'
environment: ${{ inputs.use_environment || null }}
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create multi-arch manifests
env:
TAG_CONFIG: ${{ inputs.tag_config }}
SGL_VERSION: ${{ inputs.sgl_version }}
IMAGE_REPO: ${{ inputs.image_repo }}
X86_CU129: ${{ needs.build-x86.outputs.digest-cu129 }}
X86_CU130: ${{ needs.build-x86.outputs.digest-cu130 }}
ARM64_CU129: ${{ needs.build-arm64.outputs.digest-cu129 }}
ARM64_CU130: ${{ needs.build-arm64.outputs.digest-cu130 }}
SHORT_SHA: ${{ github.sha }}
run: |
echo "${TAG_CONFIG}" | jq -c '.[]' | while read -r entry; do
CUDA=$(echo "${entry}" | jq -r '.cuda')
if [ "${CUDA}" = "cu129" ]; then
X86_DIGEST="${X86_CU129}"
ARM64_DIGEST="${ARM64_CU129}"
else
X86_DIGEST="${X86_CU130}"
ARM64_DIGEST="${ARM64_CU130}"
fi
TAG_ARGS=""
for tag in $(echo "${entry}" | jq -r '.tags[]'); do
# Substitute template variables
tag=$(echo "${tag}" | sed "s/{version}/${SGL_VERSION}/g")
tag=$(echo "${tag}" | sed "s/{date}/$(date +%Y%m%d)/g")
tag=$(echo "${tag}" | sed "s/{short_sha}/${SHORT_SHA:0:8}/g")
TAG_ARGS="${TAG_ARGS} -t ${IMAGE_REPO}:${tag}"
done
docker buildx imagetools create \
${TAG_ARGS} \
${IMAGE_REPO}@${X86_DIGEST} \
${IMAGE_REPO}@${ARM64_DIGEST}
echo "Published:${TAG_ARGS}"
done
@@ -0,0 +1,78 @@
name: Cleanup Old Nightly Docker Tags
# Reusable workflow: deletes old nightly Docker Hub tags, keeping the most recent ones.
# Can also be triggered manually to clean up tags on demand.
on:
workflow_call:
inputs:
tag_prefixes:
description: 'JSON array of tag prefixes to clean up (e.g. ["nightly-dev", "nightly-dev-cu13"])'
required: true
type: string
keep_count:
description: "Number of most recent tags to keep per prefix"
required: false
type: number
default: 14
image_repo:
description: "Docker Hub repo to clean up (e.g. lmsysorg/sglang-staging for testing)"
required: false
type: string
default: "lmsysorg/sglang"
workflow_dispatch:
inputs:
tag_prefixes:
description: 'JSON array of tag prefixes to clean up (e.g. ["nightly-dev", "nightly-dev-cu13"])'
required: true
type: string
keep_count:
description: "Number of most recent tags to keep per prefix"
required: false
type: number
default: 14
image_repo:
description: "Docker Hub repo to clean up (e.g. lmsysorg/sglang-staging for testing)"
required: false
type: string
default: "lmsysorg/sglang"
jobs:
cleanup:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
steps:
- name: Cleanup old nightly tags
env:
TAG_PREFIXES: ${{ inputs.tag_prefixes }}
KEEP_COUNT: ${{ inputs.keep_count }}
IMAGE_REPO: ${{ inputs.image_repo }}
run: |
TOKEN=$(curl -s -H "Content-Type: application/json" \
-X POST -d '{"username": "${{ secrets.DOCKERHUB_USERNAME }}", "password": "${{ secrets.DOCKERHUB_TOKEN }}"}' \
https://hub.docker.com/v2/users/login/ | jq -r .token)
TAGS_RESPONSE=$(curl -s -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/${IMAGE_REPO}/tags/?page_size=100")
echo "${TAG_PREFIXES}" | jq -r '.[]' | while read -r PREFIX; do
echo "--- Checking prefix: ${PREFIX} ---"
TAGS=$(echo "$TAGS_RESPONSE" | jq -r \
--arg prefix "${PREFIX}" \
'.results[] | select(.name | test("^\($prefix)-[0-9]")) | "\(.last_updated)|\(.name)"' \
| sort -r | cut -d'|' -f2)
TAG_COUNT=$(echo "$TAGS" | grep -c . || true)
if [ "$TAG_COUNT" -gt "$KEEP_COUNT" ]; then
echo "Found $TAG_COUNT tags, keeping only the $KEEP_COUNT most recent"
TAGS_TO_DELETE=$(echo "$TAGS" | tail -n +$((KEEP_COUNT + 1)))
for tag in $TAGS_TO_DELETE; do
echo "Deleting tag: $tag"
curl -X DELETE -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/${IMAGE_REPO}/tags/$tag/"
done
else
echo "Only $TAG_COUNT tags found, no cleanup needed"
fi
done
@@ -0,0 +1,223 @@
name: Check Changes
on:
workflow_call:
inputs:
git_ref:
type: string
default: ''
run_all_tests:
type: boolean
default: false
force_continue_on_error:
type: boolean
default: false
pr_test_yml:
description: 'Workflow YAML whose stage `run_timeout_minutes` drives partition sizing.'
type: string
default: '.github/workflows/pr-test.yml'
outputs:
main_package:
value: ${{ jobs.run.outputs.main_package }}
sgl_kernel:
value: ${{ jobs.run.outputs.sgl_kernel }}
jit_kernel:
value: ${{ jobs.run.outputs.jit_kernel }}
multimodal_gen:
value: ${{ jobs.run.outputs.multimodal_gen }}
partitions:
value: ${{ jobs.run.outputs.partitions }}
partition_model_sha:
value: ${{ jobs.run.outputs.partition_model_sha }}
runs_on_map:
value: ${{ jobs.run.outputs.runs_on_map }}
enable_retry:
value: ${{ jobs.run.outputs.enable_retry }}
continue_on_error:
value: ${{ jobs.run.outputs.continue_on_error }}
jobs:
run:
name: check-changes
runs-on: ubuntu-latest
outputs:
main_package: ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests }}
sgl_kernel: ${{ steps.filter.outputs.sgl_kernel }}
jit_kernel: ${{ steps.filter.outputs.jit_kernel || steps.run-mode.outputs.run_all_tests }}
multimodal_gen: ${{ steps.filter.outputs.multimodal_gen || steps.run-mode.outputs.run_all_tests }}
partitions: ${{ steps.partitions.outputs.partitions }}
partition_model_sha: ${{ steps.partition-model-sha.outputs.sha }}
runs_on_map: ${{ steps.runner-map.outputs.runs_on_map }}
enable_retry: ${{ steps.set-retry.outputs.enable_retry }}
continue_on_error: ${{ steps.set-continue-on-error.outputs.continue_on_error }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-maintenance
- name: Determine run mode
id: run-mode
run: |
# Run all tests for scheduled runs and workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.git_ref
if [[ "${{ github.event_name }}" == "schedule" || "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (schedule=${{ github.event_name == 'schedule' }}, run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- ".github/workflows/pr-test.yml"
- ".github/workflows/pr-gate.yml"
- ".github/actions/**"
- "python/pyproject.toml"
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "scripts/ci/cuda/*"
- "scripts/ci/utils/*"
- "test/**/!(*.md)"
multimodal_gen:
- ".github/workflows/pr-test.yml"
- ".github/workflows/pr-test-multimodal-gen.yml"
- "python/pyproject.toml"
- "python/sglang/multimodal_gen/**/!(*.md|*.ipynb)"
- "python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/**"
- "python/sglang/srt/observability/**"
- "python/sglang/jit_kernel/**"
- "test/registered/jit/diffusion/**"
- "test/registered/jit/benchmark/diffusion/**"
- "python/sglang/cli/**"
jit_kernel:
- ".github/workflows/pr-test.yml"
- ".github/workflows/pr-test-jit-kernel.yml"
- "python/pyproject.toml"
- "python/sglang/jit_kernel/**"
- "test/registered/jit/**"
sgl_kernel:
# Intentionally excludes ".github/workflows/pr-test-sgl-kernel.yml" —
# see API-side detector below for rationale.
- "sgl-kernel/**/!(*.md|THIRDPARTYNOTICES.txt|LICENSE)"
- name: Determine full-parallel mode
id: parallel-mode
run: |
# `full=true` lifts the matrix-fanout throttle so each suite's
# max_parallel = size. Conditions:
# 1. Scheduled cron run.
# 2. run_all_tests run (manual full dispatch / release) -- a full
# run should mirror the cron's parallelism, not just its test set.
# 3. pull_request event with the `high priority` label.
FULL=false
if [[ "${{ github.event_name }}" == "schedule" ]]; then
FULL=true
echo "Scheduled run -> full parallelism"
elif [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
FULL=true
echo "run_all_tests -> full parallelism"
elif [[ "${{ github.event_name }}" == "pull_request" && "${{ contains(github.event.pull_request.labels.*.name, 'high priority') }}" == "true" ]]; then
FULL=true
echo "high priority PR -> full parallelism"
fi
echo "full=$FULL" >> "$GITHUB_OUTPUT"
- name: Resolve sglang-ci-stats SHA
id: partition-model-sha
env:
GH_TOKEN: ${{ github.token }}
run: |
# Pin all shards to one immutable commit so dispatch and every
# runtime LPT use the same model snapshot. Soft fail -> static.
SHA=$(gh api repos/sgl-project/sglang-ci-stats/commits/main --jq '.sha' 2>/dev/null || true)
if [[ -n "$SHA" ]]; then
echo "Pinned sglang-ci-stats@$SHA"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
else
echo "::warning::Could not resolve sglang-ci-stats SHA; using in-source est_time"
echo "sha=" >> "$GITHUB_OUTPUT"
fi
- name: Fetch live partition model
if: steps.partition-model-sha.outputs.sha != ''
run: |
# SHA resolved -> require fetch (curl --retry). Soft fallback
# would risk cross-shard LPT divergence on transient curl flake.
rm -f /tmp/partition-model.json
URL="https://raw.githubusercontent.com/sgl-project/sglang-ci-stats/${{ steps.partition-model-sha.outputs.sha }}/model.json"
curl --fail --silent --show-error --max-time 15 --retry 3 --retry-delay 2 \
"$URL" -o /tmp/partition-model.json
echo "Fetched partition-model.json ($(wc -c < /tmp/partition-model.json) bytes)"
- name: Compute partitions
id: partitions
run: |
# Emit a single JSON output `partitions` keyed by suite name with
# {size, arr, max_parallel} fields per suite. Replaces the prior
# full/low max-parallel presets; `--full-parallel` keeps the
# `high priority` PR / scheduled cron escape hatch.
# See scripts/ci/utils/compute_partitions.py.
python3 scripts/ci/utils/compute_partitions.py \
--full-parallel ${{ steps.parallel-mode.outputs.full }} \
--partition-model-file /tmp/partition-model.json \
--pr-test-yml ${{ inputs.pr_test_yml }} \
>> "$GITHUB_OUTPUT"
- name: Set B200 runner tag
id: set-runner
run: |
# Use kernel-build runner only when sgl_kernel changes are detected.
sgl_kernel="${{ steps.filter.outputs.sgl_kernel }}"
if [[ "$sgl_kernel" == "true" ]]; then
echo "b200_runner=4-gpu-b200-kernel" >> $GITHUB_OUTPUT
else
echo "b200_runner=4-gpu-b200" >> $GITHUB_OUTPUT
fi
- name: Build runs_on_map (resolves $b200_runner sentinel)
id: runner-map
run: |
python3 scripts/ci/runner_configs.py --map \
'${{ steps.set-runner.outputs.b200_runner }}' \
>> "$GITHUB_OUTPUT"
- name: Enable retry for CI
id: set-retry
run: |
echo "enable_retry=true" >> $GITHUB_OUTPUT
echo "Retry logic enabled for CI"
- name: Set continue-on-error for full test runs
id: set-continue-on-error
run: |
if [[ "${{ steps.run-mode.outputs.run_all_tests }}" == "true" || "${{ inputs.force_continue_on_error }}" == "true" ]]; then
echo "continue_on_error=true" >> $GITHUB_OUTPUT
echo "Full test run or force flag detected, enabling continue-on-error to run all tests"
else
echo "continue_on_error=false" >> $GITHUB_OUTPUT
echo "Filtered run, continue-on-error disabled"
fi
- name: Show filter results in summary (table)
run: |
{
echo "## Change Detection"
echo ""
echo "| Component | Changed |"
echo "|-------------------|---------|"
echo "| main_package | ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests }} |"
echo "| sgl_kernel | ${{ steps.filter.outputs.sgl_kernel }} |"
echo "| jit_kernel | ${{ steps.filter.outputs.jit_kernel || steps.run-mode.outputs.run_all_tests }} |"
echo "| multimodal_gen | ${{ steps.filter.outputs.multimodal_gen || steps.run-mode.outputs.run_all_tests }} |"
echo "| b200_runner | ${{ steps.set-runner.outputs.b200_runner }} |"
echo "| enable_retry | ${{ steps.set-retry.outputs.enable_retry }} |"
echo "| continue_on_error | ${{ steps.set-continue-on-error.outputs.continue_on_error }} |"
} >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,118 @@
name: PR Test - Build sgl-kernel Wheels
# Reusable workflow that builds the sgl-kernel CUDA wheels for one
# architecture / runner pool. Called twice from pr-test.yml: once for the
# x86_64 runner pool, once for arm64. All architecture-specific values
# (runner label, display name, artifact name suffix) are passed in via
# inputs.
on:
workflow_call:
inputs:
runs_on:
description: 'Runner label (e.g. x64-kernel-build-node, arm-kernel-build-node)'
type: string
required: true
job_display_name:
description: 'Display name shown in the Actions UI (e.g. "Build Wheel", "Build Wheel Arm")'
type: string
default: 'Build Wheel'
arch_suffix:
description: "Suffix appended to the upload-artifact name. '' for x86, '-aarch64' for arm."
type: string
default: ''
git_ref:
type: string
default: ''
skip_pr_test_health_check:
description: 'Forwarded from the caller workflow input of the same name. Not consumed inside this reusable workflow today, but mirrored so the resulting env context matches the inline-job baseline.'
type: boolean
default: false
# Mirror the caller workflow's top-level env block. GitHub Actions does NOT
# automatically inherit workflow-level env vars across the workflow_call
# boundary, so anything pr-test.yml defines must be redeclared here for the
# reusable jobs to see the same context. Most of these are not read by the
# wheel-build path, but PR_TEST_BYPASS_MAINTENANCE_ON_MAIN is consumed by the
# check-maintenance composite action and would otherwise be unset.
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
SKIP_PR_TEST_HEALTH_CHECK: ${{ inputs.skip_pr_test_health_check && 'true' || 'false' }}
FORCE_REBUILD_DEEPEP: '1'
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
USE_VENV: false
jobs:
build:
runs-on: ${{ inputs.runs_on }}
timeout-minutes: 240
strategy:
matrix:
include:
- python-version: "3.10"
cuda-version: "13.0"
name: ${{ inputs.job_display_name }}
steps:
- name: Cleanup
run: |
if [ -d "$GITHUB_WORKSPACE" ]; then
sudo rm -rf "$GITHUB_WORKSPACE"/* || true
else
echo "$GITHUB_WORKSPACE does not exist, nothing to clean"
fi
- uses: actions/checkout@v4
with:
submodules: "recursive"
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-maintenance
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Free Docker disk space
run: |
set -x
# `-f` only (no `-a`): drops dangling <none>:<none> layers from
# prior runs but keeps the tagged sgl-kernel-deps:* base images.
docker image prune -f
# Reap orphan buildx builders from interrupted prior runs. Must
# `buildx rm` (not `volume rm`) — the volume is held open by the
# buildkit helper container; removing the builder frees both.
# `sgl-kernel-builder` and `default` are skipped on purpose.
for b in $(docker buildx ls --format '{{.Name}}' | grep -E '^builder-' || true); do
echo "Removing orphan buildx builder: $b"
docker buildx rm "$b" || true
done
# Trim the persistent local buildx cache (--cache-to mode=max grows
# unbounded). Cache-hit imports rewrite the blob, so mtime tracks
# "last carried forward by a build" — 14d is well past the freshest
# CI cycle.
if [ -d "$HOME/.cache/sgl-kernel/buildx/blobs" ]; then
find "$HOME/.cache/sgl-kernel/buildx/blobs" -type f -mtime +14 -delete
fi
df -h /
- name: Build wheel for Python ${{ matrix.python-version }} and CUDA ${{ matrix.cuda-version }}
run: |
cd sgl-kernel
./build.sh "${{ matrix.python-version }}" "${{ matrix.cuda-version }}"
env:
USE_CCACHE: 1
- name: Verify wheel artifacts
run: |
ls -alh sgl-kernel/dist
ls -alh sgl-kernel/dist/*.whl
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: wheel-python${{ matrix.python-version }}-cuda${{ matrix.cuda-version }}${{ inputs.arch_suffix }}
path: sgl-kernel/dist/*
if-no-files-found: error
+184
View File
@@ -0,0 +1,184 @@
name: PR Test Stage
# Reusable workflow for one CUDA test stage. Caller pr-test.yml forwards
# `check_changes` = toJson(needs.check-changes.outputs) and
# `caller_inputs` = toJson(inputs) as bundles. `partitions` is forwarded
# separately to keep matrix expressions single-fromJson — its value is itself
# a JSON string, so reading it via `check_changes` would need double fromJson,
# and matrix can't use step-resolved values. base-a-test-cpu stays inline in
# pr-test.yml (bespoke uv pip / protoc / rust-cache install).
on:
workflow_call:
inputs:
self_name:
description: 'Caller job key; used for partitions[suite] lookup.'
type: string
required: true
runner_config:
description: 'Key in scripts/ci/runner_configs.yml. Resolves install script, artifact version, install timeout, runs-on label, and rdma_devices.'
type: string
required: true
check_changes:
description: 'toJson(needs.check-changes.outputs). Read via fromJson(...).main_package / sgl_kernel / continue_on_error / runs_on_map etc.'
type: string
required: true
caller_inputs:
description: 'toJson(inputs) from pr-test.yml. Read via fromJson(...).git_ref / skip_pr_test_health_check / test_parallel_dispatch.'
type: string
required: true
partitions:
description: 'check-changes.outputs.partitions raw — kept separate to avoid double-fromJson in matrix expressions.'
type: string
required: true
run_timeout_minutes:
description: 'timeout-minutes for the Run test step. Required so compute_partitions.py can read it from pr-test.yml without a duplicated default constant.'
type: string
required: true
timeout_per_file:
description: 'run_suite.py --timeout-per-file value (empty = unset).'
type: string
default: ''
warmup_deep_gemm_models:
description: 'Space-separated `model:gpus` list; empty skips DeepGEMM warmup.'
type: string
default: ''
warmup_server_models:
description: 'Space-separated `model:gpus` list; empty skips server-CUDA-graph warmup.'
type: string
default: ''
warmup_timeout_minutes:
description: 'timeout-minutes for each warmup step. Bump for stages with large cold-cache model lists.'
type: string
default: '25'
extra_pytest_path:
description: 'pytest path to run after the suite (b200 FA4 jit_kernel tests). Empty skips.'
type: string
default: ''
# Mirror pr-test.yml top-level env. Reusable workflows do NOT inherit caller's
# workflow-level env across the workflow_call boundary, so anything pr-test.yml
# defines must be redeclared here for the called job to see the same context.
env:
SGLANG_IS_IN_CI: true
# base-a runs the fwd-occupancy sanity kit; async-assert probes add GPU work
# and skew the occupancy measurement, so keep them off in that stage.
SGLANG_ENABLE_ASYNC_ASSERT: ${{ startsWith(inputs.self_name, 'base-a') && 'false' || 'true' }}
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
SKIP_PR_TEST_HEALTH_CHECK: ${{ (fromJson(inputs.caller_inputs).skip_pr_test_health_check || fromJson(inputs.caller_inputs).test_parallel_dispatch || fromJson(inputs.caller_inputs).run_all_tests) && 'true' || 'false' }}
FORCE_REBUILD_DEEPEP: '1'
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
USE_VENV: false
jobs:
run:
name: ${{ inputs.self_name }} (${{ matrix.partition }})
# Runs on schedule / parallel-dispatch / non-failed PR with main_package or sgl_kernel changes.
if: |
always() &&
((github.event_name == 'schedule' || fromJson(inputs.caller_inputs).test_parallel_dispatch == true) || (!failure() && !cancelled())) &&
(fromJson(inputs.check_changes).main_package == 'true' || fromJson(inputs.check_changes).sgl_kernel == 'true')
# runs-on resolved from runs_on_map; check-changes already substituted
# $b200_runner (see runner_configs.py --map). rdma_devices is exported
# below in a setup step via $GITHUB_ENV.
runs-on: ${{ fromJson(fromJson(inputs.check_changes).runs_on_map)[inputs.runner_config] }}
timeout-minutes: 240
strategy:
fail-fast: false
max-parallel: ${{ fromJson(inputs.partitions)[inputs.self_name].max_parallel }}
matrix:
partition: ${{ fromJson(inputs.partitions)[inputs.self_name].arr }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ fromJson(inputs.caller_inputs).git_ref || github.sha }}
- name: Resolve runner_config
id: rc
run: python3 scripts/ci/runner_configs.py '${{ inputs.runner_config }}' >> "$GITHUB_OUTPUT"
- name: Export rdma_devices to job env
run: echo "SGLANG_CI_RDMA_ALL_DEVICES=${{ steps.rc.outputs.rdma_devices || '' }}" >> "$GITHUB_ENV"
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts (v4)
if: ${{ fromJson(inputs.check_changes).sgl_kernel == 'true' && steps.rc.outputs.artifact_version == 'v4' }}
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Download artifacts (v6)
if: ${{ fromJson(inputs.check_changes).sgl_kernel == 'true' && steps.rc.outputs.artifact_version == 'v6' }}
uses: actions/download-artifact@v6
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: ${{ fromJson(steps.rc.outputs.install_timeout) }}
env:
GRACE_BLACKWELL: ${{ steps.rc.outputs.grace_blackwell || '0' }}
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ fromJson(inputs.check_changes).sgl_kernel }} bash ${{ steps.rc.outputs.install }}
- name: Warmup DeepGEMM JIT Compilation
if: inputs.warmup_deep_gemm_models != ''
timeout-minutes: ${{ fromJson(inputs.warmup_timeout_minutes) }}
run: |
# Activate venv if available (GITHUB_ENV may have failed to propagate)
[ -f "${SGLANG_CI_VENV_PATH:-/dev/null}/bin/activate" ] && source "${SGLANG_CI_VENV_PATH}/bin/activate"
[ -f "${SGLANG_CI_VENV_PATH:-/dev/null}/env.sh" ] && source "${SGLANG_CI_VENV_PATH}/env.sh"
python3 scripts/ci/cuda/warmup_deep_gemm.py ${{ inputs.warmup_deep_gemm_models }}
- name: Warmup Server CUDA Graphs
if: inputs.warmup_server_models != ''
timeout-minutes: ${{ fromJson(inputs.warmup_timeout_minutes) }}
run: |
[ -f "${SGLANG_CI_VENV_PATH:-/dev/null}/bin/activate" ] && source "${SGLANG_CI_VENV_PATH}/bin/activate"
[ -f "${SGLANG_CI_VENV_PATH:-/dev/null}/env.sh" ] && source "${SGLANG_CI_VENV_PATH}/env.sh"
python3 scripts/ci/cuda/warmup_server.py ${{ inputs.warmup_server_models }}
- name: Fetch live partition model
if: fromJson(inputs.check_changes).partition_model_sha != ''
run: |
# SHA resolved by check-changes -> require fetch (curl --retry)
# so all shards stay on the same snapshot.
rm -f /tmp/partition-model.json
URL="https://raw.githubusercontent.com/sgl-project/sglang-ci-stats/${{ fromJson(inputs.check_changes).partition_model_sha }}/model.json"
curl --fail --silent --show-error --max-time 15 --retry 3 --retry-delay 2 \
"$URL" -o /tmp/partition-model.json
- name: Run test
timeout-minutes: ${{ fromJson(inputs.run_timeout_minutes) }}
env:
CONTINUE_ON_ERROR_FLAG: ${{ fromJson(inputs.check_changes).continue_on_error == 'true' && '--continue-on-error' || '' }}
run: |
cd test
python3 run_suite.py --hw cuda --suite ${{ inputs.self_name }} \
--auto-partition-id ${{ matrix.partition }} \
--auto-partition-size ${{ fromJson(inputs.partitions)[inputs.self_name].size }} \
--partition-model-file /tmp/partition-model.json \
${{ inputs.timeout_per_file && format('--timeout-per-file {0}', inputs.timeout_per_file) || '' }} \
$CONTINUE_ON_ERROR_FLAG
- name: Run extra pytest
if: inputs.extra_pytest_path != ''
timeout-minutes: 10
run: python3 -m pytest -q ${{ inputs.extra_pytest_path }}
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.partition }}
tracker-issue: "26340"
bot-token: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
- name: Cleanup venv
if: always()
run: bash scripts/ci/cuda/ci_cleanup_venv.sh
+170
View File
@@ -0,0 +1,170 @@
name: AMD AITER Scout
on:
schedule:
- cron: '0 20 * * 1' # Monday 20:00 UTC
# - cron: '0 20 * * 4' # Thursday 20:00 UTC (temporarily disabled)
workflow_dispatch:
inputs:
aiter_ref:
description: 'AITER git ref (branch, tag, or SHA). Default: main (latest commit)'
required: false
type: string
default: 'main'
job_filter:
description: 'Comma-separated workflows to run: nightly-amd, nightly-amd-rocm720, pr-test-amd, pr-test-amd-rocm720. Default: all'
required: false
type: string
default: 'all'
continue_on_error:
description: 'Continue running other workflows even if one fails'
required: false
type: boolean
default: true
permissions:
actions: write
contents: read
issues: read
pull-requests: read
concurrency:
group: amd-aiter-scout-${{ github.run_id }}
cancel-in-progress: true
jobs:
resolve-aiter:
runs-on: ubuntu-latest
outputs:
aiter_sha: ${{ steps.resolve.outputs.sha }}
run_nightly_amd: ${{ steps.parse.outputs.run_nightly_amd }}
run_nightly_amd_rocm720: ${{ steps.parse.outputs.run_nightly_amd_rocm720 }}
run_pr_test_amd: ${{ steps.parse.outputs.run_pr_test_amd }}
run_pr_test_amd_rocm720: ${{ steps.parse.outputs.run_pr_test_amd_rocm720 }}
steps:
- name: Resolve AITER commit
id: resolve
run: |
REF="${{ inputs.aiter_ref || 'main' }}"
echo "Resolving AITER ref: ${REF}"
SHA=$(git ls-remote https://github.com/ROCm/aiter.git "refs/heads/${REF}" | head -1 | cut -f1)
if [ -z "$SHA" ]; then
SHA=$(git ls-remote https://github.com/ROCm/aiter.git "refs/tags/${REF}" | head -1 | cut -f1)
fi
if [ -z "$SHA" ]; then
SHA=$(git ls-remote https://github.com/ROCm/aiter.git "${REF}" | head -1 | cut -f1)
fi
if [ -z "$SHA" ]; then
SHA="${REF}"
fi
echo "sha=${SHA}" >> $GITHUB_OUTPUT
echo "### AITER Ref Resolution" >> $GITHUB_STEP_SUMMARY
echo "- **Requested ref:** \`${REF}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Resolved SHA:** \`${SHA}\`" >> $GITHUB_STEP_SUMMARY
echo "- **AITER commit:** https://github.com/ROCm/aiter/commit/${SHA}" >> $GITHUB_STEP_SUMMARY
- name: Parse job filter
id: parse
run: |
FILTER="${{ inputs.job_filter || 'all' }}"
echo "Job filter: ${FILTER}"
if [[ "$FILTER" == "all" ]]; then
echo "run_nightly_amd=true" >> $GITHUB_OUTPUT
echo "run_nightly_amd_rocm720=true" >> $GITHUB_OUTPUT
echo "run_pr_test_amd=true" >> $GITHUB_OUTPUT
echo "run_pr_test_amd_rocm720=true" >> $GITHUB_OUTPUT
else
# Wrap with commas for exact substring matching (avoids "nightly-amd" matching "nightly-amd-rocm720")
PADDED=",${FILTER// /},"
echo "run_nightly_amd=$(echo "$PADDED" | grep -q ',nightly-amd,' && echo true || echo false)" >> $GITHUB_OUTPUT
echo "run_nightly_amd_rocm720=$(echo "$PADDED" | grep -q ',nightly-amd-rocm720,' && echo true || echo false)" >> $GITHUB_OUTPUT
echo "run_pr_test_amd=$(echo "$PADDED" | grep -q ',pr-test-amd,' && echo true || echo false)" >> $GITHUB_OUTPUT
echo "run_pr_test_amd_rocm720=$(echo "$PADDED" | grep -q ',pr-test-amd-rocm720,' && echo true || echo false)" >> $GITHUB_OUTPUT
fi
echo "### Job Filter" >> $GITHUB_STEP_SUMMARY
echo "- **Filter:** \`${FILTER}\`" >> $GITHUB_STEP_SUMMARY
call-nightly-amd:
if: needs.resolve-aiter.outputs.run_nightly_amd == 'true'
needs: resolve-aiter
uses: ./.github/workflows/nightly-test-amd.yml
secrets: inherit
with:
ref: amd/aiter-ci
aiter_ref: ${{ needs.resolve-aiter.outputs.aiter_sha }}
job_filter: 'all'
continue_on_error: ${{ inputs.continue_on_error == '' && true || inputs.continue_on_error }}
call-nightly-amd-rocm720:
if: needs.resolve-aiter.outputs.run_nightly_amd_rocm720 == 'true'
needs: resolve-aiter
uses: ./.github/workflows/nightly-test-amd-rocm720.yml
secrets: inherit
with:
ref: amd/aiter-ci
aiter_ref: ${{ needs.resolve-aiter.outputs.aiter_sha }}
job_filter: 'all'
continue_on_error: ${{ inputs.continue_on_error == '' && true || inputs.continue_on_error }}
call-pr-test-amd:
if: needs.resolve-aiter.outputs.run_pr_test_amd == 'true'
needs: resolve-aiter
uses: ./.github/workflows/pr-test-amd.yml
secrets: inherit
with:
ref: amd/aiter-ci
run_all_tests: true
aiter_ref: ${{ needs.resolve-aiter.outputs.aiter_sha }}
continue_on_error: ${{ inputs.continue_on_error == '' && true || inputs.continue_on_error }}
call-pr-test-amd-rocm720:
if: needs.resolve-aiter.outputs.run_pr_test_amd_rocm720 == 'true'
needs: resolve-aiter
uses: ./.github/workflows/pr-test-amd-rocm720.yml
secrets: inherit
with:
ref: amd/aiter-ci
run_all_tests: true
aiter_ref: ${{ needs.resolve-aiter.outputs.aiter_sha }}
continue_on_error: ${{ inputs.continue_on_error == '' && true || inputs.continue_on_error }}
check-all-jobs:
if: always()
needs:
- resolve-aiter
- call-nightly-amd
- call-nightly-amd-rocm720
- call-pr-test-amd
- call-pr-test-amd-rocm720
runs-on: ubuntu-latest
steps:
- name: Summary
run: |
echo "## AMD AITER Scout Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **SGLang ref:** \`amd/aiter-ci\`" >> $GITHUB_STEP_SUMMARY
echo "- **AITER SHA:** \`${{ needs.resolve-aiter.outputs.aiter_sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **AITER commit:** https://github.com/ROCm/aiter/commit/${{ needs.resolve-aiter.outputs.aiter_sha }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Workflow | Result |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Nightly AMD (AITER Latest) | \`${{ needs.call-nightly-amd.result }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Nightly AMD ROCm 7.2 | \`${{ needs.call-nightly-amd-rocm720.result }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| PR Test AMD (AITER Latest) | \`${{ needs.call-pr-test-amd.result }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| PR Test AMD ROCm 7.2 | \`${{ needs.call-pr-test-amd-rocm720.result }}\` |" >> $GITHUB_STEP_SUMMARY
- name: Check if any job failed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more workflows failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more workflows were cancelled"
exit 1
fi
echo "All workflows passed"
+352
View File
@@ -0,0 +1,352 @@
name: AMD CI Job Monitor
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight UTC
pull_request:
paths:
- '.github/workflows/amd-ci-job-monitor.yml'
- 'scripts/ci/utils/query_job_status.py'
workflow_dispatch:
inputs:
hours:
description: 'Time window in hours'
required: false
default: '24'
type: string
job_filter:
description: 'Job name filter (leave empty for all AMD jobs)'
required: false
type: string
# Bound the API cost when the same ref pushes repeatedly. See note in
# runner-utilization.yml for the original incident this guards against.
concurrency:
group: amd-ci-job-monitor-${{ github.ref }}
cancel-in-progress: true
jobs:
fetch-actions-data:
name: Fetch Actions Snapshot
# Skip fork PRs entirely, and require the `run-ci` label on same-repo PRs.
# schedule and workflow_dispatch always run.
if: >-
github.event_name != 'pull_request' ||
(github.event.pull_request.head.repo.full_name == github.repository &&
contains(github.event.pull_request.labels.*.name, 'run-ci'))
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Select workflows for snapshot
id: select-workflows
run: |
if [[ -n "${{ inputs.job_filter }}" ]]; then
echo "workflows=pr-test-amd.yml" >> "$GITHUB_OUTPUT"
else
echo "workflows=pr-test-amd.yml,nightly-test-amd.yml,pr-test-amd-rocm720.yml,nightly-test-amd-rocm720.yml" >> "$GITHUB_OUTPUT"
fi
- name: Fetch Actions data snapshot
timeout-minutes: 30
run: |
# PR-trigger is just a script smoke check; scan a 20-minute window
# (0.34h) to bound API cost. schedule / dispatch run the full report.
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--workflow "${{ steps.select-workflows.outputs.workflows }}" \
--hours ${{ (github.event_name == 'pull_request' && '0.34') || inputs.hours || '24' }} \
--dump-data-file actions-job-snapshot.json
- name: Upload Actions data snapshot
uses: actions/upload-artifact@v4
with:
name: actions-job-snapshot
path: actions-job-snapshot.json
if-no-files-found: error
# Single job filter mode
custom-report:
name: Custom Job Report
if: ${{ inputs.job_filter }}
needs: fetch-actions-data
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Custom Job Report
timeout-minutes: 30
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ inputs.job_filter }}" \
--workflow "pr-test-amd.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# Parse workflow files to get job names dynamically
parse-workflows:
name: Parse Workflow Jobs
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
outputs:
pr_jobs: ${{ steps.parse.outputs.pr_jobs }}
nightly_jobs: ${{ steps.parse.outputs.nightly_jobs }}
pr_rocm720_jobs: ${{ steps.parse.outputs.pr_rocm720_jobs }}
nightly_rocm720_jobs: ${{ steps.parse.outputs.nightly_rocm720_jobs }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Parse workflow files
id: parse
run: |
# Parse pr-test-amd.yml and extract job names (exclude utility jobs)
# Excluded: call-gate, check-changes, pr-test-amd-finish, cancel, check-all-jobs
pr_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/pr-test-amd.yml | \
grep -v -E '^(call-gate|check-changes|pr-test-amd-finish|cancel|check-all-jobs)$' | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "pr_jobs=$pr_jobs" >> $GITHUB_OUTPUT
echo "PR jobs: $pr_jobs"
# Parse nightly-test-amd.yml and extract job names (exclude utility jobs)
# Excluded: check-all-jobs
nightly_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/nightly-test-amd.yml | \
grep -v -E '^(check-all-jobs)$' | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "nightly_jobs=$nightly_jobs" >> $GITHUB_OUTPUT
echo "Nightly jobs: $nightly_jobs"
# Parse pr-test-amd-rocm720.yml (exclude utility jobs)
# Excluded: call-gate, check-changes, pr-test-amd-finish, cancel, check-all-jobs
pr_rocm720_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/pr-test-amd-rocm720.yml | \
grep -v -E '^(call-gate|check-changes|pr-test-amd-finish|cancel|check-all-jobs)$' | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "pr_rocm720_jobs=$pr_rocm720_jobs" >> $GITHUB_OUTPUT
echo "PR ROCm 7.2 jobs: $pr_rocm720_jobs"
# Parse nightly-test-amd-rocm720.yml (exclude utility jobs)
# Excluded: check-all-jobs
nightly_rocm720_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/nightly-test-amd-rocm720.yml | \
grep -v -E '^(check-all-jobs)$' | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "nightly_rocm720_jobs=$nightly_rocm720_jobs" >> $GITHUB_OUTPUT
echo "Nightly ROCm 7.2 jobs: $nightly_rocm720_jobs"
# PR CI reports using dynamic matrix
pr-ci-reports:
name: PR - ${{ matrix.job_name }}
needs: [parse-workflows, fetch-actions-data]
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job_name: ${{ fromJson(needs.parse-workflows.outputs.pr_jobs) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Report
timeout-minutes: 15
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "pr-test-amd.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# Nightly AMD test reports using dynamic matrix
nightly-reports:
name: Nightly - ${{ matrix.job_name }}
needs: [parse-workflows, fetch-actions-data]
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job_name: ${{ fromJson(needs.parse-workflows.outputs.nightly_jobs) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Nightly Report
timeout-minutes: 15
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "nightly-test-amd.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# PR ROCm 7.2 CI reports using dynamic matrix
pr-rocm720-ci-reports:
name: PR ROCm720 - ${{ matrix.job_name }}
needs: [parse-workflows, fetch-actions-data]
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job_name: ${{ fromJson(needs.parse-workflows.outputs.pr_rocm720_jobs) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate PR ROCm 7.2 Report
timeout-minutes: 15
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "pr-test-amd-rocm720.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# Nightly ROCm 7.2 reports using dynamic matrix
nightly-rocm720-reports:
name: Nightly ROCm720 - ${{ matrix.job_name }}
needs: [parse-workflows, fetch-actions-data]
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job_name: ${{ fromJson(needs.parse-workflows.outputs.nightly_rocm720_jobs) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Nightly ROCm 7.2 Report
timeout-minutes: 15
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "nightly-test-amd-rocm720.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# Runner fleet report - cross-workflow runner analytics in a single pass
runner-fleet-report:
name: Runner Fleet Report
if: ${{ !inputs.job_filter }}
needs: fetch-actions-data
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Runner Fleet Report
timeout-minutes: 30
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--runner-report \
--workflow "pr-test-amd.yml,nightly-test-amd.yml,pr-test-amd-rocm720.yml,nightly-test-amd-rocm720.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
+10
View File
@@ -0,0 +1,10 @@
name: Auto tune
on:
workflow_dispatch:
jobs:
auto-tune-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -0,0 +1,88 @@
name: Bot Bump Docs Version
#
# Bumps the "install from source" release-branch version pinned in the docs
# (the `git clone -b v<version> ...sglang.git` line in
# docs_new/docs/get-started/install.mdx "Method 2: From source" and
# docs_new/docs/hardware-platforms/amd_gpu.mdx) whenever a release tag is
# pushed, and opens a PR with the change.
#
# Triggers mirror release-docker.yml: a pushed `v*` tag (the release), or a
# manual run with an explicit version.
on:
push:
tags:
- "v[0-9]+.*"
workflow_dispatch:
inputs:
version:
description: "Version to set (without v prefix, e.g., 0.5.13)"
required: true
permissions:
contents: write
pull-requests: write
jobs:
resolve-version:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#v}"
fi
if [ -z "$VERSION" ] || ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
bump-docs-version:
needs: resolve-version
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# Branch off main so the PR targets the docs on the default branch,
# not the (detached) tag commit that triggered this run.
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Configure Git and branch
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
# github.run_id is unique per run repo-wide; run_attempt disambiguates re-runs.
BRANCH_NAME="bot/bump-docs-version-${{ needs.resolve-version.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
- name: Run docs version bump script
run: |
python scripts/release/bump_docs_install_version.py "${{ needs.resolve-version.outputs.version }}"
- name: Commit and create PR
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
if git diff --quiet; then
echo "Docs already pin this version; no PR needed."
exit 0
fi
bash scripts/release/commit_and_pr.sh "docs install" "${{ needs.resolve-version.outputs.version }}" "$BRANCH_NAME"
@@ -0,0 +1,50 @@
name: Bot Bump Flashinfer Version
on:
workflow_dispatch:
inputs:
new_version:
description: 'New flashinfer version (e.g., 0.6.4)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
bump-flashinfer-version:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Configure Git and branch
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
RANDOM_SUFFIX=$(echo $RANDOM | md5sum | head -c 4)
BRANCH_NAME="bot/bump-flashinfer-version-${{ github.event.inputs.new_version }}-${RANDOM_SUFFIX}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
- name: Run flashinfer version bump script
run: |
python scripts/release/bump_flashinfer_version.py "${{ github.event.inputs.new_version }}"
- name: Commit and create PR
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
bash scripts/release/commit_and_pr.sh "flashinfer" "${{ github.event.inputs.new_version }}" "$BRANCH_NAME"
@@ -0,0 +1,60 @@
name: Bot Bump Kernel Version to SGLang
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
bump-kernel-version-to-sglang:
runs-on: ubuntu-latest
outputs:
branch_name: ${{ steps.set_output.outputs.branch_name }}
needs_sync: ${{ steps.check_sync.outputs.needs_sync }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Check if sync is needed
id: check_sync
run: |
python scripts/release/check_kernel_version_to_sglang.py
- name: Configure Git and branch
if: steps.check_sync.outputs.needs_sync == 'true'
id: set_output
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
RANDOM_SUFFIX=$(echo $RANDOM | md5sum | head -c 4)
KERNEL_VERSION="${{ steps.check_sync.outputs.kernel_version }}"
BRANCH_NAME="bot/bump-kernel-version-to-sglang-${KERNEL_VERSION}-${RANDOM_SUFFIX}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
echo "KERNEL_VERSION=$KERNEL_VERSION" >> $GITHUB_ENV
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Run kernel version bump script
if: steps.check_sync.outputs.needs_sync == 'true'
run: |
python scripts/release/bump_kernel_version_to_sglang.py
- name: Commit and create PR
if: steps.check_sync.outputs.needs_sync == 'true'
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
bash scripts/release/commit_and_pr_kernel_to_sglang.sh "$KERNEL_VERSION" "$BRANCH_NAME"
@@ -0,0 +1,50 @@
name: Bot Bump Kernel Version
on:
workflow_dispatch:
inputs:
new_version:
description: 'New sgl-kernel version (e.g., 0.3.12)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
bump-kernel-version:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Configure Git and branch
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
RANDOM_SUFFIX=$(echo $RANDOM | md5sum | head -c 4)
BRANCH_NAME="bot/bump-kernel-version-${{ github.event.inputs.new_version }}-${RANDOM_SUFFIX}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
- name: Run kernel version bump script
run: |
python scripts/release/bump_kernel_version.py "${{ github.event.inputs.new_version }}"
- name: Commit and create PR
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
bash scripts/release/commit_and_pr.sh "sgl-kernel" "${{ github.event.inputs.new_version }}" "$BRANCH_NAME"
@@ -0,0 +1,89 @@
name: Bot Bump SGLang Version
on:
workflow_dispatch:
inputs:
new_version:
description: 'New SGLang version (e.g., 0.5.3 or 0.5.3rc0)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
bump-sglang-version:
runs-on: ubuntu-latest
outputs:
branch_name: ${{ steps.set_output.outputs.branch_name }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Configure Git and branch
id: set_output
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
RANDOM_SUFFIX=$(echo $RANDOM | md5sum | head -c 4)
BRANCH_NAME="bot/bump-sglang-version-${{ github.event.inputs.new_version }}-${RANDOM_SUFFIX}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Run SGLang version bump script
run: |
python scripts/release/bump_sglang_version.py "${{ github.event.inputs.new_version }}"
- name: Commit and create PR
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
bash scripts/release/commit_and_pr.sh "SGLang" "${{ github.event.inputs.new_version }}" "$BRANCH_NAME"
run-nightly-tests-nvidia:
needs: bump-sglang-version
uses: ./.github/workflows/nightly-test-nvidia.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
run-nightly-tests-amd:
needs: bump-sglang-version
uses: ./.github/workflows/nightly-test-amd.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
run-nightly-tests-npu:
needs: bump-sglang-version
uses: ./.github/workflows/nightly-test-npu.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
run-pr-tests-xeon:
needs: bump-sglang-version
uses: ./.github/workflows/pr-test-xeon.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
run-pr-tests-xpu:
needs: bump-sglang-version
uses: ./.github/workflows/pr-test-xpu.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
+273
View File
@@ -0,0 +1,273 @@
name: Bot Cherry Pick to Release Branch
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to cherry-pick (preferred). Provide either pr_number OR commit_sha, not both.'
required: false
type: string
commit_sha:
description: 'Commit SHA to cherry-pick (alternative to pr_number; full or short hash).'
required: false
type: string
target_branch:
description: 'Target release branch (e.g., release/v0.5.7)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
cherry-pick:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
steps:
- name: Validate inputs
env:
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
COMMIT_SHA_INPUT: ${{ github.event.inputs.commit_sha }}
run: |
if [[ ! "$TARGET_BRANCH" =~ ^release/v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
echo "::error::Target branch must match pattern 'release/vX.Y' or 'release/vX.Y.Z' (e.g., release/v0.5.7)"
exit 1
fi
if [[ -z "$PR_NUMBER" && -z "$COMMIT_SHA_INPUT" ]]; then
echo "::error::Provide either 'pr_number' or 'commit_sha'."
exit 1
fi
if [[ -n "$PR_NUMBER" && -n "$COMMIT_SHA_INPUT" ]]; then
echo "::error::Provide exactly one of 'pr_number' or 'commit_sha', not both."
exit 1
fi
if [[ -n "$PR_NUMBER" && ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::pr_number must be a positive integer."
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure Git
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
- name: Validate target branch exists
env:
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
run: |
git fetch origin
if ! git ls-remote --exit-code --heads origin "$TARGET_BRANCH" > /dev/null 2>&1; then
echo "::error::Target branch '$TARGET_BRANCH' does not exist on remote"
exit 1
fi
- name: Resolve PR / commit
id: resolve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
COMMIT_SHA_INPUT: ${{ github.event.inputs.commit_sha }}
run: |
set -eo pipefail
shopt -s inherit_errexit
SOURCE_PR=""
PR_URL=""
if [[ -n "$PR_NUMBER" ]]; then
if ! PR_JSON=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state,mergeCommit,title,number,url); then
echo "::error::Failed to fetch PR #$PR_NUMBER from $GITHUB_REPOSITORY (see gh output above)."
exit 1
fi
STATE=$(jq -r '.state' <<<"$PR_JSON")
if [[ "$STATE" != "MERGED" ]]; then
echo "::error::PR #$PR_NUMBER is not merged (state=$STATE). Only merged PRs can be cherry-picked."
exit 1
fi
FULL_SHA=$(jq -r '.mergeCommit.oid // empty' <<<"$PR_JSON")
if [[ -z "$FULL_SHA" ]]; then
echo "::error::PR #$PR_NUMBER is marked merged but has no associated merge commit."
exit 1
fi
PR_TITLE=$(jq -r '.title // empty' <<<"$PR_JSON")
PR_URL=$(jq -r '.url // empty' <<<"$PR_JSON")
SOURCE_PR="#$PR_NUMBER"
else
if ! git cat-file -t "$COMMIT_SHA_INPUT" > /dev/null 2>&1; then
echo "::error::Commit SHA '$COMMIT_SHA_INPUT' does not exist in the fetched history"
exit 1
fi
FULL_SHA=$(git rev-parse --verify "${COMMIT_SHA_INPUT}^{commit}")
PR_TITLE=$(git log -1 --format="%s" "$FULL_SHA")
# Look up the merged PR associated with this commit. Distinguish "API call
# failed" (abort — could mask an unmerged commit) from "API returned no
# merged PR" (fall through to ancestry check).
if ! PR_LOOKUP_RAW=$(gh api -X GET "repos/${GITHUB_REPOSITORY}/commits/${FULL_SHA}/pulls"); then
echo "::error::Failed to query PRs associated with commit ${FULL_SHA}. Aborting to avoid cherry-picking unverified commits."
exit 1
fi
PR_LOOKUP=$(jq '[.[] | select(.merged_at != null)][0] // empty' <<<"$PR_LOOKUP_RAW")
if [[ -n "$PR_LOOKUP" ]]; then
ASSOC_PR_NUM=$(jq -r '.number' <<<"$PR_LOOKUP")
PR_URL=$(jq -r '.html_url // empty' <<<"$PR_LOOKUP")
PR_TITLE=$(jq -r '.title // empty' <<<"$PR_LOOKUP")
SOURCE_PR="#$ASSOC_PR_NUM"
else
# No merged PR found for this commit. Require it to at least be reachable
# from the default branch so we don't cherry-pick unmerged work.
if ! DEFAULT_BRANCH=$(gh repo view "$GITHUB_REPOSITORY" --json defaultBranchRef --jq '.defaultBranchRef.name'); then
echo "::error::Could not determine default branch for ${GITHUB_REPOSITORY}."
exit 1
fi
if [[ -z "$DEFAULT_BRANCH" ]]; then
echo "::error::GitHub API returned an empty default branch for ${GITHUB_REPOSITORY}."
exit 1
fi
if ! git rev-parse --verify "origin/${DEFAULT_BRANCH}" >/dev/null 2>&1; then
echo "::error::origin/${DEFAULT_BRANCH} is not fetched locally; cannot verify whether ${FULL_SHA} is merged."
exit 1
fi
if ! git merge-base --is-ancestor "$FULL_SHA" "origin/${DEFAULT_BRANCH}"; then
echo "::error::Commit '$FULL_SHA' has no associated merged PR and is not reachable from origin/${DEFAULT_BRANCH}. Refusing to cherry-pick unmerged commits."
exit 1
fi
fi
fi
SHORT_SHA=$(git rev-parse --short "$FULL_SHA")
# Detect merge commit by counting parents. `git show -s --format=%P` prints
# parent SHAs separated by spaces; >1 means it's a merge commit.
PARENTS_LINE=$(git show -s --format=%P "$FULL_SHA")
PARENT_COUNT=$(wc -w <<<"$PARENTS_LINE" | tr -d ' ')
IS_MERGE_COMMIT="false"
if [[ "$PARENT_COUNT" -gt 1 ]]; then
IS_MERGE_COMMIT="true"
fi
echo "full_sha=$FULL_SHA" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "is_merge_commit=$IS_MERGE_COMMIT" >> $GITHUB_OUTPUT
echo "source_pr=$SOURCE_PR" >> $GITHUB_OUTPUT
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
{
echo "pr_title<<EOF"
echo "$PR_TITLE"
echo "EOF"
} >> $GITHUB_OUTPUT
echo "Cherry-picking ${SHORT_SHA} (${SOURCE_PR:-no associated PR}) - ${PR_TITLE}"
- name: Cherry-pick commit
id: cherry_pick
env:
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
FULL_SHA: ${{ steps.resolve.outputs.full_sha }}
SHORT_SHA: ${{ steps.resolve.outputs.short_sha }}
IS_MERGE_COMMIT: ${{ steps.resolve.outputs.is_merge_commit }}
run: |
RANDOM_SUFFIX=$(head -c 4 /dev/urandom | xxd -p)
NEW_BRANCH="cherry-pick/${SHORT_SHA}-to-${TARGET_BRANCH#release/}-${RANDOM_SUFFIX}"
git checkout -b "$NEW_BRANCH" "origin/$TARGET_BRANCH"
echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT
CP_ARGS=()
if [[ "$IS_MERGE_COMMIT" == "true" ]]; then
CP_ARGS+=(-m 1)
fi
if git cherry-pick "${CP_ARGS[@]}" "$FULL_SHA"; then
echo "cherry_pick_success=true" >> $GITHUB_OUTPUT
else
echo "::error::Cherry-pick of ${FULL_SHA} onto ${TARGET_BRANCH} failed due to conflicts. Resolve locally: 'git checkout ${TARGET_BRANCH} && git cherry-pick ${FULL_SHA}', fix conflicts, push a branch, and open the PR by hand."
git cherry-pick --abort || echo "::warning::git cherry-pick --abort returned non-zero; ignoring since the job is exiting."
echo "cherry_pick_success=false" >> $GITHUB_OUTPUT
exit 1
fi
- name: Push branch
if: steps.cherry_pick.outputs.cherry_pick_success == 'true'
env:
NEW_BRANCH: ${{ steps.cherry_pick.outputs.new_branch }}
run: |
git push origin "$NEW_BRANCH"
- name: Create Pull Request
id: create_pr
if: steps.cherry_pick.outputs.cherry_pick_success == 'true'
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_CHERRY_PICK }}
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
SHORT_SHA: ${{ steps.resolve.outputs.short_sha }}
FULL_SHA: ${{ steps.resolve.outputs.full_sha }}
PR_TITLE: ${{ steps.resolve.outputs.pr_title }}
SOURCE_PR: ${{ steps.resolve.outputs.source_pr }}
PR_URL: ${{ steps.resolve.outputs.pr_url }}
NEW_BRANCH: ${{ steps.cherry_pick.outputs.new_branch }}
run: |
if [[ -n "$SOURCE_PR" ]]; then
CHERRY_PICK_TITLE="[Cherry-pick to ${TARGET_BRANCH}] ${PR_TITLE} (${SOURCE_PR})"
else
CHERRY_PICK_TITLE="[Cherry-pick to ${TARGET_BRANCH}] ${PR_TITLE} (${SHORT_SHA})"
fi
BODY="Cherry-pick of commit \`${FULL_SHA}\` to \`${TARGET_BRANCH}\`."$'\n\n'
if [[ -n "$SOURCE_PR" ]]; then
BODY+="**Source PR:** ${SOURCE_PR} (${PR_URL})"$'\n'
fi
BODY+="**Source commit:** \`${FULL_SHA}\`"$'\n'
BODY+="**Original title:** ${PR_TITLE}"$'\n\n'
BODY+="---"$'\n'
BODY+="*This PR was automatically created by the cherry-pick workflow.*"
CREATED_PR_URL=$(printf '%s' "$BODY" | gh pr create \
--title "$CHERRY_PICK_TITLE" \
--base "$TARGET_BRANCH" \
--head "$NEW_BRANCH" \
--label "cherry-pick" \
--body-file -)
CREATED_PR_NUMBER="${CREATED_PR_URL##*/}"
echo "created_pr_url=$CREATED_PR_URL" >> $GITHUB_OUTPUT
echo "created_pr_number=$CREATED_PR_NUMBER" >> $GITHUB_OUTPUT
echo "Created cherry-pick PR #${CREATED_PR_NUMBER}: ${CREATED_PR_URL}"
- name: Summary
if: always()
env:
FULL_SHA: ${{ steps.resolve.outputs.full_sha }}
PR_TITLE: ${{ steps.resolve.outputs.pr_title }}
SOURCE_PR: ${{ steps.resolve.outputs.source_pr }}
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
CHERRY_PICK_SUCCESS: ${{ steps.cherry_pick.outputs.cherry_pick_success }}
NEW_BRANCH: ${{ steps.cherry_pick.outputs.new_branch }}
CREATED_PR_NUMBER: ${{ steps.create_pr.outputs.created_pr_number }}
CREATED_PR_URL: ${{ steps.create_pr.outputs.created_pr_url }}
ACTOR: ${{ github.actor }}
run: |
echo "## Cherry-Pick Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Triggered by:** @${ACTOR}" >> $GITHUB_STEP_SUMMARY
if [[ -n "$SOURCE_PR" ]]; then
echo "- **Source PR:** ${SOURCE_PR}" >> $GITHUB_STEP_SUMMARY
fi
echo "- **Source commit:** ${FULL_SHA}" >> $GITHUB_STEP_SUMMARY
echo "- **Title:** ${PR_TITLE}" >> $GITHUB_STEP_SUMMARY
echo "- **Target Branch:** ${TARGET_BRANCH}" >> $GITHUB_STEP_SUMMARY
if [[ "$CHERRY_PICK_SUCCESS" == "true" ]]; then
echo "- **Status:** Success" >> $GITHUB_STEP_SUMMARY
echo "- **PR Branch:** ${NEW_BRANCH}" >> $GITHUB_STEP_SUMMARY
if [[ -n "$CREATED_PR_NUMBER" ]]; then
echo "- **Cherry-pick PR:** [#${CREATED_PR_NUMBER}](${CREATED_PR_URL})" >> $GITHUB_STEP_SUMMARY
fi
else
echo "- **Status:** Failed" >> $GITHUB_STEP_SUMMARY
fi
@@ -0,0 +1,104 @@
name: Cancel PR Workflows on Close
# Cancels every unfinished run on a PR's head branch when the PR closes
# (merged or not). Runs are listed repo-wide by branch, not per workflow:
# the previous styfle/cancel-workflow-action `workflow_id: all` fetched
# only the first page (30) of repo workflows, so newer workflows (e.g.
# pr-test-extra.yml) were never cancelled and kept occupying runners.
on:
pull_request_target:
types:
- closed
permissions:
actions: write
jobs:
cancel:
runs-on: ubuntu-latest
steps:
- name: Cancel unfinished runs of this PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
HEAD_REPO_ID: ${{ github.event.pull_request.head.repo.id }}
shell: bash
run: |
set -euo pipefail
if [ -z "$HEAD_REPO_ID" ]; then
echo "Head repo no longer exists (fork deleted), nothing to match"
exit 0
fi
FAILURES=0
# `branch=` matches run.head_branch, shared with same-named
# branches of other forks -- filter by head repo id. Exclude this
# run itself: pull_request_target runs also carry the head branch.
# The status filter takes one value per request, so sweep every
# non-terminal state (pending/waiting/action_required runs would
# otherwise start after the PR is closed). Pass 2 catches runs
# still materializing during pass 1.
for pass in 1 2; do
run_ids=""
for status in queued in_progress waiting pending requested action_required; do
ids=""
for attempt in 1 2 3; do
if ids=$(gh api -X GET "repos/$REPO/actions/runs" \
--paginate \
-f branch="$HEAD_BRANCH" \
-f status="$status" \
-F per_page=100 \
--jq ".workflow_runs[]
| select(.head_repository.id == $HEAD_REPO_ID and .id != $GITHUB_RUN_ID)
| .id"); then
break
fi
ids=""
if [ "$attempt" -eq 3 ]; then
echo "::error::Listing $status runs failed after 3 attempts"
FAILURES=1
else
sleep 5
fi
done
run_ids="$run_ids"$'\n'"$ids"
done
run_ids=$(echo "$run_ids" | sed '/^$/d' | sort -u)
if [ -z "$run_ids" ]; then
echo "Pass $pass: no unfinished runs found"
break
fi
echo "Pass $pass: cancelling $(echo "$run_ids" | wc -l | tr -d ' ') run(s)"
for run_id in $run_ids; do
echo "Cancelling https://github.com/$REPO/actions/runs/$run_id"
if gh run cancel "$run_id" --repo "$REPO" 2>/dev/null; then
continue
fi
# Plain cancel fails for runs stuck behind approval /
# deployment protection rules; force-cancel handles those.
if gh api -X POST "repos/$REPO/actions/runs/$run_id/force-cancel" >/dev/null 2>&1; then
echo " force-cancelled"
continue
fi
# Finished between listing and cancelling is fine.
state=$(gh api "repos/$REPO/actions/runs/$run_id" --jq '.status' 2>/dev/null || echo "unknown")
if [ "$state" = "completed" ]; then
echo " already finished, nothing to cancel"
else
echo "::error::Failed to cancel run $run_id (status: $state)"
FAILURES=1
fi
done
if [ "$pass" -eq 1 ]; then
sleep 20
fi
done
exit "$FAILURES"
@@ -0,0 +1,234 @@
name: Cancel Unfinished PR Runs
on:
workflow_dispatch:
inputs:
workflows:
description: 'Space-separated list of workflow filenames to cancel'
required: true
type: string
default: 'pr-test.yml pr-test-extra.yml'
include_high_priority:
description: 'Also cancel runs from high-priority PRs'
required: false
type: boolean
default: false
include_rerun_test:
description: 'Also cancel /rerun-test dispatched runs (rerun-test.yml)'
required: false
type: boolean
default: false
permissions:
actions: write # Needed to cancel runs
contents: read # Needed to read repo info
pull-requests: read # needed for gh pr view (labels)
jobs:
cancel-unfinished-pr-runs:
runs-on: ubuntu-latest
steps:
- name: Install GitHub CLI
run: sudo apt-get install -y gh jq
- name: Cancel unfinished PR-associated runs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
WORKFLOWS: ${{ github.event.inputs.workflows }}
INCLUDE_HIGH_PRIORITY: ${{ github.event.inputs.include_high_priority }}
INCLUDE_RERUN_TEST: ${{ github.event.inputs.include_rerun_test }}
shell: bash
run: |
set -euo pipefail
# Read the space-separated string from the input into a bash array
read -r -a WORKFLOW_FILES <<< "${WORKFLOWS}"
if [ "$INCLUDE_RERUN_TEST" = "true" ]; then
WORKFLOW_FILES+=("rerun-test.yml")
fi
echo "Targeting ${#WORKFLOW_FILES[@]} workflow(s): ${WORKFLOW_FILES[*]}"
echo "include_high_priority=$INCLUDE_HIGH_PRIORITY, include_rerun_test=$INCLUDE_RERUN_TEST"
echo ""
# Decide whether to cancel run_id given a PR-lookup endpoint.
# $1 = run_id
# $2 = gh api path returning a list of PR objects (head=... or commits/<sha>/pulls)
# $3 = short label for log messages
maybe_cancel_for_pr() {
local run_id="$1"
local pr_query="$2"
local pr_label="$3"
local pr_info
pr_info=$(gh api -H "Accept: application/vnd.github+json" "$pr_query" \
--jq '.[0] | {number, state, merged_at}' 2>/dev/null || true)
if [ -z "$pr_info" ] || [ "$pr_info" = "null" ]; then
echo " ⚠️ No PR found ($pr_label), skipping"
return
fi
local pr_number pr_state
pr_number=$(echo "$pr_info" | jq -r '.number // empty')
pr_state=$(echo "$pr_info" | jq -r '.state // empty')
if [ -z "$pr_number" ]; then
echo " ⚠️ PR lookup returned empty number, skipping"
return
fi
local pr_url="https://github.com/$REPO/pull/$pr_number"
echo " PR: $pr_url ($pr_state)"
# Closed PR (merged or not): always cancel, skip label checks.
if [ "$pr_state" = "closed" ]; then
echo " 🚫 Cancelling (PR closed)..."
gh run cancel "$run_id" --repo "$REPO" || echo " ⚠️ Cancellation failed"
return
fi
# Open PR: apply label-based skip rules.
local labels
labels=$(gh pr view "$pr_number" --repo "$REPO" --json labels \
| jq -r '.labels[].name' 2>/dev/null || true)
if echo "$labels" | grep -Fxq "bypass-maintenance"; then
echo " 🛑 Skipping (bypass-maintenance label, never cancelled)"
return
fi
if echo "$labels" | grep -Fxq "high priority"; then
if [ "$INCLUDE_HIGH_PRIORITY" != "true" ]; then
echo " 🛑 Skipping (high priority label)"
return
fi
echo " ⚠️ High priority PR, but include_high_priority is enabled"
fi
echo " 🚫 Cancelling..."
gh run cancel "$run_id" --repo "$REPO" || echo " ⚠️ Cancellation failed"
}
export -f maybe_cancel_for_pr
for workflow_file in "${WORKFLOW_FILES[@]}"; do
echo "========================================="
echo "Workflow: $workflow_file"
echo "========================================="
# Get all unfinished runs.
# Use server-side --status filter: without it, `gh run list --limit 1000`
# only sees the most recent 1000 runs by createdAt, which on busy workflows
# like pr-test.yml is < 3 days. Old stuck runs would be missed.
all_runs=""
for status in queued in_progress waiting; do
batch=$(gh run list \
--repo "$REPO" \
--workflow "$workflow_file" \
--status "$status" \
--json databaseId,status,event,url,createdAt,displayTitle \
--limit 1000 \
| jq -c '.[]')
if [ -n "$batch" ]; then
if [ -n "$all_runs" ]; then
all_runs="$all_runs"$'\n'"$batch"
else
all_runs="$batch"
fi
fi
done
if [ -z "$all_runs" ]; then
echo "✅ No unfinished runs found"
echo ""
continue
fi
# Count runs by event type
total_runs=$(echo "$all_runs" | wc -l)
pr_runs=$(echo "$all_runs" | jq -s '[.[] | select(.event=="pull_request")] | length')
dispatch_runs=$(echo "$all_runs" | jq -s '[.[] | select(.event=="workflow_dispatch")] | length')
other_runs=$(echo "$all_runs" | jq -s '[.[] | select(.event!="pull_request" and .event!="workflow_dispatch")] | length')
echo "📊 Summary: $total_runs unfinished ($pr_runs pull_request, $dispatch_runs workflow_dispatch, $other_runs other)"
echo ""
# Other runs: list only, do not cancel.
if [ "$other_runs" -gt 0 ]; then
echo "--- Other Runs (listed only, not cancelled) ---"
echo "$all_runs" | jq -c 'select(.event!="pull_request" and .event!="workflow_dispatch")' | while read -r run; do
run_url=$(echo "$run" | jq -r '.url')
run_event=$(echo "$run" | jq -r '.event')
run_status=$(echo "$run" | jq -r '.status')
echo " • $run_event ($run_status): $run_url"
done
echo ""
fi
# PR runs: resolve PR via head=owner:branch.
if [ "$pr_runs" -gt 0 ]; then
echo "--- PR Runs (resolving via head=owner:branch) ---"
echo "$all_runs" | jq -c 'select(.event=="pull_request")' | while read -r run; do
run_id=$(echo "$run" | jq -r '.databaseId')
run_url=$(echo "$run" | jq -r '.url')
run_status=$(echo "$run" | jq -r '.status')
echo ""
echo "Run ($run_status): $run_url"
run_details=$(gh api -H "Accept: application/vnd.github+json" \
"repos/$REPO/actions/runs/$run_id" 2>/dev/null || true)
if [ -z "$run_details" ]; then
echo " ⚠️ Could not fetch run details, skipping"
continue
fi
head_owner=$(echo "$run_details" | jq -r '.head_repository.owner.login // empty')
head_branch=$(echo "$run_details" | jq -r '.head_branch // empty')
if [ -z "$head_owner" ] || [ -z "$head_branch" ]; then
echo " ⚠️ Missing head info, skipping"
continue
fi
echo " Branch: ${head_owner}:${head_branch}"
maybe_cancel_for_pr "$run_id" \
"repos/$REPO/pulls?state=all&head=${head_owner}:${head_branch}" \
"head=${head_owner}:${head_branch}"
done
echo ""
fi
# workflow_dispatch runs (e.g. /rerun-test): resolve PR via pr_head_sha
# parsed from run-name (`[rerun-test] <cmd> <sha>`).
if [ "$dispatch_runs" -gt 0 ]; then
echo "--- Dispatch Runs (resolving via pr_head_sha in display_title) ---"
echo "$all_runs" | jq -c 'select(.event=="workflow_dispatch")' | while read -r run; do
run_id=$(echo "$run" | jq -r '.databaseId')
run_url=$(echo "$run" | jq -r '.url')
run_status=$(echo "$run" | jq -r '.status')
display_title=$(echo "$run" | jq -r '.displayTitle // empty')
echo ""
echo "Run ($run_status): $run_url"
echo " Title: $display_title"
# Last whitespace-delimited token if it is a 40-hex SHA.
last_token=$(echo "$display_title" | awk '{print $NF}')
if ! [[ "$last_token" =~ ^[0-9a-f]{40}$ ]]; then
echo " ⚠️ No pr_head_sha in title, skipping"
continue
fi
echo " pr_head_sha: $last_token"
maybe_cancel_for_pr "$run_id" \
"repos/$REPO/commits/$last_token/pulls" \
"sha=$last_token"
done
echo ""
fi
done
echo "========================================="
echo "✅ Processing complete"
echo "========================================="
+66
View File
@@ -0,0 +1,66 @@
name: CI Auto Bisect
on:
workflow_run:
workflows: ["PR Test Base"]
types: [completed]
branches: [main]
workflow_dispatch: {}
concurrency:
group: ci-auto-bisect
cancel-in-progress: true
permissions:
contents: read
actions: read
jobs:
auto-bisect:
# Only run for scheduled pr-test completions (not PR-triggered), or manual dispatch
if: >
github.repository == 'sgl-project/sglang' && (
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.event == 'schedule' &&
github.event.workflow_run.conclusion != 'cancelled')
)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for git log between SHAs
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.14'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests anthropic
- name: Run Auto Bisect
id: bisect
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PYTHONUNBUFFERED: 1
PYTHONIOENCODING: utf-8
run: |
cd scripts/ci_monitor
python ci_auto_bisect.py \
--github-token $GITHUB_TOKEN \
--anthropic-api-key $ANTHROPIC_API_KEY \
--output bisect_results.json \
--max-failures 10
- name: Upload Bisect Results
if: always() && hashFiles('scripts/ci_monitor/bisect_results.json') != ''
uses: actions/upload-artifact@v4
with:
name: ci-auto-bisect-${{ github.run_number }}
path: scripts/ci_monitor/bisect_results.json
retention-days: 14
+164
View File
@@ -0,0 +1,164 @@
name: CI Coverage Overview
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
workflow_dispatch:
inputs:
output_format:
description: 'Output format'
required: false
default: 'markdown'
type: choice
options:
- markdown
- json
jobs:
summary:
name: Summary
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Generate Summary Report
run: |
python scripts/ci/utils/ci_coverage_report.py --section summary
by-folder:
name: Tests by Folder
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Generate Tests by Folder Report
run: |
python scripts/ci/utils/ci_coverage_report.py --section by-folder
by-suite:
name: Tests by Suite
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Generate Tests by Suite Report
run: |
python scripts/ci/utils/ci_coverage_report.py --section by-suite
unit-test-coverage:
name: Unit Test Code Coverage
runs-on: 1-gpu-h100
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain + protoc
# Editable install of sglang triggers `build_rust` for the bundled
# native gRPC extension (rust/sglang-grpc). The 1-gpu-h100 runner
# image has no Rust toolchain by default, which is why this job has
# been failing on every scheduled run. Use the same helper that
# ci_install_dependency.sh uses so the install path matches the
# rest of CI.
run: |
bash scripts/ci/utils/install_rust_protoc.sh
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
- name: Install dependencies
timeout-minutes: 10
run: |
pip install -e "python/[test]"
- name: Run unit tests with coverage
timeout-minutes: 10
run: |
pytest test/registered/unit/ \
--cov --cov-config=.coveragerc \
--cov-report=term-missing:skip-covered \
--continue-on-collection-errors \
-v | tee coverage_output.txt
- name: Write coverage to summary
if: always()
run: |
echo "## Unit Test Code Coverage" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${GITHUB_SHA::8}\` | **Branch:** \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Test result line (e.g., "== 42 passed, 1 failed in 23.5s ==")
echo '```' >> $GITHUB_STEP_SUMMARY
grep -E '^=+.*passed' coverage_output.txt >> $GITHUB_STEP_SUMMARY || true
echo "" >> $GITHUB_STEP_SUMMARY
# Coverage total, reformatted
awk '/^TOTAL / { for(i=1;i<=NF;i++) if($i~/^[0-9]+$/ || $i~/^[0-9]+%$/) a[++n]=$i; if(n>=3) printf "TOTAL Stmts: %s Miss: %s Cover: %s\n", a[1], a[2], a[3] }' coverage_output.txt >> $GITHUB_STEP_SUMMARY || true
echo '```' >> $GITHUB_STEP_SUMMARY
# Core modules with coverage < 50%, sorted by uncovered lines (descending)
LOW_COV=$(awk '/^python\/.*%/ {
for (i=1; i<=NF; i++) {
if ($i ~ /^[0-9]+%$/) {
pct = $i + 0
if (pct >= 1 && pct < 50) {
stmts = $(i-2) + 0
miss = $(i-1) + 0
printf "%d|%s|%d|%d|%d%%\n", miss, $1, stmts, miss, pct
}
break
}
}
}' coverage_output.txt \
| grep -E '/(mem_cache|managers|sampling|parser|observability|function_call|entrypoints|speculative|multimodal|utils)/' \
| sort -t'|' -k1 -nr | cut -d'|' -f2- | head -40 || true)
if [ -n "$LOW_COV" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details><summary>Top uncovered core modules (coverage < 50%, sorted by uncovered lines)</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| File | Stmts | Miss | Cover |" >> $GITHUB_STEP_SUMMARY
echo "|------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY
echo "$LOW_COV" | while IFS='|' read -r file stmts miss pct; do
echo "| \`$file\` | $stmts | $miss | $pct |" >> $GITHUB_STEP_SUMMARY
done
echo "</details>" >> $GITHUB_STEP_SUMMARY
fi
json-export:
name: JSON Export
runs-on: ubuntu-latest
if: inputs.output_format == 'json'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Generate JSON Report
run: |
python scripts/ci/utils/ci_coverage_report.py --output-format json > ci_coverage.json
- name: Upload JSON artifact
uses: actions/upload-artifact@v4
with:
name: ci-coverage-report
path: ci_coverage.json
+53
View File
@@ -0,0 +1,53 @@
name: CI Failure Monitor
on:
schedule:
- cron: '0 */12 * * *' # Every 12 hour
workflow_dispatch:
concurrency:
group: ci-failure-monitor-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
actions: read
jobs:
failure-analysis:
if: github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.14'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Run Failure Analysis
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GH_PAT_FOR_RUNNER_ADMIN: ${{ secrets.GH_PAT_FOR_RUNNER_ADMIN }}
PYTHONUNBUFFERED: 1
PYTHONIOENCODING: utf-8
run: |
cd scripts/ci_monitor
python ci_failures_analysis.py \
--token $GITHUB_TOKEN \
--limit 100 \
--output ci_failure_analysis_$(date +%Y%m%d_%H%M%S).json
- name: Upload Analysis Results
uses: actions/upload-artifact@v4
with:
name: ci-failure-analysis-${{ github.run_number }}
path: |
scripts/ci_monitor/ci_failure_analysis_*.json
retention-days: 7
+68
View File
@@ -0,0 +1,68 @@
name: CI Model Inventory
# Builds models-per-stage.json: each NVIDIA (CUDA) CI suite -> the HuggingFace
# model ids its tests reference, for runner cache pre-warming. Pure static
# analysis (no GPU, no sglang install), so it stays fresh per push to main.
# Grab the result with:
# gh run download -R sgl-project/sglang --name models-per-stage
# or read the rendered table in the run's job summary.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- "test/registered/**"
- "test/run_suite.py"
- "python/sglang/test/test_utils.py"
- "python/sglang/test/lora_utils.py"
- "python/sglang/test/ci/**"
- "scripts/ci/list_stage_models.py"
- "scripts/ci/stage_models_overrides.json"
- "scripts/ci/runner_configs.yml"
- ".github/workflows/ci-model-inventory.yml"
pull_request:
# Validate the tool itself end-to-end when it changes. Deliberately NOT
# filtered on test/registered/** -- that would run on most PRs; the
# published inventory is refreshed by the push-to-main trigger instead.
paths:
- "scripts/ci/list_stage_models.py"
- "scripts/ci/test_list_stage_models.py"
- "scripts/ci/stage_models_overrides.json"
- ".github/workflows/ci-model-inventory.yml"
schedule:
# Nightly safety net (09:00 UTC) so the artifact never goes stale even if a
# change lands outside the path filter above.
- cron: "0 9 * * *"
jobs:
build-inventory:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Run extractor unit tests
run: python3 -m unittest discover -s scripts/ci -p 'test_list_stage_models.py'
- name: Generate model inventory
run: |
python3 scripts/ci/list_stage_models.py \
--backend cuda \
--commit "${{ github.sha }}" \
--output models-per-stage.json \
--markdown inventory.md
- name: Write job summary
run: cat inventory.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload inventory artifact
uses: actions/upload-artifact@v4
with:
name: models-per-stage
path: models-per-stage.json
retention-days: 90
@@ -0,0 +1,96 @@
name: Close Inactive Issues
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
contents: read
jobs:
close-inactive-issues:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
steps:
- name: Check and close inactive issues
uses: actions/github-script@v8
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const sixtyDaysAgo = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000);
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
console.log(`Owner: ${owner}, Repo: ${repo}`);
async function fetchIssues(page = 1) {
console.log(`Fetching issues for ${owner}/${repo}, page ${page}`);
return await github.rest.issues.listForRepo({
owner,
repo,
state: 'open',
sort: 'updated',
direction: 'asc',
per_page: 100,
page: page
});
}
async function processIssues() {
console.log('Starting to process issues');
console.log(`Repository: ${owner}/${repo}`);
let page = 1;
let hasMoreIssues = true;
while (hasMoreIssues) {
try {
const issues = await fetchIssues(page);
console.log(`Fetched ${issues.data.length} issues on page ${page}`);
if (issues.data.length === 0) {
hasMoreIssues = false;
break;
}
for (const issue of issues.data) {
// Skip if the issue has 'good first issue' label
if (issue.labels.some(label => label.name === 'good first issue')) {
console.log(`Skipping issue #${issue.number} as it's marked as 'good first issue'`);
continue;
}
if (new Date(issue.updated_at) < sixtyDaysAgo) {
try {
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: 'closed',
labels: [...issue.labels.map(l => l.name), 'inactive']
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: 'This issue has been automatically closed due to inactivity. Please feel free to reopen it if needed.'
});
console.log(`Closed issue #${issue.number} due to inactivity.`);
} catch (error) {
console.error(`Failed to close issue #${issue.number}: ${error.message}`);
}
} else {
console.log(`Issue #${issue.number} is still active. Stopping processing.`);
hasMoreIssues = false;
break;
}
}
page += 1;
} catch (error) {
console.error(`Error fetching issues on page ${page}: ${error.message}`);
hasMoreIssues = false;
}
}
console.log('Finished processing issues');
}
await processIssues();
@@ -0,0 +1,214 @@
name: Diffusion CI Ground Truth Generation (NPU)
on:
workflow_dispatch:
inputs:
ref:
description: 'Git ref to checkout'
required: false
default: ''
type: string
case_ids:
description: 'Specific case IDs to run (space-separated, optional)'
required: false
default: ''
type: string
output_name:
description: 'Custom local output/artifact folder name. Leave empty to use defaults.'
required: false
default: ''
type: string
publish_target_dir:
description: 'Remote target directory in sgl-project/ci-data. Leave empty to use diffusion-ci/consistency_gt/sglang_generated/ascend.'
required: false
default: ''
type: string
concurrency:
group: diffusion-ci-gt-gen-${{ github.ref }}-${{ inputs.output_name || inputs.case_ids || 'default' }}
cancel-in-progress: true
permissions:
contents: write
actions: read
env:
SGLANG_IS_IN_CI: true
OUTPUT_NAME: ${{ inputs.output_name || 'diffusion-ci-outputs' }}
PUBLISH_TARGET_DIR: ${{ inputs.publish_target_dir || 'diffusion-ci/consistency_gt/sglang_generated/ascend' }}
jobs:
compute-diffusion-partitions:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
matrix-1npu: ${{ steps.compute.outputs.matrix-1npu }}
matrix-2npu: ${{ steps.compute.outputs.matrix-2npu }}
partition-count-1npu: ${{ steps.compute.outputs['partition-count-1npu'] }}
partition-count-2npu: ${{ steps.compute.outputs['partition-count-2npu'] }}
plan-1npu: ${{ steps.compute.outputs.plan-1npu }}
plan-2npu: ${{ steps.compute.outputs.plan-2npu }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Compute partitions
env:
USE_NPU_CONFIGS: true
id: compute
run: |
python scripts/ci/utils/diffusion/compute_diffusion_partitions.py \
--min-time 1200 \
--target-time 1800 \
--max-time 2400 \
--max-partitions 10 \
--parametrized-only
multimodal-diffusion-gen-1npu:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-1npu != '{"include":[]}'
runs-on: linux-aarch64-a3-2
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-1npu) }}
timeout-minutes: 150
container:
image: swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-a3-ubuntu22.04-py3.11
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3 diffusion
- name: Generate outputs
env:
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-1npu }}
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 1-npu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-1npu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-1npu-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
multimodal-diffusion-gen-2npu:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-2npu != '{"include":[]}'
runs-on: linux-aarch64-a3-16
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-2npu) }}
timeout-minutes: 150
container:
image: swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-a3-ubuntu22.04-py3.11
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3 diffusion
- name: Generate outputs
env:
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-2npu }}
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 2-npu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-2npu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-2npu-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
+571
View File
@@ -0,0 +1,571 @@
name: Diffusion CI Ground Truth Generation
on:
workflow_dispatch:
inputs:
ref:
description: 'Git ref to checkout'
required: false
default: ''
type: string
case_ids:
description: 'Specific case IDs to run (space-separated, optional)'
required: false
default: ''
type: string
output_name:
description: 'Custom local output/artifact folder name. Leave empty to use defaults.'
required: false
default: ''
type: string
publish_target_dir:
description: 'Remote target directory in sgl-project/ci-data. Leave empty to use sglang_generated, or official_generated when run_official_cases is true.'
required: false
default: ''
type: string
kernel_artifact_run_id:
description: "Optional sgl-kernel wheel source: GitHub Actions run ID of a pr-test.yml run on the SAME commit as 'ref' that uploaded wheel-python3.10-cuda13.0. Use only when this branch's torch/ABI change makes the PyPI sglang-kernel wheel incompatible. Find it under Actions > pr-test; artifacts expire after 90 days."
required: false
default: ''
type: string
run_official_cases:
description: 'Run official comparable GT cases instead of native SGLang GT cases.'
required: false
default: false
type: boolean
official_case_ids:
description: 'Specific official case IDs to run (space-separated). Leave empty to run all official comparable cases.'
required: false
default: ''
type: string
official_source_group:
description: 'Official GT source group filter: all, diffusers, wan21, ltx, or ltx23. Used only when run_official_cases is true.'
required: false
default: ''
type: string
ci_data_ref:
description: 'ci-data ref to use for repro scripts when running official GT cases.'
required: false
default: 'main'
type: string
concurrency:
group: diffusion-ci-gt-gen-${{ github.ref }}-${{ inputs.output_name || inputs.case_ids || inputs.official_case_ids || inputs.official_source_group || inputs.run_official_cases || 'default' }}
cancel-in-progress: true
permissions:
contents: write
actions: read
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
OUTPUT_NAME: ${{ inputs.output_name || 'diffusion-ci-outputs' }}
PUBLISH_TARGET_DIR: ${{ inputs.publish_target_dir || (inputs.run_official_cases && 'diffusion-ci/consistency_gt/official_generated' || 'diffusion-ci/consistency_gt/sglang_generated') }}
jobs:
compute-official-gt-matrix:
if: github.repository == 'sgl-project/sglang' && inputs.run_official_cases
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.compute.outputs.matrix }}
case-count: ${{ steps.compute.outputs.case-count }}
steps:
- name: Compute official case matrix
id: compute
env:
OFFICIAL_CASE_IDS: ${{ inputs.official_case_ids }}
OFFICIAL_SOURCE_GROUP: ${{ inputs.official_source_group || 'all' }}
run: |
python3 - <<'PY'
import json
import os
groups = {
"diffusers": [
"flux_2_image_t2i",
"flux_2_klein_image_t2i",
"flux_2_klein_base_image_t2i",
"flux_2_ti2i",
"flux_image_t2i",
"qwen_image_edit_2509_ti2i",
"qwen_image_edit_2511_ti2i",
"qwen_image_edit_ti2i",
"qwen_image_layered_i2i",
"qwen_image_t2i",
"zimage_image_t2i",
],
"wan21": ["wan2_1_t2v_1.3b"],
"ltx": [
"ltx_2_two_stage_t2v",
"ltx_2.3_two_stage_t2v_2gpus",
"ltx_2_3_two_stage_ti2v_2gpus",
"ltx_2.3_one_stage_ti2v",
"ltx_2_3_hq_pipeline",
],
"ltx23": [
"ltx_2.3_two_stage_t2v_2gpus",
"ltx_2.3_one_stage_ti2v",
],
}
source_group = os.environ["OFFICIAL_SOURCE_GROUP"].strip() or "all"
if source_group == "all":
selected_groups = ["diffusers", "wan21", "ltx"]
elif source_group in groups:
selected_groups = [source_group]
else:
raise SystemExit(f"unknown official_source_group: {source_group}")
requested = os.environ["OFFICIAL_CASE_IDS"].split()
requested_set = set(requested)
h200_cases = {
"flux_2_image_t2i",
"flux_2_klein_image_t2i",
"flux_2_klein_base_image_t2i",
"flux_2_ti2i",
}
include = []
for group in selected_groups:
for case_id in groups[group]:
if requested_set and case_id not in requested_set:
continue
include.append(
{
"source_group": group,
"case_id": case_id,
"runner": "8-gpu-h200" if case_id in h200_cases else "1-gpu-h100",
}
)
known_cases = {case for cases in groups.values() for case in cases}
unknown = sorted(requested_set - known_cases)
if unknown:
raise SystemExit(f"unknown official case id(s): {' '.join(unknown)}")
if not include:
raise SystemExit("official case matrix is empty")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
f.write(f"matrix={json.dumps({'include': include}, separators=(',', ':'))}\n")
f.write(f"case-count={len(include)}\n")
PY
official-gt-hopper:
needs: compute-official-gt-matrix
if: needs.compute-official-gt-matrix.result == 'success'
runs-on: ${{ matrix.runner }}
timeout-minutes: 240
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-official-gt-matrix.outputs.matrix) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Checkout ci-data repro scripts
uses: actions/checkout@v4
with:
repository: sgl-project/ci-data
ref: ${{ inputs.ci_data_ref || 'main' }}
path: ci-data
token: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
sparse-checkout: |
diffusion-ci/repro_scripts
diffusion-ci/consistency_gt/official_generated/case_map.json
sparse-checkout-cone-mode: false
- name: Prepare sgl-kernel/dist for prebuilt wheel
if: inputs.kernel_artifact_run_id != ''
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download prebuilt sgl-kernel wheel
if: inputs.kernel_artifact_run_id != ''
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
name: wheel-python3.10-cuda13.0
run-id: ${{ inputs.kernel_artifact_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
CUSTOM_BUILD_SGL_KERNEL="${{ inputs.kernel_artifact_run_id != '' && 'true' || 'false' }}" \
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Install official LTX repro dependencies
if: matrix.source_group == 'ltx' || matrix.source_group == 'ltx23'
run: |
UV_SYSTEM_PYTHON=1 uv pip install \
"transformers==4.52.4" \
openimageio \
--index-strategy unsafe-best-match \
--prerelease allow
UV_SYSTEM_PYTHON=1 uv pip uninstall kernels
- name: Checkout official Wan2.1 repo
if: matrix.source_group == 'wan21'
run: |
mkdir -p /tmp/mmgen-official-code
if [ ! -d /tmp/mmgen-official-code/Wan2.1/.git ]; then
git clone https://github.com/Wan-Video/Wan2.1.git /tmp/mmgen-official-code/Wan2.1
fi
git -C /tmp/mmgen-official-code/Wan2.1 fetch --depth 1 origin 9737cba9c1c3c4d04b33fcad41c111989865d315
git -C /tmp/mmgen-official-code/Wan2.1 checkout 9737cba9c1c3c4d04b33fcad41c111989865d315
git -C /tmp/mmgen-official-code/Wan2.1 rev-parse HEAD
- name: Checkout official LTX-2 repo
if: matrix.source_group == 'ltx' || matrix.source_group == 'ltx23'
run: |
mkdir -p /tmp/mmgen-official-code
if [ ! -d /tmp/mmgen-official-code/LTX-2/.git ]; then
git clone https://github.com/Lightricks/LTX-2.git /tmp/mmgen-official-code/LTX-2
fi
git -C /tmp/mmgen-official-code/LTX-2 fetch --depth 1 origin 41d924371612b692c0fd1e4d9d94c3dfb3c02cb3
git -C /tmp/mmgen-official-code/LTX-2 checkout 41d924371612b692c0fd1e4d9d94c3dfb3c02cb3
git -C /tmp/mmgen-official-code/LTX-2 rev-parse HEAD
- name: Generate official output
env:
HF_TOKEN: ${{ secrets.SGLANG_DIFFUSION_CI_HF_TOKEN || secrets.HF_TOKEN }}
HUGGING_FACE_HUB_TOKEN: ${{ secrets.SGLANG_DIFFUSION_CI_HF_TOKEN || secrets.HF_TOKEN }}
RUNAI_STREAMER_MEMORY_LIMIT: 0
PYTORCH_CUDA_ALLOC_CONF: expandable_segments:True
CUDA_VISIBLE_DEVICES: 0
CASE_ID: ${{ matrix.case_id }}
SOURCE_GROUP: ${{ matrix.source_group }}
run: |
git -C ci-data rev-parse HEAD
mkdir -p "python/${{ env.OUTPUT_NAME }}"
if [ "$SOURCE_GROUP" = "diffusers" ]; then
cd python
python ../ci-data/diffusion-ci/repro_scripts/gen_official_diffusion_gt.py \
--suite 1-gpu \
--out-dir ./${{ env.OUTPUT_NAME }} \
--case-ids "$CASE_ID" \
--dtype bf16 \
--device-map none \
--generator-device cuda
elif [ "$SOURCE_GROUP" = "wan21" ]; then
cd python
python ../ci-data/diffusion-ci/repro_scripts/gen_official_diffusion_gt.py \
--suite 1-gpu \
--out-dir ./${{ env.OUTPUT_NAME }} \
--case-ids "$CASE_ID" \
--wan-official-repo-dir /tmp/mmgen-official-code/Wan2.1 \
--dtype bf16 \
--device-map none \
--generator-device cuda
elif [ "$SOURCE_GROUP" = "ltx" ] || [ "$SOURCE_GROUP" = "ltx23" ]; then
cd python
extra_ltx_args=()
if [ "$CASE_ID" = "ltx_2_3_hq_pipeline" ]; then
extra_ltx_args+=(--num-frames 24)
fi
set +e
PYTHONPATH=/tmp/mmgen-official-code/LTX-2/packages/ltx-core/src:/tmp/mmgen-official-code/LTX-2/packages/ltx-pipelines/src:$PWD:$PYTHONPATH \
python ../ci-data/diffusion-ci/repro_scripts/gen_official_ltx23.py \
--out-dir ./${{ env.OUTPUT_NAME }} \
--case-ids "$CASE_ID" \
"${extra_ltx_args[@]}"
status=$?
set -e
if [ "$status" -ne 0 ]; then
find ./${{ env.OUTPUT_NAME }} \( -name 'official_ltx_manifest.json' -o -name 'official_ltx23_manifest.json' \) -print -exec cat {} \;
exit "$status"
fi
else
echo "Unknown SOURCE_GROUP=$SOURCE_GROUP"
exit 1
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-${{ matrix.source_group }}-${{ matrix.case_id }}
path: |
python/${{ env.OUTPUT_NAME }}/*.jpg
python/${{ env.OUTPUT_NAME }}/*.png
python/${{ env.OUTPUT_NAME }}/official_gt_manifest_*.json
python/${{ env.OUTPUT_NAME }}/official_ltx_manifest.json
python/${{ env.OUTPUT_NAME }}/official_ltx23_manifest.json
retention-days: 7
- name: Validate generated GT images
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}" \
--check-only
- name: Publish official GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
compute-diffusion-partitions:
if: github.repository == 'sgl-project/sglang' && !inputs.run_official_cases
runs-on: ubuntu-latest
outputs:
matrix-1gpu: ${{ steps.compute.outputs.matrix-1gpu }}
matrix-2gpu: ${{ steps.compute.outputs.matrix-2gpu }}
matrix-b200: ${{ steps.compute.outputs.matrix-b200 }}
partition-count-1gpu: ${{ steps.compute.outputs['partition-count-1gpu'] }}
partition-count-2gpu: ${{ steps.compute.outputs['partition-count-2gpu'] }}
partition-count-b200: ${{ steps.compute.outputs['partition-count-b200'] }}
plan-1gpu: ${{ steps.compute.outputs.plan-1gpu }}
plan-2gpu: ${{ steps.compute.outputs.plan-2gpu }}
plan-b200: ${{ steps.compute.outputs.plan-b200 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Compute partitions
id: compute
run: |
python scripts/ci/utils/diffusion/compute_diffusion_partitions.py \
--min-time 1200 \
--target-time 1800 \
--max-time 2400 \
--max-partitions 10 \
--parametrized-only
multimodal-diffusion-gen-1gpu:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-1gpu != '{"include":[]}'
runs-on: 1-gpu-h100
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-1gpu) }}
timeout-minutes: 150
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Prepare sgl-kernel/dist for prebuilt wheel
if: inputs.kernel_artifact_run_id != ''
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download prebuilt sgl-kernel wheel
if: inputs.kernel_artifact_run_id != ''
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
name: wheel-python3.10-cuda13.0
run-id: ${{ inputs.kernel_artifact_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
CUSTOM_BUILD_SGL_KERNEL="${{ inputs.kernel_artifact_run_id != '' && 'true' || 'false' }}" \
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Generate outputs
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-1gpu }}
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 1-gpu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-1gpu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-1gpu-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Validate generated GT images
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}" \
--check-only
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
multimodal-diffusion-gen-2gpu:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-2gpu != '{"include":[]}'
runs-on: 2-gpu-h100
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-2gpu) }}
timeout-minutes: 150
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Prepare sgl-kernel/dist for prebuilt wheel
if: inputs.kernel_artifact_run_id != ''
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download prebuilt sgl-kernel wheel
if: inputs.kernel_artifact_run_id != ''
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
name: wheel-python3.10-cuda13.0
run-id: ${{ inputs.kernel_artifact_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
CUSTOM_BUILD_SGL_KERNEL="${{ inputs.kernel_artifact_run_id != '' && 'true' || 'false' }}" \
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Generate outputs
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-2gpu }}
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 2-gpu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-2gpu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-2gpu-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Validate generated GT images
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}" \
--check-only
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
multimodal-diffusion-gen-b200:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-b200 != '{"include":[]}'
runs-on: 4-gpu-b200
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-b200) }}
timeout-minutes: 240
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Prepare sgl-kernel/dist for prebuilt wheel
if: inputs.kernel_artifact_run_id != ''
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download prebuilt sgl-kernel wheel
if: inputs.kernel_artifact_run_id != ''
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
name: wheel-python3.10-cuda13.0
run-id: ${{ inputs.kernel_artifact_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
CUSTOM_BUILD_SGL_KERNEL="${{ inputs.kernel_artifact_run_id != '' && 'true' || 'false' }}" \
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Generate outputs
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-b200 }}
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 1-gpu-b200 \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-b200'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-b200-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Validate generated GT images
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}" \
--check-only
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
+359
View File
@@ -0,0 +1,359 @@
name: Full Test (NPU)
on:
# pull_request:
# branches:
# - main
# paths:
# - ".github/workflows/full-test-npu.yml"
workflow_dispatch:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
image_a3:
description: 'The a3 running docker image of the test task.'
required: false
type: string
default: 'swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-a3-ubuntu22.04-py3.11'
skip_install_flag:
description: 'Indicates whether to skip the installation of sglang, defaulting to false.'
required: false
type: string
default: 'false'
concurrency:
group: full-test-npu-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
set-image-config:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.set-vars.outputs.ref }}
job_filter: ${{ steps.set-vars.outputs.job_filter }}
image_a3: ${{ steps.set-vars.outputs.image_a3 }}
skip_install_flag: ${{ steps.set-vars.outputs.skip_install_flag }}
steps:
# When triggered by PR, no inputs parameters are used. The latest community code is tested by default.
- name: Set image config
id: set-vars
run: |
if [ -z "${{ inputs.ref }}" ]; then
echo "ref=" >> $GITHUB_OUTPUT
else
echo "ref=${{ inputs.ref }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.job_filter }}" ]; then
echo "job_filter=all" >> $GITHUB_OUTPUT
else
echo "job_filter=${{ inputs.job_filter }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.image_a3 }}" ]; then
echo "image_a3=swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:8.5.0-a3-ubuntu22.04-py3.11" >> $GITHUB_OUTPUT
else
echo "image_a3=${{ inputs.image_a3 }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.skip_install_flag }}" ]; then
echo "skip_install_flag=false" >> $GITHUB_OUTPUT
else
echo "skip_install_flag=${{ inputs.skip_install_flag }}" >> $GITHUB_OUTPUT
fi
nighly-test-npu:
needs: [set-image-config]
name: nightly-test-npu
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
uses: ./.github/workflows/nightly-test-npu.yml
with:
ref: ${{ needs.set-image-config.outputs.ref }}
job_filter: ${{ needs.set-image-config.outputs.job_filter }}
image_a3: ${{ needs.set-image-config.outputs.image_a3 }}
skip_install_flag: ${{ needs.set-image-config.outputs.skip_install_flag }}
secrets: inherit
full-1-npu-a3:
needs: [set-image-config]
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
runs-on: linux-aarch64-a3-2
container:
image: ${{ needs.set-image-config.outputs.image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.set-image-config.outputs.ref || github.ref }}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
if [ ${{ needs.set-image-config.outputs.skip_install_flag }} != "true" ];then
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
fi
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Print Log Information
run: |
bash scripts/ci/npu/npu_log_print.sh
- name: Run test
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
pip install sglang_router
hf download lmms-lab/MMMU --repo-type dataset
pip install sentence_transformers torchaudio==2.8.0
pip install protobuf==6.31.1 zss pre-commit wandb>=0.16.0 tenacity==8.3.0 loguru openpyxl latex2sympy2 zstandard transformers-stream-generator tqdm-multiprocess pycocoevalcap
pip install yt-dlp sentencepiece==0.1.99 nltk av ftfy sqlitedict==2.1.0 sacrebleu>=1.5.0 pytablewriter black==24.1.0 isort==5.13.2 peft>=0.2.0 accelerate>=0.29.1
pip install jsonlines httpx==0.25.0 evaluate>=0.4.0 datasets==2.16.1 numexpr xgrammar==0.2.1 numpy==1.26.4 dotenv
git clone --branch v0.3.3 --depth 1 https://github.com/EvolvingLMMs-Lab/lmms-eval.git
cd ./lmms-eval
nohup pip install . > lmmslog.txt 2>&1 &
sleep 120
export PYTHONPATH=$PYTHONPATH:$(pwd)
cd ../
cd test
python3 run_suite.py --hw npu --suite full-1-npu-a3 --nightly --continue-on-error --timeout-per-file 3600
full-2-npu-a3:
needs: [set-image-config]
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
runs-on: linux-aarch64-a3-2
container:
image: ${{ needs.set-image-config.outputs.image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.set-image-config.outputs.ref || github.ref }}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
if [ ${{ needs.set-image-config.outputs.skip_install_flag }} != "true" ];then
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
fi
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Print Log Information
run: |
bash scripts/ci/npu/npu_log_print.sh
- name: Run test
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
pip install sglang_router
hf download lmms-lab/MMMU --repo-type dataset
pip install sentence_transformers torchaudio==2.8.0
pip install protobuf==6.31.1 zss pre-commit wandb>=0.16.0 tenacity==8.3.0 loguru openpyxl latex2sympy2 zstandard transformers-stream-generator tqdm-multiprocess pycocoevalcap
pip install yt-dlp sentencepiece==0.1.99 nltk av ftfy sqlitedict==2.1.0 sacrebleu>=1.5.0 pytablewriter black==24.1.0 isort==5.13.2 peft>=0.2.0 accelerate>=0.29.1
pip install jsonlines httpx==0.25.0 evaluate>=0.4.0 datasets==2.16.1 numexpr xgrammar==0.2.1 numpy==1.26.4 dotenv
git clone --branch v0.3.3 --depth 1 https://github.com/EvolvingLMMs-Lab/lmms-eval.git
cd ./lmms-eval
nohup pip install . > lmmslog.txt 2>&1 &
sleep 120
export PYTHONPATH=$PYTHONPATH:$(pwd)
cd ../
cd test
python3 run_suite.py --hw npu --suite full-2-npu-a3 --nightly --continue-on-error --timeout-per-file 3600
full-4-npu-a3:
needs: [set-image-config]
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
runs-on: linux-aarch64-a3-4
container:
image: ${{ needs.set-image-config.outputs.image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.set-image-config.outputs.ref || github.ref }}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
if [ ${{ needs.set-image-config.outputs.skip_install_flag }} != "true" ];then
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
fi
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Print Log Information
run: |
bash scripts/ci/npu/npu_log_print.sh
- name: Run test
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
pip install sglang_router
hf download lmms-lab/MMMU --repo-type dataset
pip install sentence_transformers torchaudio==2.8.0
pip install protobuf==6.31.1 zss pre-commit wandb>=0.16.0 tenacity==8.3.0 loguru openpyxl latex2sympy2 zstandard transformers-stream-generator tqdm-multiprocess pycocoevalcap
pip install yt-dlp sentencepiece==0.1.99 nltk av ftfy sqlitedict==2.1.0 sacrebleu>=1.5.0 pytablewriter black==24.1.0 isort==5.13.2 peft>=0.2.0 accelerate>=0.29.1
pip install jsonlines httpx==0.25.0 evaluate>=0.4.0 datasets==2.16.1 numexpr xgrammar==0.2.1 numpy==1.26.4 dotenv
git clone --branch v0.3.3 --depth 1 https://github.com/EvolvingLMMs-Lab/lmms-eval.git
cd ./lmms-eval
nohup pip install . > lmmslog.txt 2>&1 &
sleep 120
export PYTHONPATH=$PYTHONPATH:$(pwd)
cd ../
cd test
python3 run_suite.py --hw npu --suite full-4-npu-a3 --nightly --continue-on-error --timeout-per-file 3600
full-16-npu-a3:
needs: [set-image-config]
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
runs-on: linux-aarch64-a3-16
container:
image: ${{ needs.set-image-config.outputs.image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.set-image-config.outputs.ref || github.ref }}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
if [ ${{ needs.set-image-config.outputs.skip_install_flag }} != "true" ];then
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
fi
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Print Log Information
run: |
bash scripts/ci/npu/npu_log_print.sh
- name: Run test
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
pip install sglang_router
hf download lmms-lab/MMMU --repo-type dataset
pip install sentence_transformers torchaudio==2.8.0
pip install protobuf==6.31.1 zss pre-commit wandb>=0.16.0 tenacity==8.3.0 loguru openpyxl latex2sympy2 zstandard transformers-stream-generator tqdm-multiprocess pycocoevalcap
pip install yt-dlp sentencepiece==0.1.99 nltk av ftfy sqlitedict==2.1.0 sacrebleu>=1.5.0 pytablewriter black==24.1.0 isort==5.13.2 peft>=0.2.0 accelerate>=0.29.1
pip install jsonlines httpx==0.25.0 evaluate>=0.4.0 datasets==2.16.1 numexpr xgrammar==0.2.1 numpy==1.26.4 dotenv
git clone --branch v0.3.3 --depth 1 https://github.com/EvolvingLMMs-Lab/lmms-eval.git
cd ./lmms-eval
nohup pip install . > lmmslog.txt 2>&1 &
sleep 120
export PYTHONPATH=$PYTHONPATH:$(pwd)
cd ../
cd test
python3 run_suite.py --hw npu --suite full-16-npu-a3 --nightly --continue-on-error --timeout-per-file 3600
check-all-jobs:
if: github.repository == 'sgl-project/sglang' && always()
needs:
- nighly-test-npu
- full-1-npu-a3
- full-2-npu-a3
- full-4-npu-a3
- full-16-npu-a3
runs-on: ubuntu-latest
container:
image: docker.m.daocloud.io/ubuntu:22.04
steps:
- name: Check if any job failed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more nightly test jobs failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more nightly test jobs were cancelled"
exit 1
fi
echo "All nightly test jobs passed"
+20
View File
@@ -0,0 +1,20 @@
name: Auto Label PRs
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Auto-label by file changes
uses: actions/labeler@v5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/labeler.yml
sync-labels: false
+63
View File
@@ -0,0 +1,63 @@
name: Lint
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Reject changes under legacy docs/
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
BASE_REF="${{ github.base_ref }}"
# Refresh origin/<base_ref> with full history; --depth=1 would
# shallow the ref and break the merge-base used by `...`.
git fetch --no-tags origin "$BASE_REF"
# First, verify the diff itself succeeds and check whether any
# files changed. A silent failure here would let docs/ changes
# through, so the explicit `if !` guard is important.
if ! CHANGED=$(git diff --name-only --diff-filter=ACMRDTUXB "origin/${BASE_REF}...HEAD"); then
echo "git diff origin/${BASE_REF}...HEAD failed; aborting." >&2
exit 2
fi
if [ -z "$CHANGED" ]; then
echo "No changed files vs origin/${BASE_REF}; skipping."
exit 0
fi
# Re-emit with -z so xargs can safely handle whitespace in paths.
git diff -z --name-only --diff-filter=ACMRDTUXB "origin/${BASE_REF}...HEAD" \
| xargs -0 python3 scripts/ci/check_no_docs_changes.py
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install pre-commit hook
run: |
python -m pip install pre-commit
pre-commit install
- name: Run pre-commit checks
run: SKIP=no-commit-to-branch pre-commit run --all-files --show-diff-on-failure
- name: Run lychee docs checks (offline references)
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
with:
args: --config .github/linters/lychee.toml README.md "docs/**/*.md" "docs/**/*.rst" "docs/**/*.ipynb"
- name: Run sgl-kernel clang-format checks
uses: DoozyX/clang-format-lint-action@v0.20
with:
source: sgl-kernel
extensions: h,c,cpp,hpp,cu,cuh,cc
clangFormatVersion: 20
style: file
+317
View File
@@ -0,0 +1,317 @@
name: List Active Runs
on:
workflow_dispatch:
inputs:
workflows:
description: 'Space-separated list of workflow filenames to check'
required: false
type: string
default: 'pr-test.yml'
permissions:
actions: read
contents: read
pull-requests: read
jobs:
list-active-runs:
runs-on: ubuntu-latest
steps:
- name: Install GitHub CLI
run: sudo apt-get install -y gh jq
- name: List active runs grouped by PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
WORKFLOWS: ${{ github.event.inputs.workflows || 'pr-test.yml' }}
shell: bash
run: |
set -euo pipefail
echo "========================================="
echo "🔍 Active Workflow Runs Report"
echo "========================================="
echo ""
# Get all workflows or specific ones
read -r -a workflow_files <<< "${WORKFLOWS}"
echo "📋 Checking specified workflows: ${WORKFLOWS}"
echo ""
# Create a temporary file to store PR data
pr_data_file=$(mktemp)
# Process each workflow
for workflow_file in ${workflow_files[@]}; do
echo "Scanning workflow: $workflow_file"
# Get all active runs (queued, waiting, in_progress)
active_runs=$(gh run list \
--repo "$REPO" \
--workflow "$workflow_file" \
--json databaseId,status,event,headBranch,createdAt,updatedAt,headSha,number,attempt \
--limit 500 \
| jq -c '.[] | select(.status=="queued" or .status=="waiting" or .status=="in_progress")')
if [ -z "$active_runs" ]; then
continue
fi
# Process each run
echo "$active_runs" | while read -r run; do
run_id=$(echo "$run" | jq -r '.databaseId')
run_status=$(echo "$run" | jq -r '.status')
run_event=$(echo "$run" | jq -r '.event')
created_at=$(echo "$run" | jq -r '.createdAt')
head_sha=$(echo "$run" | jq -r '.headSha')
run_number=$(echo "$run" | jq -r '.number')
run_attempt=$(echo "$run" | jq -r '.attempt // 1')
# Get detailed run information including jobs
run_details=$(gh api "repos/$REPO/actions/runs/$run_id" 2>/dev/null || true)
if [ -z "$run_details" ]; then
continue
fi
head_owner=$(echo "$run_details" | jq -r '.head_repository.owner.login // empty')
head_branch=$(echo "$run_details" | jq -r '.head_branch // empty')
if [ -z "$head_owner" ] || [ -z "$head_branch" ]; then
continue
fi
# Find PR number (may be empty for non-PR runs)
pr_number=$(gh api "repos/$REPO/pulls?state=open&head=${head_owner}:${head_branch}" \
--jq '.[0].number // empty' 2>/dev/null || true)
if [ -z "$pr_number" ]; then
pr_number="NO_PR"
fi
# Get jobs for this run (with pagination to avoid missing jobs)
jobs=$(gh api "repos/$REPO/actions/runs/$run_id/jobs" --paginate --jq '.jobs[]' | jq -s '.')
running_jobs=$(echo "$jobs" | jq '[.[] | select(.status=="in_progress")] | length')
queued_jobs=$(echo "$jobs" | jq '[.[] | select(.status=="queued" or .status=="waiting")] | length')
# Get runner info for running jobs
runners=$(echo "$jobs" | jq -r '.[] | select(.status=="in_progress") | .runner_name // "N/A"' | paste -sd "," -)
# Calculate queue time
current_time=$(date -u +%s)
created_time=$(date -u -d "$created_at" +%s 2>/dev/null || echo "$current_time")
queue_time=$((current_time - created_time))
queue_minutes=$((queue_time / 60))
# Store data in temporary file (unified format with event and branch)
echo "$pr_number|$workflow_file|$run_id|$run_status|$running_jobs|$queued_jobs|$runners|$queue_minutes|$created_at|$head_sha|$run_attempt|$run_event|$head_branch" >> "$pr_data_file"
done
done
echo ""
echo "========================================="
echo "📊 Active Runs Summary"
echo "========================================="
echo ""
if [ ! -s "$pr_data_file" ]; then
echo "✅ No active runs found"
rm -f "$pr_data_file"
exit 0
fi
# Get unique PR numbers (exclude NO_PR entries)
pr_numbers=$(cut -d'|' -f1 < "$pr_data_file" | grep -v '^NO_PR$' | sort -u || true)
# Separate high priority and normal PRs
high_priority_prs=()
normal_prs=()
for pr_num in $pr_numbers; do
labels=$(gh pr view "$pr_num" --repo "$REPO" --json labels \
| jq -r '.labels[].name' 2>/dev/null || true)
if echo "$labels" | grep -Fxq "high priority"; then
high_priority_prs+=($pr_num)
else
normal_prs+=($pr_num)
fi
done
# Combine: high priority first, then normal
sorted_pr_numbers=("${high_priority_prs[@]}" "${normal_prs[@]}")
pr_count=0
total_running=0
total_queued=0
for pr_num in "${sorted_pr_numbers[@]}"; do
pr_count=$((pr_count + 1))
# Get PR details
pr_info=$(gh pr view "$pr_num" --repo "$REPO" --json title,author,labels,url 2>/dev/null || true)
if [ -z "$pr_info" ]; then
continue
fi
pr_title=$(echo "$pr_info" | jq -r '.title')
pr_author=$(echo "$pr_info" | jq -r '.author.login')
pr_url=$(echo "$pr_info" | jq -r '.url')
pr_labels=$(echo "$pr_info" | jq -r '.labels[].name' | paste -sd ", " -)
if [ -z "$pr_labels" ]; then
pr_labels="(no labels)"
fi
# Add priority indicator
priority_indicator=""
if echo "$pr_labels" | grep -q "high priority"; then
priority_indicator="🔴 [HIGH PRIORITY] "
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔗 ${priority_indicator}PR #$pr_num: $pr_title"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "👤 Author: $pr_author"
echo "🏷️ Labels: $pr_labels"
echo "🔗 URL: $pr_url"
echo ""
# Get all runs for this PR
pr_runs=$(grep "^$pr_num|" "$pr_data_file")
pr_running_total=0
pr_queued_total=0
echo "$pr_runs" | while read -r line; do
workflow=$(echo "$line" | cut -d'|' -f2)
run_id=$(echo "$line" | cut -d'|' -f3)
status=$(echo "$line" | cut -d'|' -f4)
running=$(echo "$line" | cut -d'|' -f5)
queued=$(echo "$line" | cut -d'|' -f6)
runners=$(echo "$line" | cut -d'|' -f7)
queue_min=$(echo "$line" | cut -d'|' -f8)
created=$(echo "$line" | cut -d'|' -f9)
attempt=$(echo "$line" | cut -d'|' -f11)
pr_running_total=$((pr_running_total + running))
pr_queued_total=$((pr_queued_total + queued))
run_url="https://github.com/$REPO/actions/runs/$run_id"
# Calculate retry count for this specific run
retry_count=$((attempt - 1))
# Show retry indicator
retry_indicator=""
if [ "$retry_count" -gt 0 ]; then
retry_indicator=" 🔄 Retry #$retry_count"
fi
echo " 📦 Workflow: $workflow (Run #$run_id)$retry_indicator"
echo " Status: $status"
echo " 🟢 Running jobs: $running"
echo " 🟡 Queued jobs: $queued"
if [ "$running" -gt 0 ] && [ "$runners" != "" ]; then
echo " 🖥️ Runners: $runners"
fi
if [ "$queue_min" -gt 0 ]; then
echo " ⏱️ Queue time: ${queue_min} minutes"
fi
echo " 🔗 Run URL: $run_url"
echo ""
done
# Summary for this PR
pr_running_total=$(grep "^$pr_num|" "$pr_data_file" | cut -d'|' -f5 | awk '{sum+=$1} END {print sum+0}')
pr_queued_total=$(grep "^$pr_num|" "$pr_data_file" | cut -d'|' -f6 | awk '{sum+=$1} END {print sum+0}')
total_running=$((total_running + pr_running_total))
total_queued=$((total_queued + pr_queued_total))
echo " 📊 PR Total: $pr_running_total running, $pr_queued_total queued"
echo ""
done
# --- Non-PR Runs Section ---
non_pr_runs=$(grep '^NO_PR|' "$pr_data_file" 2>/dev/null || true)
non_pr_running=0
non_pr_queued=0
if [ -n "$non_pr_runs" ]; then
echo "========================================="
echo "📦 Non-PR Runs (manual / scheduled / other)"
echo "========================================="
echo ""
echo "$non_pr_runs" | while read -r line; do
workflow=$(echo "$line" | cut -d'|' -f2)
run_id=$(echo "$line" | cut -d'|' -f3)
status=$(echo "$line" | cut -d'|' -f4)
running=$(echo "$line" | cut -d'|' -f5)
queued=$(echo "$line" | cut -d'|' -f6)
runners=$(echo "$line" | cut -d'|' -f7)
queue_min=$(echo "$line" | cut -d'|' -f8)
created=$(echo "$line" | cut -d'|' -f9)
attempt=$(echo "$line" | cut -d'|' -f11)
event=$(echo "$line" | cut -d'|' -f12)
branch=$(echo "$line" | cut -d'|' -f13)
run_url="https://github.com/$REPO/actions/runs/$run_id"
retry_count=$((attempt - 1))
retry_indicator=""
if [ "$retry_count" -gt 0 ]; then
retry_indicator=" 🔄 Retry #$retry_count"
fi
echo " 📦 Workflow: $workflow (Run #$run_id)$retry_indicator"
echo " Event: $event"
echo " Branch: $branch"
echo " Status: $status"
echo " 🟢 Running jobs: $running"
echo " 🟡 Queued jobs: $queued"
if [ "$running" -gt 0 ] && [ "$runners" != "" ]; then
echo " 🖥️ Runners: $runners"
fi
if [ "$queue_min" -gt 0 ]; then
echo " ⏱️ Queue time: ${queue_min} minutes"
fi
echo " 🔗 Run URL: $run_url"
echo ""
done
non_pr_running=$(echo "$non_pr_runs" | cut -d'|' -f5 | awk '{sum+=$1} END {print sum+0}')
non_pr_queued=$(echo "$non_pr_runs" | cut -d'|' -f6 | awk '{sum+=$1} END {print sum+0}')
non_pr_count=$(echo "$non_pr_runs" | wc -l | tr -d ' ')
total_running=$((total_running + non_pr_running))
total_queued=$((total_queued + non_pr_queued))
echo " 📊 Non-PR Total: $non_pr_running running, $non_pr_queued queued"
echo ""
fi
# Overall summary
echo "========================================="
echo "📈 Overall Summary"
echo "========================================="
echo "Total PRs with active runs: $pr_count"
echo "Total non-PR active runs: ${non_pr_count:-0}"
echo "Total running jobs: $total_running"
echo "Total queued jobs: $total_queued"
echo "========================================="
# Cleanup
rm -f "$pr_data_file"
+466
View File
@@ -0,0 +1,466 @@
name: Nightly Test (GB200 72GPU)
# NOTE: Nightly (schedule) runs require no approval.
# Manual (workflow_dispatch) runs are gated by the gb200-ci environment
# to prevent individuals from queuing arbitrary jobs on the shared GB200 cluster.
on:
schedule:
- cron: '0 2 * * *' # 2 AM UTC daily (offset from other nightly runs)
workflow_dispatch: # allow manual trigger; gated by gb200-ci environment
inputs:
image:
description: 'Optional. SGLang Docker image to benchmark. Leave empty for the default nightly image. Mutually exclusive with pr_number and sglang_branch.'
required: false
default: ''
pr_number:
description: 'Optional. PR number to build from (works for PRs from forks too, via refs/pull/<N>/head). Preferred over sglang_branch when a PR exists. Mutually exclusive with image and sglang_branch.'
required: false
default: ''
sglang_branch:
description: 'Optional. Branch name on sgl-project/sglang to build from (use when no PR is open yet). For fork branches, open a PR and use pr_number instead. Mutually exclusive with image and pr_number.'
required: false
default: ''
configs:
description: 'Optional. Comma-separated names to run only a subset. Format: {model-prefix}-{precision}-{isl}{osl}-{recipe}. E.g. "dsr1-fp8-1k1k-max-tpt" or "dsr1-fp8-1k1k-max-tpt,dsr1-fp4-1k1k-mid-curve". Leave empty to run all. Available names are listed in the setup job log.'
required: false
default: ''
concurrency:
group: nightly-test-gb200
cancel-in-progress: false
env:
SGLANG_IS_IN_CI: true
SGLANG_ENABLE_ASYNC_ASSERT: true
SRT_SLURM_BRANCH: sglang-nightly-regression
SLURM_PARTITION: batch
SLURM_ACCOUNT: sglang
# Docker Hub repo for ephemeral branch/PR build images (kept separate from
# the released `lmsysorg/sglang` repo). Cleaned up by `cleanup-image`.
CI_IMAGE_REPO: lmsysorg/sglang-staging
# How many most recent staging tags to retain after each run.
CI_IMAGE_KEEP_TAGS: 60
jobs:
# ---------------------------------------------------------------------------
# Reject conflicting inputs early. At most one of `image`, `pr_number`,
# `sglang_branch` may be set — they select different image sources. Only runs
# on manual dispatch; all downstream jobs chain through this so invalid
# inputs halt the pipeline before cluster resources are reserved.
# ---------------------------------------------------------------------------
validate-inputs:
if: github.repository == 'sgl-project/sglang' && github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Reject conflicting inputs
run: |
IMAGE="${{ inputs.image }}"
PR="${{ inputs.pr_number }}"
BRANCH="${{ inputs.sglang_branch }}"
sources=0
[ -n "$IMAGE" ] && sources=$((sources + 1))
[ -n "$PR" ] && sources=$((sources + 1))
[ -n "$BRANCH" ] && sources=$((sources + 1))
if [ "$sources" -gt 1 ]; then
echo "::error::Specify at most one of 'image' ('$IMAGE'), 'pr_number' ('$PR'), or 'sglang_branch' ('$BRANCH')."
exit 1
fi
if [ -n "$PR" ] && ! echo "$PR" | grep -Eq '^[0-9]+$'; then
echo "::error::pr_number must be a positive integer, got '$PR'."
exit 1
fi
# ---------------------------------------------------------------------------
# Reads scripts/ci/slurm/nightly-configs.yaml and generates one matrix entry
# per recipe YAML. Each job runs the full concurrency sweep defined in the
# recipe as a single Slurm job.
# To add/remove configs, edit nightly-configs.yaml only.
# ---------------------------------------------------------------------------
setup:
needs: validate-inputs
# Run if validate-inputs succeeded (dispatch) or was skipped (cron).
if: |
always() && github.repository == 'sgl-project/sglang'
&& (needs.validate-inputs.result == 'success' || needs.validate-inputs.result == 'skipped')
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate benchmark matrix
id: generate
env:
CONFIGS_FILTER: ${{ inputs.configs }}
run: |
pip install pyyaml -q
# List all available config names first so they're visible in logs
# even when a filter rejects an unknown name.
ALL_MATRIX=$(python3 scripts/ci/slurm/generate_matrix.py \
scripts/ci/slurm/nightly-configs.yaml --runner gb200)
echo "Available config names for runner gb200:"
echo "$ALL_MATRIX" | python3 -c "import json,sys; [print(f' - {e[\"name\"]}') for e in json.load(sys.stdin)]"
FILTER_ARG=()
if [ -n "$CONFIGS_FILTER" ]; then
echo ""
echo "Filtering to: $CONFIGS_FILTER"
FILTER_ARG=(--filter "$CONFIGS_FILTER")
fi
MATRIX=$(python3 scripts/ci/slurm/generate_matrix.py \
scripts/ci/slurm/nightly-configs.yaml --runner gb200 "${FILTER_ARG[@]}")
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
# ---------------------------------------------------------------------------
# When pr_number or sglang_branch is provided, build an ARM64 (GB200) image
# from that ref and push it to Docker Hub under lmsysorg/sglang-staging.
# Uses refs/pull/<N>/head for PRs so fork PRs work without cross-repo auth.
# Old staging tags are pruned by `cleanup-image` at the end of the run.
# Skipped on nightly (cron) runs and manual runs with neither pr_number nor
# sglang_branch.
# ---------------------------------------------------------------------------
build-image:
needs: [validate-inputs, setup]
if: |
github.repository == 'sgl-project/sglang' && github.event_name == 'workflow_dispatch'
&& (inputs.pr_number != '' || inputs.sglang_branch != '')
runs-on: arm-docker-build-node
outputs:
image_ref: ${{ steps.build.outputs.image_ref }}
image_tag: ${{ steps.build.outputs.image_tag }}
steps:
# Self-hosted runners retain the workspace across jobs. Prior `docker buildx`
# runs on this node leave root-owned build artifacts (e.g. sgl-kernel/build/)
# that actions/checkout cannot remove, causing EACCES on rmdir. Wipe them
# via a throwaway root container before checkout recreates the workspace.
- name: Clean workspace (remove root-owned files from prior runs)
run: |
docker run --rm -v "${{ github.workspace }}:/workspace" alpine:3 \
sh -c 'rm -rf /workspace/..?* /workspace/.[!.]* /workspace/*' || true
- name: Checkout code
uses: actions/checkout@v4
with:
# PRs (including fork PRs) resolve via refs/pull/<N>/head on upstream.
# Otherwise fall back to the branch name on sgl-project/sglang.
ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || inputs.sglang_branch }}
- name: Verify checkout
env:
PR_NUMBER: ${{ inputs.pr_number }}
BRANCH: ${{ inputs.sglang_branch }}
run: |
SHA=$(git rev-parse HEAD)
echo "Commit SHA: $SHA"
echo "Author: $(git log -1 --format='%an <%ae>')"
echo "Date: $(git log -1 --format='%aI')"
echo "Subject: $(git log -1 --format='%s')"
echo ""
if [ -n "$PR_NUMBER" ]; then
echo "Cross-check: https://github.com/sgl-project/sglang/pull/${PR_NUMBER}/commits"
else
echo "Cross-check: https://github.com/sgl-project/sglang/commits/${BRANCH}"
fi
echo "Commit URL: https://github.com/sgl-project/sglang/commit/${SHA}"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push ARM64 image
id: build
run: |
if [ -n "${{ inputs.pr_number }}" ]; then
TAG_STUB="pr-${{ inputs.pr_number }}"
SOURCE_DESC="PR #${{ inputs.pr_number }}"
else
TAG_STUB=$(echo "${{ inputs.sglang_branch }}" | tr '/' '-' | tr -cd '[:alnum:]._-')
SOURCE_DESC="branch ${{ inputs.sglang_branch }}"
fi
# run_attempt disambiguates "Re-run jobs" so the squash filename
# (derived from the image URL) doesn't collide with a stale one.
TAG="${TAG_STUB}-${{ github.run_id }}-${{ github.run_attempt }}"
IMAGE_REF="${CI_IMAGE_REPO}:${TAG}"
echo "Building ${IMAGE_REF} from ${SOURCE_DESC}"
docker buildx build \
--platform linux/arm64 \
--output type=image,name=${IMAGE_REF},push=true \
--target framework_final \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=13.0.1 \
--build-arg BUILD_TYPE=all \
--build-arg CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) \
--build-arg GRACE_BLACKWELL=1 \
--build-arg BRANCH_TYPE=local \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
--no-cache \
.
echo "image_ref=${IMAGE_REF}" >> $GITHUB_OUTPUT
echo "image_tag=${TAG}" >> $GITHUB_OUTPUT
# ---------------------------------------------------------------------------
# Import Docker images to Lustre squash files once before all benchmark jobs.
# This avoids parallel jobs racing to enroot import the same image.
# When build-image ran, we import the freshly built Docker Hub staging image
# (lmsysorg/sglang-staging is public → no auth needed for enroot pull).
# Otherwise we use the `image` input (or its default public nightly image).
# ---------------------------------------------------------------------------
prepare-image:
needs: [setup, build-image]
if: |
always() && github.repository == 'sgl-project/sglang'
&& needs.setup.result == 'success'
&& (needs.build-image.result == 'success' || needs.build-image.result == 'skipped')
environment: ${{ github.event_name == 'workflow_dispatch' && 'gb200-ci' || '' }}
runs-on: 72-gpu-gb200
outputs:
squash_file: ${{ steps.import.outputs.squash_file }}
nginx_squash_file: ${{ steps.import.outputs.nginx_squash_file }}
image: ${{ steps.resolve.outputs.image }}
env:
NGINX_IMAGE: nginx:1.27.4
steps:
- name: Resolve image to import
id: resolve
run: |
BUILT_IMAGE="${{ needs.build-image.outputs.image_ref }}"
if [ -n "$BUILT_IMAGE" ]; then
echo "Using freshly built image: $BUILT_IMAGE"
echo "image=$BUILT_IMAGE" >> $GITHUB_OUTPUT
else
IMAGE="${{ inputs.image || 'lmsysorg/sglang:dev-cu13' }}"
echo "Using pre-existing image: $IMAGE"
echo "image=$IMAGE" >> $GITHUB_OUTPUT
fi
- name: Import Docker images to Lustre
id: import
env:
IMAGE: ${{ steps.resolve.outputs.image }}
run: |
SQUASH_FILE="/mnt/lustre01/users-public/sglang-ci/$(echo "$IMAGE" | sed 's/[\/:@#]/_/g')_$(date +%Y%m%d).sqsh"
NGINX_SQUASH_FILE="/mnt/lustre01/users-public/sglang-ci/$(echo "$NGINX_IMAGE" | sed 's/[\/:@#]/_/g').sqsh"
if [ -f "$SQUASH_FILE" ]; then
echo "Squash file already exists, skipping import: $SQUASH_FILE"
else
enroot import -o "$SQUASH_FILE" "docker://$IMAGE"
fi
if [ -f "$NGINX_SQUASH_FILE" ]; then
echo "Nginx squash file already exists, skipping import: $NGINX_SQUASH_FILE"
else
enroot import -o "$NGINX_SQUASH_FILE" "docker://$NGINX_IMAGE"
fi
echo "squash_file=$SQUASH_FILE" >> $GITHUB_OUTPUT
echo "nginx_squash_file=$NGINX_SQUASH_FILE" >> $GITHUB_OUTPUT
nightly-gb200-benchmark:
needs: [setup, prepare-image]
# Use always() + explicit success checks so a skipped transitive upstream
# (e.g. build-image when neither pr_number nor sglang_branch is set) does
# not propagate a skip to this job. Direct deps must still have succeeded.
if: |
always() && github.repository == 'sgl-project/sglang'
&& needs.setup.result == 'success'
&& needs.prepare-image.result == 'success'
runs-on: 72-gpu-gb200
strategy:
fail-fast: false
matrix:
config: ${{ fromJson(needs.setup.outputs.matrix) }}
env:
FRAMEWORK: dynamo-sglang
MODEL: ${{ matrix.config.model }}
MODEL_PREFIX: ${{ matrix.config.model_prefix }}
PRECISION: ${{ matrix.config.precision }}
ISL: ${{ matrix.config.isl }}
OSL: ${{ matrix.config.osl }}
CONFIG_FILE: ${{ matrix.config.config_file }}
RESULT_FILENAME: gb200-${{ matrix.config.name }}
MATRIX_CONFIG_NAME: ${{ matrix.config.name }}
SQUASH_FILE: ${{ needs.prepare-image.outputs.squash_file }}
NGINX_SQUASH_FILE: ${{ needs.prepare-image.outputs.nginx_squash_file }}
# S3 log-upload credentials — consumed by srt-slurm's postprocess stage
# to upload /logs after each Slurm job; prefix derived in launch_gb200.sh.
AWS_ACCESS_KEY_ID: ${{ secrets.NV_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NV_S3_SECRET_ACCESS_KEY }}
S3_BUCKET: ${{ secrets.NV_S3_BUCKET }}
S3_ENDPOINT_URL: ${{ secrets.NV_S3_ENDPOINT_URL }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Clean up prior Slurm jobs from this runner
continue-on-error: true
env:
RUNNER_NAME: ${{ runner.name }}
run: |
STALE_JOBS=$(squeue --noheader --format="%i %j" | grep "${RUNNER_NAME}" | awk '{print $1}')
if [ -n "$STALE_JOBS" ]; then
echo "Cancelling stale jobs: $STALE_JOBS"
scancel $STALE_JOBS
fi
- name: Launch GB200 benchmark via srt-slurm
timeout-minutes: 360
env:
RUNNER_NAME: ${{ runner.name }}
run: bash scripts/ci/slurm/launch_gb200.sh
- name: Process results
if: always()
env:
RUNNER_NAME: ${{ runner.name }}
run: |
pip install tabulate pyyaml -q
SRT_REPO_DIR="/mnt/lustre01/users-public/sglang-ci/workspace/${RUNNER_NAME}/srt-slurm"
for result_file in ${{ github.workspace }}/${RESULT_FILENAME}_*.json; do
[ -f "$result_file" ] || continue
basename_file=$(basename "$result_file")
ctx=$(echo "$basename_file" | sed -n 's/.*_ctx_\([0-9]*\)_gen.*/\1/p')
gen=$(echo "$basename_file" | sed -n 's/.*_gen_\([0-9]*\)\.json/\1/p')
[ -n "$ctx" ] && [ -n "$gen" ] || continue
RESULT_FILENAME="${result_file%.json}" PREFILL_GPUS="$ctx" DECODE_GPUS="$gen" \
RECIPE_FILE="$SRT_REPO_DIR/$CONFIG_FILE" \
python3 scripts/ci/slurm/process_result.py
done
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: gb200-${{ matrix.config.name }}-${{ github.run_id }}
path: |
${{ github.workspace }}/*.json
${{ github.workspace }}/multinode_server_logs.tar.gz
retention-days: 30
if-no-files-found: warn
- name: Analyze logs with AI on failure
if: failure()
continue-on-error: true
env:
MODAL_TOKEN_ID: ${{ secrets.NV_MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.NV_MODAL_TOKEN_SECRET }}
run: |
TARBALL="${{ github.workspace }}/multinode_server_logs.tar.gz"
if [ -f "$TARBALL" ]; then
uv run --with modal python scripts/ci/slurm/analyze_logs_with_modal.py \
--tarball "$TARBALL" \
--job-id "${{ matrix.config.name }}-${{ github.run_id }}" \
--output "${{ github.workspace }}/ai_analysis.md"
if [ -f "${{ github.workspace }}/ai_analysis.md" ]; then
echo "## AI Log Analysis" >> $GITHUB_STEP_SUMMARY
cat "${{ github.workspace }}/ai_analysis.md" >> $GITHUB_STEP_SUMMARY
fi
else
echo "No log tarball found, skipping analysis"
fi
- name: Upload AI analysis to S3
if: failure()
continue-on-error: true
env:
ISL: ${{ matrix.config.isl }}
OSL: ${{ matrix.config.osl }}
run: |
ANALYSIS="${{ github.workspace }}/ai_analysis.md"
[ -f "$ANALYSIS" ] || { echo "no ai_analysis.md, skipping"; exit 0; }
case "${{ github.event_name }}" in
schedule) TRIGGER=cron ;;
workflow_dispatch) TRIGGER=manual ;;
*) TRIGGER="${{ github.event_name }}" ;;
esac
fmt() { if [ $(( $1 % 1024 )) -eq 0 ]; then echo "$(( $1 / 1024 ))k"; else echo "$1"; fi; }
SEQ_LEN="$(fmt "$ISL")$(fmt "$OSL")"
KEY="${TRIGGER}/${{ github.run_id }}-${{ github.run_attempt }}/${SEQ_LEN}/${{ matrix.config.name }}/ai_analysis.md"
aws --endpoint-url "$S3_ENDPOINT_URL" s3 cp "$ANALYSIS" "s3://${S3_BUCKET}/${KEY}"
echo "uploaded to s3://${S3_BUCKET}/${KEY}"
- name: Clean up Slurm jobs on failure/cancel
if: failure() || cancelled()
continue-on-error: true
env:
RUNNER_NAME: ${{ runner.name }}
run: |
ACTIVE_JOBS=$(squeue --noheader --format="%i %j" | grep "${RUNNER_NAME}" | awk '{print $1}')
if [ -n "$ACTIVE_JOBS" ]; then
echo "Cancelling jobs: $ACTIVE_JOBS"
scancel $ACTIVE_JOBS
fi
collect-results:
needs: nightly-gb200-benchmark
if: github.repository == 'sgl-project/sglang' && always()
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: results/
pattern: gb200-*
- name: Print summary
run: |
pip install tabulate -q
python3 scripts/ci/slurm/summarize.py results/ >> $GITHUB_STEP_SUMMARY
# ---------------------------------------------------------------------------
# Prune old tags in the staging repo, keeping only the most recent N. Mirrors
# the pattern used by release-docker-dev.yml. Runs after benchmarks so the
# freshly built image (whose sqsh is already on Lustre) becomes a regular
# aged-out tag over time. No-op when the repo has ≤ CI_IMAGE_KEEP_TAGS tags.
# ---------------------------------------------------------------------------
cleanup-image:
needs: [build-image, nightly-gb200-benchmark]
if: always() && needs.build-image.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Prune old staging tags on Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
TOKEN=$(curl -s -H "Content-Type: application/json" \
-X POST -d "{\"username\": \"${DOCKERHUB_USERNAME}\", \"password\": \"${DOCKERHUB_TOKEN}\"}" \
https://hub.docker.com/v2/users/login/ | jq -r .token)
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "::error::Docker Hub login failed"
exit 1
fi
TAGS_RESPONSE=$(curl -s -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/${CI_IMAGE_REPO}/tags/?page_size=100")
# Sort tags by last_updated (newest first), keep names only.
TAGS=$(echo "$TAGS_RESPONSE" | jq -r \
'.results[] | "\(.last_updated)|\(.name)"' \
| sort -r | cut -d'|' -f2)
TAG_COUNT=$(echo "$TAGS" | grep -c . || true)
if [ "$TAG_COUNT" -gt "$CI_IMAGE_KEEP_TAGS" ]; then
echo "Found $TAG_COUNT tags in ${CI_IMAGE_REPO}, keeping $CI_IMAGE_KEEP_TAGS most recent"
TAGS_TO_DELETE=$(echo "$TAGS" | tail -n +$((CI_IMAGE_KEEP_TAGS + 1)))
for tag in $TAGS_TO_DELETE; do
echo "Deleting ${CI_IMAGE_REPO}:${tag}"
curl -s -X DELETE -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/${CI_IMAGE_REPO}/tags/${tag}/"
done
else
echo "Only $TAG_COUNT tags in ${CI_IMAGE_REPO}, no cleanup needed"
fi
@@ -0,0 +1,250 @@
name: Nightly Test (AMD MI355X 2N 1P1D Disagg)
# A self-hosted runner on the amd-sglang login node salloc's two MI355X nodes
# and runs the Docker harness (prefill + decode over MORI, a standalone load
# balancer, then an sglang.bench_serving concurrency sweep) via
# scripts/ci/slurm/launch_mi355x.sh.
on:
schedule:
- cron: '17 3 * * *' # 03:17 UTC
workflow_dispatch:
inputs:
image:
description: 'Optional. ROCm SGLang Docker image to benchmark. Leave empty for the recipe default.'
required: false
default: ''
configs:
description: 'Optional. Comma-separated names to run only a subset. Leave empty to run all. Available names are listed in the setup job log.'
required: false
default: ''
concurrency:
group: nightly-test-mi355x
cancel-in-progress: false
env:
SGLANG_IS_IN_CI: true
jobs:
# ---------------------------------------------------------------------------
# Reads scripts/ci/slurm/nightly-configs.yaml and generates one matrix entry
# per recipe YAML for runner=mi355x. To add/remove configs, edit
# nightly-configs.yaml only.
# ---------------------------------------------------------------------------
setup:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
image: ${{ steps.image.outputs.image }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Resolve latest MI35x nightly image
id: image
# Track the newest published image so the nightly actually catches
# regressions instead of re-testing one pinned build. We resolve on the
# GitHub-hosted runner (clean Docker Hub access, no rate limits) and pass
# the tag to the self-hosted runner. Scope is deliberately tight: only
# the clean main MI35x ROCm 7.2 line
# `lmsysorg/sglang-rocm:v*-rocm720-mi35x-YYYYMMDD` (excludes -test-,
# miles-, mori-, vllm-, rocm7_14 variants). On any failure we emit an
# empty string and the launcher falls back to the recipe's pinned
# `runtime.image`.
run: |
set -o pipefail
RESOLVED=$(curl -fsSL "https://hub.docker.com/v2/repositories/lmsysorg/sglang-rocm/tags?page_size=100&ordering=last_updated" \
| python3 -c "import json,re,sys;p=re.compile(r'^v[0-9][0-9A-Za-z.]*-rocm720-mi35x-([0-9]{8})\$');c=sorted((m.group(1),t['name']) for t in json.load(sys.stdin).get('results',[]) for m in [p.match(t.get('name',''))] if m);print(c[-1][1] if c else '')" || true)
if [ -n "$RESOLVED" ]; then
echo "Resolved latest MI35x image: lmsysorg/sglang-rocm:$RESOLVED"
echo "image=lmsysorg/sglang-rocm:$RESOLVED" >> $GITHUB_OUTPUT
else
echo "Could not resolve a latest image; launcher will use recipe default."
echo "image=" >> $GITHUB_OUTPUT
fi
- name: Generate benchmark matrix
id: generate
env:
CONFIGS_FILTER: ${{ inputs.configs }}
run: |
pip install pyyaml -q
ALL_MATRIX=$(python3 scripts/ci/slurm/generate_matrix.py \
scripts/ci/slurm/nightly-configs.yaml --runner mi355x)
echo "Available config names for runner mi355x:"
echo "$ALL_MATRIX" | python3 -c "import json,sys; [print(f' - {e[\"name\"]}') for e in json.load(sys.stdin)]"
FILTER_ARG=()
if [ -n "$CONFIGS_FILTER" ]; then
echo ""
echo "Filtering to: $CONFIGS_FILTER"
FILTER_ARG=(--filter "$CONFIGS_FILTER")
fi
MATRIX=$(python3 scripts/ci/slurm/generate_matrix.py \
scripts/ci/slurm/nightly-configs.yaml --runner mi355x "${FILTER_ARG[@]}")
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
nightly-mi355x-benchmark:
needs: setup
if: |
always() && github.repository == 'sgl-project/sglang'
&& needs.setup.result == 'success'
runs-on: linux-sglang-mi35x-di
strategy:
fail-fast: false
matrix:
config: ${{ fromJson(needs.setup.outputs.matrix) }}
env:
FRAMEWORK: sglang
HW: mi355x
MODEL: ${{ matrix.config.model }}
MODEL_PREFIX: ${{ matrix.config.model_prefix }}
PRECISION: ${{ matrix.config.precision }}
ISL: ${{ matrix.config.isl }}
OSL: ${{ matrix.config.osl }}
CONFIG_FILE: ${{ matrix.config.config_file }}
RESULT_FILENAME: mi355x-${{ matrix.config.name }}
MATRIX_CONFIG_NAME: ${{ matrix.config.name }}
# NOTE: RUNNER_NAME is a built-in default env var on every runner, so the
# launch + cleanup steps read it directly. Do NOT set it here from
# ${{ runner.name }} -- the `runner` context is not available in job-level
# env and makes the whole workflow file invalid.
# Shared-NFS HuggingFace cache dir; the launcher resolves the live snapshot
# via refs/main. Per-model value comes from nightly-configs.yaml model_path.
MODEL_PATH: ${{ matrix.config.model_path }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Clean up prior Slurm jobs from this runner
continue-on-error: true
run: |
# launch_mi355x.sh names the allocation
# mi355x-ci-<RUNNER_NAME>-<GITHUB_RUN_ID>-<config>
# Here we clear THIS runner's leftovers from a crashed previous run, so
# we match the RUNNER_NAME prefix (older runs have a different run id).
# Never a blanket `squeue --me`, which would kill a concurrent leg.
# %200j: squeue truncates the job name (%j) by default -- widen it so
# the grep sees the full name.
if [ -z "${RUNNER_NAME:-}" ]; then echo "RUNNER_NAME unset; skipping"; exit 0; fi
STALE_JOBS=$(squeue --me --noheader --format="%i %200j" | grep -F "mi355x-ci-${RUNNER_NAME}-" | awk '{print $1}' || true)
if [ -n "$STALE_JOBS" ]; then
echo "Cancelling stale jobs for ${RUNNER_NAME}: $STALE_JOBS"
scancel $STALE_JOBS
fi
- name: Launch MI355X 2N 1P1D benchmark
timeout-minutes: 180
env:
# Manual dispatch input wins; otherwise use the latest image resolved
# by the setup job; otherwise the launcher falls back to the recipe default.
IMAGE_OVERRIDE: ${{ inputs.image != '' && inputs.image || needs.setup.outputs.image }}
# Keep the scheduler off mia1-p01-g20: its ionic RDMA driver ABI
# mismatches the container, so MORI reports "no active RDMA device"
# and the disagg server dies on init. Remove once the node is fixed.
SLURM_EXCLUDE: mia1-p01-g20
run: bash scripts/ci/slurm/launch_mi355x.sh
- name: Pack logs
if: always()
run: |
LOG_DIR="$HOME/.mi355x_ci/${MATRIX_CONFIG_NAME}"
LOG_BUNDLE="${GITHUB_WORKSPACE}/${RESULT_FILENAME}_logs.tar.gz"
if [ ! -d "$LOG_DIR" ]; then
echo "WARN: log directory not found: $LOG_DIR"
exit 0
fi
mapfile -d '' LOG_FILES < <(
find "$LOG_DIR" -maxdepth 1 -type f \
\( -name '*.log' -o -name 'server_exit_*' -o -name 'bench_exit' \) \
-printf '%P\0' | sort -z
)
if [ "${#LOG_FILES[@]}" -eq 0 ]; then
echo "WARN: no log files found in $LOG_DIR"
exit 0
fi
STAGING_DIR="$(mktemp -d)"
trap 'rm -rf "$STAGING_DIR"' EXIT
for log_file in "${LOG_FILES[@]}"; do
cp -a "$LOG_DIR/$log_file" "$STAGING_DIR/$log_file" || true
done
tar czf "$LOG_BUNDLE" -C "$STAGING_DIR" .
echo "Packed ${#LOG_FILES[@]} log file(s) -> $LOG_BUNDLE"
- name: Process results
if: always()
run: |
pip install tabulate pyyaml -q
for result_file in ${{ github.workspace }}/${RESULT_FILENAME}_*.json; do
[ -f "$result_file" ] || continue
basename_file=$(basename "$result_file")
ctx=$(echo "$basename_file" | sed -n 's/.*_ctx_\([0-9]*\)_gen.*/\1/p')
gen=$(echo "$basename_file" | sed -n 's/.*_gen_\([0-9]*\)\.json/\1/p')
[ -n "$ctx" ] && [ -n "$gen" ] || continue
RESULT_FILENAME="${result_file%.json}" PREFILL_GPUS="$ctx" DECODE_GPUS="$gen" \
RECIPE_FILE="${{ github.workspace }}/$CONFIG_FILE" \
python3 scripts/ci/slurm/process_result.py
done
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: mi355x-${{ matrix.config.name }}-${{ github.run_id }}
path: |
${{ github.workspace }}/*.json
${{ github.workspace }}/*_logs.tar.gz
retention-days: 30
if-no-files-found: warn
- name: Clean up Slurm jobs on failure/cancel
if: failure() || cancelled()
continue-on-error: true
run: |
# Cancel only THIS leg's allocation -- match the full job name
# mi355x-ci-<RUNNER_NAME>-<GITHUB_RUN_ID>-<config>. GITHUB_RUN_ID +
# config are unique per matrix leg, so a concurrent leg is never hit.
if [ -z "${RUNNER_NAME:-}" ]; then echo "RUNNER_NAME unset; skipping"; exit 0; fi
JOB_TAG="mi355x-ci-${RUNNER_NAME}-${GITHUB_RUN_ID}-${MATRIX_CONFIG_NAME}"
ACTIVE_JOBS=$(squeue --me --noheader --format="%i %200j" | grep -F "$JOB_TAG" | awk '{print $1}' || true)
if [ -n "$ACTIVE_JOBS" ]; then
echo "Cancelling jobs for ${JOB_TAG}: $ACTIVE_JOBS"
scancel $ACTIVE_JOBS
fi
- name: Clean up MI355X scratch
if: always()
continue-on-error: true
run: |
LOG_DIR="$HOME/.mi355x_ci/${MATRIX_CONFIG_NAME}"
if [ -d "$LOG_DIR" ]; then
echo "Removing MI355X scratch directory: $LOG_DIR"
rm -rf "$LOG_DIR"
fi
collect-results:
needs: nightly-mi355x-benchmark
if: github.repository == 'sgl-project/sglang' && always()
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: results/
pattern: mi355x-*
- name: Print summary
run: |
pip install tabulate -q
python3 scripts/ci/slurm/summarize.py results/ >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,95 @@
name: Nightly Experimental sgl-router Docker Image
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
env:
IMAGE_REPO: lmsysorg/sglang-staging
IMAGE_TAG: experimental-router-dev
jobs:
# Build each arch natively (no QEMU emulation) and push the image by digest.
# The manifest job then stitches the per-arch digests into one multi-arch
# tag, so `docker pull` resolves the right arch automatically.
build:
if: github.repository == 'sgl-project/sglang'
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
arch: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest
run: |
docker buildx build . -f docker/sgl-router.Dockerfile \
--platform ${{ matrix.platform }} \
--output "type=image,name=${IMAGE_REPO},push-by-digest=true,name-canonical=true,push=true" \
--metadata-file /tmp/metadata.json
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata.json'))['containerimage.digest'])")
echo "Pushed ${{ matrix.platform }} digest: ${DIGEST}"
mkdir -p /tmp/digests
echo "${DIGEST}" > "/tmp/digests/${{ matrix.arch }}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.arch }}
path: /tmp/digests/${{ matrix.arch }}
retention-days: 1
create-manifest:
if: github.repository == 'sgl-project/sglang'
needs: build
runs-on: ubuntu-24.04
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push multi-arch manifest
run: |
AMD64_DIGEST=$(cat /tmp/digests/amd64)
ARM64_DIGEST=$(cat /tmp/digests/arm64)
docker buildx imagetools create \
-t "${IMAGE_REPO}:${IMAGE_TAG}" \
"${IMAGE_REPO}@${AMD64_DIGEST}" \
"${IMAGE_REPO}@${ARM64_DIGEST}"
- name: Inspect manifest
run: docker buildx imagetools inspect "${IMAGE_REPO}:${IMAGE_TAG}"
+32
View File
@@ -0,0 +1,32 @@
name: Nightly Link Check
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
concurrency:
group: nightly-link-check-${{ github.ref }}
cancel-in-progress: true
jobs:
lychee-online:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run lychee online link checks
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
with:
fail: true
args: >-
--config .github/linters/lychee-ci.toml
README.md
docs/**/*.md
docs/**/*.rst
docs/**/*.ipynb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,196 @@
# Nightly release workflow for SGLang Model Gateway
name: Nightly Release SGLang Model Gateway to PyPI
on:
schedule:
# Run at 2 AM UTC every day
- cron: '0 2 * * *'
workflow_dispatch: # Allow manual trigger
jobs:
build:
name: build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }})
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
target: [x86_64, aarch64]
manylinux: [auto]
include:
- os: ubuntu
platform: linux
- os: windows
ls: dir
target: x86_64
python-architecture: x64
interpreter: 3.9 3.10 3.11 3.12 3.13
- os: macos
target: aarch64
interpreter: 3.9 3.10 3.11 3.12 3.13
- os: ubuntu
platform: linux
target: aarch64
# musllinux
- os: ubuntu
platform: linux
target: x86_64
manylinux: musllinux_1_1
- os: ubuntu
platform: linux
target: aarch64
manylinux: musllinux_1_1
exclude:
- os: windows
target: aarch64
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root and delete sglang-repo
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
ls -alt
shell: bash
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
architecture: ${{ matrix.python-architecture || 'x64' }}
- name: Modify version for nightly release
run: |
# Get current version from pyproject.toml
CURRENT_VERSION=$(python -c "import tomllib; print(tomllib.load(open('bindings/python/pyproject.toml', 'rb'))['project']['version'])" 2>/dev/null || python -c "import tomli; print(tomli.load(open('bindings/python/pyproject.toml', 'rb'))['project']['version'])")
# Create nightly version with date: e.g., 0.2.1.dev20250128
NIGHTLY_VERSION="${CURRENT_VERSION}.dev$(date +%Y%m%d)"
echo "Nightly version: $NIGHTLY_VERSION"
# Update pyproject.toml with nightly version (temporary, not committed)
sed -i.bak "s/version = \"${CURRENT_VERSION}\"/version = \"${NIGHTLY_VERSION}\"/" bindings/python/pyproject.toml
# Verify the change
cat bindings/python/pyproject.toml | grep "^version"
shell: bash
- name: Install twine and tomli
run: pip install -U twine tomli
- name: Install protoc (macOS)
if: matrix.os == 'macos'
run: brew install protobuf
- name: Install protoc (Windows)
if: matrix.os == 'windows'
run: choco install protoc -y
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
working-directory: bindings/python
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux || 'auto' }}
args: --release --out dist --features vendored-openssl --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 3.14' }}
rust-toolchain: stable
docker-options: -e CI -e CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc -e CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
before-script-linux: |
# Install build dependencies (perl/make for vendored OpenSSL, protoc for gRPC)
if command -v yum &> /dev/null; then
yum update -y && yum install -y wget unzip gcc gcc-c++ perl-core make
# Install cross-compilation toolchain for aarch64 if needed
if [ "${{ matrix.target }}" = "aarch64" ]; then
yum install -y gcc-aarch64-linux-gnu gcc-c++-aarch64-linux-gnu || true
fi
elif command -v apt-get &> /dev/null; then
apt-get update && apt-get install -y wget unzip gcc g++ perl make
# Install cross-compilation toolchain for aarch64 if needed
if [ "${{ matrix.target }}" = "aarch64" ]; then
apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu || true
fi
fi
(cd /tmp && \
wget https://github.com/protocolbuffers/protobuf/releases/download/v32.0/protoc-32.0-linux-x86_64.zip && \
unzip protoc-32.0-linux-x86_64.zip -d /usr/local && \
rm protoc-32.0-linux-x86_64.zip)
protoc --version
- name: List built packages
run: ${{ matrix.ls || 'ls -lh' }} bindings/python/dist/
- name: Check packages
run: twine check --strict bindings/python/dist/*
- uses: actions/upload-artifact@v4
with:
name: packages-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'auto' }}
path: bindings/python/dist/
build-sdist:
name: Build SDist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root and delete sglang-repo
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
ls -alt
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Modify version for nightly release
run: |
# Get current version from pyproject.toml
CURRENT_VERSION=$(python -c "import tomllib; print(tomllib.load(open('bindings/python/pyproject.toml', 'rb'))['project']['version'])" 2>/dev/null || python -c "import tomli; print(tomli.load(open('bindings/python/pyproject.toml', 'rb'))['project']['version'])")
# Create nightly version with date: e.g., 0.2.1.dev20250128
NIGHTLY_VERSION="${CURRENT_VERSION}.dev$(date +%Y%m%d)"
echo "Nightly version: $NIGHTLY_VERSION"
# Update pyproject.toml with nightly version (temporary, not committed)
sed -i "s/version = \"${CURRENT_VERSION}\"/version = \"${NIGHTLY_VERSION}\"/" bindings/python/pyproject.toml
# Verify the change
cat bindings/python/pyproject.toml | grep "^version"
- name: Build SDist
uses: PyO3/maturin-action@v1
with:
working-directory: bindings/python
command: sdist
args: --out dist
rust-toolchain: stable
- uses: actions/upload-artifact@v4
with:
name: sdist
path: bindings/python/dist/*.tar.gz
upload:
name: Upload to TestPyPI
if: github.repository == 'sgl-project/sglang' # Ensure this job only runs for the sgl-project/sglang repository
needs: [build, build-sdist]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Upload to TestPyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN_ROUTER }}
run: |
pip install twine
twine upload --repository testpypi dist/* --verbose
@@ -0,0 +1,93 @@
name: Nightly Test Miles (AMD ROCm 7.0)
on:
schedule:
# Runs after the 12:00 UTC miles image build has pushed the dated image.
- cron: "30 17 * * *"
workflow_dispatch:
inputs:
image_tag:
description: "Override miles image tag (e.g. miles-rocm700-mi35x-20260621); empty = newest by date"
required: false
type: string
default: ""
continue_on_error:
description: "Continue on error (do not fail the workflow on test failures)"
required: false
type: boolean
default: true
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.ref }}
cancel-in-progress: false
jobs:
test-miles-mi35x:
if: github.repository == 'sgl-project/sglang'
strategy:
fail-fast: false
matrix:
include:
- suite: stage-c-2-gpu-mi350
runner: linux-mi35x-gpu-2
# Use 8-GPU MI35X pool for 4-GPU suite.
- suite: stage-c-4-gpu-mi350
runner: linux-mi35x-gpu-8
- suite: stage-c-8-gpu-mi350
runner: linux-mi35x-gpu-8
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Resolve newest miles ROCm 7.0 image
run: |
repo="rocm/sgl-dev"
base="miles-rocm700-mi35x"
tag="${{ inputs.image_tag }}"
if [ -z "${tag}" ]; then
# Walk back from today; the build job pushes a dated tag at 12:00 UTC.
# Probe with `manifest inspect` so we don't pull a multi-GB image just
# to test existence; the real pull happens in amd_ci_start_container.sh.
for i in $(seq 0 6); do
d=$(date -u -d "-${i} day" +%Y%m%d)
candidate="${base}-${d}"
if docker manifest inspect "${repo}:${candidate}" >/dev/null 2>&1; then
tag="${candidate}"
break
fi
done
fi
if [ -z "${tag}" ]; then
echo "::error::No ${repo}:${base}-<date> image found on Docker Hub for the last 7 days"
exit 1
fi
echo "MILES_IMAGE=${repo}:${tag}" >> "$GITHUB_ENV"
echo "Using miles image: ${repo}:${tag}"
- name: Ensure VRAM is clear
run: bash scripts/ci/amd/ensure_vram_clear.sh rocm
- name: Start container from the miles image
run: bash scripts/ci/amd/amd_ci_start_container.sh --custom-image "${MILES_IMAGE}"
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Run miles CI suite ${{ matrix.suite }}
timeout-minutes: 120
run: |
touch "${GITHUB_WORKSPACE}/github_summary.md"
TEST_EXIT_CODE=0
bash scripts/ci/amd/amd_ci_exec.sh -w /root/miles \
-e PYTHONPATH=/root/miles:/opt/tilelang \
-e GITHUB_STEP_SUMMARY=/sglang-checkout/github_summary.md \
python3 tests/ci/run_suite.py --hw rocm --suite ${{ matrix.suite }} --match-all-labels \
${{ (github.event_name != 'workflow_dispatch' || inputs.continue_on_error) && '--continue-on-error' || '' }} || TEST_EXIT_CODE=$?
cat "${GITHUB_WORKSPACE}/github_summary.md" >> "$GITHUB_STEP_SUMMARY" || true
exit ${TEST_EXIT_CODE}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+181
View File
@@ -0,0 +1,181 @@
name: Nightly Test (Intel)
on:
schedule:
# 22:15 IST (UTC+5:30) → 16:45 UTC daily.
- cron: '45 16 * * *'
push:
branches:
- main
paths:
- "python/sglang/version.py"
workflow_dispatch:
inputs:
continue_on_error:
description: 'Continue on error (do not fail the workflow on test failures)'
required: false
type: boolean
default: true
workflow_call:
inputs:
ref:
description: "Branch, tag or SHA to checkout"
required: false
type: string
default: ""
continue_on_error:
description: 'Continue on error (do not fail the workflow on test failures)'
required: false
type: boolean
default: true
concurrency:
group: nightly-test-intel-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || inputs.ref && format('caller-{0}', github.run_id) || github.ref }}
cancel-in-progress: ${{ !inputs.ref && github.event_name != 'workflow_call' && github.event_name != 'workflow_dispatch' }}
jobs:
nightly-xpu-1-gpu:
# Placeholder: no models currently registered to nightly-xpu-1-gpu run on
# intel-bmg-nightly. Add a model test to this suite to re-enable.
if: false
runs-on: intel-bmg-nightly
steps:
- name: Placeholder
run: echo "nightly-xpu-1-gpu has no validated models; skipping."
nightly-xpu-2-gpu:
if: github.repository == 'sgl-project/sglang'
runs-on: intel-bmg-nightly
env:
DOCKERHUB_INTEL_USERNAME: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
DOCKERHUB_INTEL_TOKEN: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
steps:
- name: Reset workspace ownership
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.sha }}
- name: Start CI container (pull intel/sglang-dev:latest)
run: |
export HF_TOKEN="$(cat ~/huggingface_token.txt)"
bash scripts/ci/xpu/xpu_ci_start_container.sh
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: HF login + install run_suite extras
timeout-minutes: 10
run: |
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir tabulate
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir --no-deps xgrammar==0.1.33
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir "lm-eval==0.4.9"
docker exec ci_sglang_xpu /bin/bash -c '/opt/venv/bin/hf auth login --token ${HF_TOKEN}'
- name: Nightly Test (2-GPU XPU)
timeout-minutes: 60
run: |
touch github_summary.md
docker exec ci_sglang_xpu bash -c "
source /opt/venv/bin/activate &&
cd /sglang-checkout/test &&
GITHUB_STEP_SUMMARY=/sglang-checkout/github_summary.md \
python3 run_suite.py --hw xpu --suite nightly-xpu-2-gpu --nightly --timeout-per-file 7200 ${{ (github.event_name == 'schedule' || inputs.continue_on_error) && '--continue-on-error' || '' }}
" || TEST_EXIT_CODE=$?
echo "$(<github_summary.md)" >> $GITHUB_STEP_SUMMARY || true
exit ${TEST_EXIT_CODE:-0}
- name: Cleanup container
if: always()
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
rm -rf test/result.jsonl test/results test/.pytest_cache .pytest_cache || true
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
find . -type f -name "*.pyc" -delete || true
docker rm -f ci_sglang_xpu || true
if [[ -n "${CI_SGLANG_XPU_IMAGE:-}" ]]; then
docker rmi -f "${CI_SGLANG_XPU_IMAGE}" || true
fi
nightly-xpu-4-gpu:
if: github.repository == 'sgl-project/sglang'
runs-on: intel-bmg-nightly
env:
DOCKERHUB_INTEL_USERNAME: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
DOCKERHUB_INTEL_TOKEN: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
steps:
- name: Reset workspace ownership
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.sha }}
- name: Start CI container (pull intel/sglang-dev:latest)
run: |
export HF_TOKEN="$(cat ~/huggingface_token.txt)"
bash scripts/ci/xpu/xpu_ci_start_container.sh
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: HF login + install run_suite extras
timeout-minutes: 10
run: |
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir tabulate
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir --no-deps xgrammar==0.1.33
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir "lm-eval==0.4.9"
docker exec ci_sglang_xpu /bin/bash -c '/opt/venv/bin/hf auth login --token ${HF_TOKEN}'
- name: Nightly Test (4-GPU XPU)
timeout-minutes: 120
run: |
touch github_summary.md
docker exec ci_sglang_xpu bash -c "
source /opt/venv/bin/activate &&
cd /sglang-checkout/test &&
GITHUB_STEP_SUMMARY=/sglang-checkout/github_summary.md \
python3 run_suite.py --hw xpu --suite nightly-xpu-4-gpu --nightly --timeout-per-file 7200 ${{ (github.event_name == 'schedule' || inputs.continue_on_error) && '--continue-on-error' || '' }}
" || TEST_EXIT_CODE=$?
echo "$(<github_summary.md)" >> $GITHUB_STEP_SUMMARY || true
exit ${TEST_EXIT_CODE:-0}
- name: Cleanup container
if: always()
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
rm -rf test/result.jsonl test/results test/.pytest_cache .pytest_cache || true
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
find . -type f -name "*.pyc" -delete || true
docker rm -f ci_sglang_xpu || true
if [[ -n "${CI_SGLANG_XPU_IMAGE:-}" ]]; then
docker rmi -f "${CI_SGLANG_XPU_IMAGE}" || true
fi
check-all-jobs:
if: always() && (github.repository == 'sgl-project/sglang' || github.event_name == 'workflow_dispatch')
needs:
- nightly-xpu-2-gpu
- nightly-xpu-4-gpu
runs-on: ubuntu-latest
steps:
- name: Check if any job failed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more nightly test jobs failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more nightly test jobs were cancelled"
exit 1
fi
echo "All nightly test jobs passed"
+301
View File
@@ -0,0 +1,301 @@
name: Nightly Test (MUSA)
on:
schedule:
- cron: '0 16 * * *'
workflow_dispatch:
inputs:
job_filter:
description: "Select which job to run (empty/all to run all jobs)"
required: false
type: choice
default: 'all'
options:
- 'all'
- 'nightly-test-musa-general-kernel'
- 'nightly-test-musa-general-multimodal-layer'
- 'nightly-test-llm-server-1-gpu-musa'
- 'nightly-test-multimodal-server-1-gpu-musa'
- 'nightly-test-multimodal-server-2-gpu-musa'
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
concurrency:
group: nightly-test-musa-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
env:
SGLANG_IS_IN_CI: true
jobs:
# ==================== General: kernel ====================
nightly-test-musa-general-kernel:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
TORCHADA_ENABLE_CPP_OPS: 1
HF_HUB_CACHE: /hf-cache/hub
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-musa-general-kernel" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
timeout-minutes: 10
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run sgl-kernel unit tests (MUSA)
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 30
run: |
pytest sgl-kernel/tests/test_dsv3_router_gemm.py
pytest sgl-kernel/tests/test_per_token_quant_fp8.py
pytest sgl-kernel/tests/speculative/test_eagle_utils.py
pytest sgl-kernel/tests/speculative/test_ngram_utils.py
pytest sgl-kernel/tests/speculative/test_speculative_sampling.py
pytest sgl-kernel/tests/test_torch_defaults_reset.py
# ==================== General: multimodal layer ====================
nightly-test-musa-general-multimodal-layer:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-musa-general-multimodal-layer" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
timeout-minutes: 10
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
if: steps.gate.outputs.run_job == 'true'
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run multimodal MUSA layer unit tests
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 30
run: |
pytest python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_rmsnorm.py
pytest python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_silu_and_mul.py
# ==================== LLM server: 1-GPU ====================
nightly-test-llm-server-1-gpu-musa:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-llm-server-1-gpu-musa" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
timeout-minutes: 10
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
if: steps.gate.outputs.run_job == 'true'
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run DeepSeek-V2-Lite-Chat LLM server smoke test
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 120
run: |
cd test
python3 run_suite.py \
--hw musa \
--suite nightly-musa-1-gpu \
--nightly \
--timeout-per-file 7200
# ==================== Multimodal: 1-GPU (split) ====================
nightly-test-multimodal-server-1-gpu-musa:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-1-gpu-runner
strategy:
fail-fast: false
max-parallel: 2
matrix:
part: [0, 1]
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-multimodal-server-1-gpu-musa" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
timeout-minutes: 10
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
if: steps.gate.outputs.run_job == 'true'
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run diffusion server tests (1-GPU)
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 60
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd python
python3 sglang/multimodal_gen/test/server/musa/run_suite.py \
--suite 1-gpu-musa-nightly \
--partition-id ${{ matrix.part }} \
--total-partitions 2 \
--continue-on-error
# ==================== Multimodal: 2-GPU ====================
nightly-test-multimodal-server-2-gpu-musa:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-2-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-multimodal-server-2-gpu-musa" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
timeout-minutes: 10
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
if: steps.gate.outputs.run_job == 'true'
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run diffusion server tests (2-GPU)
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 60
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd python
python3 sglang/multimodal_gen/test/server/musa/run_suite.py \
--suite 2-gpu-musa \
--continue-on-error
# ==================== finish ====================
nightly-test-musa-finish:
needs:
[
nightly-test-musa-general-kernel,
nightly-test-musa-general-multimodal-layer,
nightly-test-llm-server-1-gpu-musa,
nightly-test-multimodal-server-1-gpu-musa,
nightly-test-multimodal-server-2-gpu-musa,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
json_needs='${{ toJson(needs) }}'
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
echo "$job: $result"
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "Nightly failed."
exit 1
fi
done
echo "All jobs completed successfully."
@@ -0,0 +1,232 @@
name: 'Nightly Test (NPU) Multi Node Template'
on:
workflow_call:
inputs:
runner:
required: false
type: string
default: linux-amd64-cpu-8
test_type:
required: false
type: string
default: perf
description: perf or accuracy
test_config_name:
required: true
type: string
description: test config name
prefill_size:
required: false
type: number
default: 0
description: number of prefill nodes for pd-separation
decode_size:
required: false
type: number
default: 0
description: number of decode nodes for pd-separation
router_size:
required: false
type: number
default: 0
description: number of router nodes for pd-separation
node_size:
required: false
type: number
default: 0
description: number of nodes for pd-mix
test_case:
required: true
type: string
description: path of test case file
image:
required: false
type: string
description: image for pods
default: "swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann8.5.0-a3"
install_sglang_from_source:
required: false
type: boolean
default: true
description: use sglang from source code or from docker image
prefill_decode_deployment:
required: true
type: string
description: "The deployment of prefill and decode nodes. ['separation', 'mix']"
transformers_version:
required: false
type: string
default: ""
description: "The transformers version number for running sglang. Use default version in image if keep empty."
concurrency:
group: ascend-nightly-multi-node-${{ github.workflow_ref }}-${{ github.ref }}-${{ inputs.test_config_name }}
cancel-in-progress: true
jobs:
e2e:
name: ${{ inputs.test_config_name }}
runs-on: ${{ inputs.runner }}
container:
image: swr.ap-southeast-1.myhuaweicloud.com/base_image/ascend-ci/sglang:main-x86
env:
KUBECONFIG: /root/.cache/.cache/kb.yaml
KUBECTL: /root/.cache/.cache/kubectl
NAMESPACE: sgl-project
KUBE_JOB_NAME: ascend-sglang-${{ inputs.test_type }}-test
SGLANG_IS_IN_CI: true
ASCEND_E2E_TEST_CONFIG_PATH: python/sglang/test/ascend/e2e
SGLANG_USE_MODELSCOPE: true
HF_ENDPOINT: https://hf-mirror.com
TRANSFORMERS_VERBOSITY: "error"
GDN_ATTN_BACKEND_TRITON: 1
steps:
- name: Install dependencies
run: |
pip3 install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple jinja2
pip3 install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple psutil
# Install kubernetes
pip3 install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple kubernetes
cp $KUBECTL /usr/local/sbin/
- name: Checkout code
uses: actions/checkout@v4
- name: Prepare code for testing
run: |
# copy source code to shared-disk
current_path=$(pwd)
target_path=/root/.cache/tests/sglang
rm -rf ${target_path}
mkdir -p ${target_path}
cp -r ${current_path}/* ${target_path}/
- name: Clear resources
run: |
cd ${ASCEND_E2E_TEST_CONFIG_PATH}
kubectl delete -f ./k8s_multi_pd_*.yaml --ignore-not-found=true || true
pod_name_prefix="${KUBE_JOB_NAME}-"
echo "kube name space: $NAMESPACE, pod name prefix: ${pod_name_prefix}"
while true; do
if kubectl get po -A -n $NAMESPACE | grep -q "${pod_name_prefix}"; then
echo "Found exist sglang job, sleeping for 30 seconds..."
sleep 30
kubectl get pods | grep "${pod_name_prefix}" | awk '{print $1}' | xargs kubectl delete pod -n $NAMESPACE || true
else
echo "No sglang job exist, start test case..."
break
fi
done
- name: Run test
timeout-minutes: 300
run: |
# sglang_source_relative_path is shared-disk path
sglang_source_relative_path=tests/sglang
sglang_source_path=/root/.cache/${sglang_source_relative_path}
echo "Source code path: ${sglang_source_path}"
test_case="${{ inputs.test_case }}"
if [ ! -f "${sglang_source_path}/${test_case}" ]; then
echo "The testcase does not exit: ${sglang_source_path}/${test_case}"
exit 1
fi
# prepare for output data
current_date=$(date +%Y%m%d)
test_data_output_path=/root/.cache/tests/output/${{ inputs.test_type }}/${current_date}
mkdir -p ${test_data_output_path}
tc_name=${test_case##*/}
tc_name=${tc_name%.*}
metrics_data_path=${test_data_output_path}/${tc_name}
mkdir -p ${metrics_data_path}
echo "Metrics file path: ${metrics_data_path}"
prefill_decode_deployment="${{ inputs.prefill_decode_deployment }}"
kube_job_type=""
if [ "$prefill_decode_deployment" = "separation" ];then
kube_job_type=multi-pd-separation
elif [ "$prefill_decode_deployment" = "mix" ];then
kube_job_type=multi-pd-mix
else
echo "Unsupported deployment type: ${prefill_decode_deployment}"
exit 1
fi
transformers_version="${{ inputs.transformers_version }}"
cd ${ASCEND_E2E_TEST_CONFIG_PATH}
CMD="python3 -u run_npu_e2e_test.py \
--env ci \
--image ${{ inputs.image }} \
--sglang-source-relative-path ${sglang_source_relative_path} \
--metrics-data-file ${metrics_data_path} \
--test-case ${test_case} \
--kube-name-space ${NAMESPACE} \
--kube-job-type ${kube_job_type} \
--kube-job-name-prefix ${KUBE_JOB_NAME}"
if [ "$prefill_decode_deployment" = "separation" ];then
CMD="${CMD} \
--prefill-size ${{ inputs.prefill_size }} \
--decode-size ${{ inputs.decode_size }} \
--router-size ${{ inputs.router_size }}"
elif [ "$prefill_decode_deployment" = "mix" ];then
CMD="${CMD} --node-size ${{ inputs.node_size }}"
else
echo "Unsupported deployment type: ${prefill_decode_deployment}"
exit 1
fi
if [ "$SGLANG_IS_IN_CI" = "true" ] || [ "$SGLANG_IS_IN_CI" = "True" ];then
echo "Run test in ci."
CMD="${CMD} --sglang-is-in-ci"
fi
install_sglang_from_source="${{ inputs.install_sglang_from_source }}"
if [ "$install_sglang_from_source" = "true" ] || [ "$install_sglang_from_source" = "True" ];then
echo "Install sglang from source."
CMD="${CMD} --install-sglang-from-source"
commit_id=${{ github.sha }}
echo "commit id: ${commit_id}" > ${test_data_output_path}/commit_id
else
echo "Use sglang from image: ${{ inputs.image }}"
fi
if [ "$transformers_version" != "" ];then
CMD="${CMD} --transformers-version ${transformers_version}"
else
echo "Use default transformers version in image."
fi
echo "Run command: ${CMD}"
eval "${CMD}"
- name: Upload metrics
if: always()
uses: actions/upload-artifact@v4
with:
name: metrics-${{ inputs.test_config_name }}
path: /tmp/metrics.json
retention-days: 7
- name: Post process
if: always()
run: |
cd ${ASCEND_E2E_TEST_CONFIG_PATH}
kubectl get pods -n $NAMESPACE | grep $KUBE_JOB_NAME
kubectl delete -f ./k8s_multi_pd_*.yaml --ignore-not-found=true || true
pod_name_prefix="${KUBE_JOB_NAME}-"
echo "kube name space: $NAMESPACE, pod name prefix: ${pod_name_prefix}"
while true; do
if kubectl get po -A -n $NAMESPACE | grep -q "${pod_name_prefix}"; then
echo "Found exist sglang job, sleeping for 30 seconds..."
sleep 30
kubectl get pods | grep "${pod_name_prefix}" | awk '{print $1}' | xargs kubectl delete pod -n $NAMESPACE || true
else
echo "No sglang job exist, start test case..."
break
fi
done
@@ -0,0 +1,255 @@
name: 'Nightly Test (NPU) Single Node Template'
on:
workflow_call:
inputs:
runner:
required: true
type: string
default: linux-aarch64-a3-16
test_type:
required: false
type: string
default: perf
description: perf or accuracy
test_config_name:
required: true
type: string
description: test config name
test_case:
required: true
type: string
description: path of test case file
image:
required: false
type: string
description: image for pods
default: "swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-a3"
install_sglang_from_source:
required: false
type: boolean
default: false
description: use sglang from source code or from docker image
install_sglang_deps:
required: false
type: boolean
default: false
description: install sglang dependencies (e.g. PyTorch, CANN packages) when using source installation
device_type_for_deps:
required: false
type: string
default: 'a3'
description: device type for dependency installation (a3 or 910b)
transformers_version:
required: false
type: string
default: ""
description: "The transformers version number for running sglang. Use default version in image if keep empty."
concurrency:
group: ascend-nightly-e2e-singlenode-${{ github.workflow_ref }}-${{ github.ref }}-${{ inputs.test_config_name }}
cancel-in-progress: true
jobs:
e2e:
name: ${{ inputs.test_config_name }}
runs-on: ${{ inputs.runner }}
container:
image: ${{ inputs.image }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check npu info
run: |
npu-smi info
- name: Install sglang dependencies
if: ${{ inputs.install_sglang_deps == true }}
shell: bash
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh ${{ inputs.device_type_for_deps }}
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
- name: Run test
timeout-minutes: 300
env:
SGLANG_USE_MODELSCOPE: true
HF_ENDPOINT: https://hf-mirror.com
SGLANG_IS_IN_CI: true
TRANSFORMERS_VERBOSITY: "error"
GDN_ATTN_BACKEND_TRITON: 1
SGLANG_TEST_METRICS_OUTPUT: /root/.cache/tests/output/metrics/metrics
shell: bash
run: |
sglang_source_path=$(pwd)
echo "Source code path: ${sglang_source_path}"
ln -sf ${sglang_source_path} /root/sglang
test_case=${{ inputs.test_case }}
if [ ! -f "${sglang_source_path}/${test_case}" ]; then
echo "The testcase does not exit: ${sglang_source_path}/${test_case}"
exit 1
fi
# prepare for output data
current_date=$(date +%Y%m%d)
test_data_output_path=/root/.cache/tests/output/${{ inputs.test_type }}/${current_date}
mkdir -p ${test_data_output_path}
tc_name=${test_case##*/}
tc_name=${tc_name%.*}
export METRICS_DATA_FILE=${test_data_output_path}/${tc_name}
mkdir -p ${METRICS_DATA_FILE}
echo "Metrics file path: ${METRICS_DATA_FILE}"
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
curl -o /tmp/test.jsonl -L https://gh-proxy.test.osinfra.cn/https://raw.githubusercontent.com/openai/grade-school-math/master/grade_school_math/data/test.jsonl
export TRANSFORMERS_VERSION_FOR_SGLANG="${{ inputs.transformers_version }}"
PYTHON_FOR_SGLANG="python"
PIP_FOR_SGLANG="pip"
if [ -n "${TRANSFORMERS_VERSION_FOR_SGLANG}" ];then
echo "===== Install transformers for sglang - Begin ====="
TRANSFORMERS_PKG_PATH_SOURCE=/root/.cache/.cache/transformers/${TRANSFORMERS_VERSION_FOR_SGLANG}
if [ ! -d "${TRANSFORMERS_PKG_PATH_SOURCE}" ]; then
echo "The dependent transformers package does not exist: ${TRANSFORMERS_PKG_PATH_SOURCE}."
echo "Install transformers ${TRANSFORMERS_VERSION_FOR_SGLANG} online."
pip install transformers=="${TRANSFORMERS_VERSION_FOR_SGLANG}" -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
else
echo "Install transformers ${TRANSFORMERS_VERSION_FOR_SGLANG} locally."
TRANSFORMERS_PKG_PATH_TARGET=/tmp/transformers/${TRANSFORMERS_VERSION_FOR_SGLANG}
mkdir -p "${TRANSFORMERS_PKG_PATH_TARGET}"
cp "${TRANSFORMERS_PKG_PATH_SOURCE}/*" "${TRANSFORMERS_PKG_PATH_TARGET}/"
pip install --no-index --find-links="${TRANSFORMERS_PKG_PATH_TARGET}" transformers=="${TRANSFORMERS_VERSION_FOR_SGLANG}"
fi
echo "===== Install transformers for sglang in virtual env - End ====="
fi
echo "Transformers version for sglang: $(${PIP_FOR_SGLANG} show transformers | grep Version | cut -d: -f2)"
echo "scaling_governor performance num: \
$(cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | grep performance | wc -l)"
echo "swappiness: $(cat /proc/sys/vm/swappiness)"
echo "numa_balancing: $(cat /proc/sys/kernel/numa_balancing)"
echo "sched_migration_cost_ns: $(cat /proc/sys/kernel/sched_migration_cost_ns)"
export SGLANG_TEST_MAX_RETRY=0
export SGLANG_SET_CPU_AFFINITY=1
echo "SGLANG_SET_CPU_AFFINITY: $SGLANG_SET_CPU_AFFINITY"
install_sglang_from_source=${{ inputs.install_sglang_from_source }}
if [ "$install_sglang_from_source" = "true" ] || [ "$install_sglang_from_source" = "True" ];then
echo "Install sglang from source"
commit_id=${{ github.sha }}
echo "commit id: ${commit_id}" > ${test_data_output_path}/commit_id
export PYTHONPATH=${sglang_source_path}/python:$PYTHONPATH
else
echo "Use sglang from image: ${{ inputs.image }}"
sglang_pkg_path=/sgl-workspace/sglang/python
ascend_test_util_path=${sglang_pkg_path}/sglang/test/ascend
mkdir -p ${ascend_test_util_path}
mv ${ascend_test_util_path} ${ascend_test_util_path}_bak
cp -r ${sglang_source_path}/python/sglang/test/ascend ${ascend_test_util_path}
fi
source /usr/local/Ascend/cann/set_env.sh || true
source /usr/local/Ascend/nnal/atb/set_env.sh || true
# Set environment of cann
log_path="/root/.cache/tests/logs/log/${current_date}/${tc_name}/${HOSTNAME}"
rm -rf ${log_path}
mkdir -p ${log_path}
echo "Log path: ${log_path}"
echo "Running test case ${test_case}"
test_exit_code=0
${PYTHON_FOR_SGLANG} -u ${test_case} 2>&1 | tee /tmp/test_output.log || test_exit_code=$?
echo "Finished test case ${test_case}"
if [ "${test_exit_code}" = "0" ]; then
test_status="pass"
status_icon="✅"
else
test_status="fail"
status_icon="❌"
fi
echo "test_status=${test_status}" >> $GITHUB_ENV
echo "tc_name=${tc_name}" >> $GITHUB_ENV
export test_status tc_name
echo "## ${tc_name} ${status_icon} ${test_status}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
metric_count=$(grep -c '\[METRIC\]' /tmp/test_output.log 2>/dev/null || echo 0)
if [ "${metric_count}" -gt 0 ]; then
echo "| Metric | Value | Pass |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|------|" >> $GITHUB_STEP_SUMMARY
grep '\[METRIC\]' /tmp/test_output.log | while IFS= read -r line; do
metric_name=$(echo "$line" | sed -E 's/.*\[METRIC\] ([^=]+)=.*/\1/')
metric_value=$(echo "$line" | sed -E 's/.*\[METRIC\] [^=]+=([^ ]+).*/\1/')
echo "| ${metric_name} | ${metric_value} | ${status_icon} |" >> $GITHUB_STEP_SUMMARY
done
else
echo "No metrics collected (test may have failed before producing results)." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "import json, re, os" > /tmp/dump_metrics.py
echo "tc = os.environ.get('tc_name', 'unknown')" >> /tmp/dump_metrics.py
echo "st = os.environ.get('test_status', 'unknown')" >> /tmp/dump_metrics.py
echo "tp = '${{ inputs.test_type }}'" >> /tmp/dump_metrics.py
echo "metrics = {}" >> /tmp/dump_metrics.py
echo "with open('/tmp/test_output.log') as f:" >> /tmp/dump_metrics.py
echo " for line in f:" >> /tmp/dump_metrics.py
echo " m = re.match(r'\[METRIC\] (\S+)=(\S+)', line)" >> /tmp/dump_metrics.py
echo " if m:" >> /tmp/dump_metrics.py
echo " v = m.group(2)" >> /tmp/dump_metrics.py
echo " try:" >> /tmp/dump_metrics.py
echo " v = float(v)" >> /tmp/dump_metrics.py
echo " except ValueError:" >> /tmp/dump_metrics.py
echo " pass" >> /tmp/dump_metrics.py
echo " metrics[m.group(1)] = v" >> /tmp/dump_metrics.py
echo "baselines = {}" >> /tmp/dump_metrics.py
echo "for k, v in list(metrics.items()):" >> /tmp/dump_metrics.py
echo " if k.endswith('_baseline'):" >> /tmp/dump_metrics.py
echo " baselines[k[:-9]] = v" >> /tmp/dump_metrics.py
echo " del metrics[k]" >> /tmp/dump_metrics.py
echo "with open('/tmp/metrics.json', 'w') as f:" >> /tmp/dump_metrics.py
echo " json.dump({'test_case': tc, 'test_type': tp, 'status': st, 'metrics': metrics, 'baselines': baselines}, f)" >> /tmp/dump_metrics.py
python3 /tmp/dump_metrics.py
exit ${test_exit_code}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@v4
with:
name: metrics-${{ inputs.test_config_name }}
path: /tmp/metrics.json
retention-days: 7
- name: Backup plog
if: always()
shell: bash
run: |
plog_path="/root/ascend/log/debug/plog"
if [ -d "$plog_path" ];then
echo "Plog files found. Begin to backup them."
tc_name=${{ inputs.test_case }}
tc_name=${tc_name##*/}
tc_name=${tc_name%.*}
target_plog_path="/root/.cache/tests/logs/plog/${tc_name}/${HOSTNAME}"
echo "Save path: ${target_plog_path}"
rm -rf ${target_plog_path}
mkdir -p ${target_plog_path}
cp ${plog_path}/* ${target_plog_path}
fi
+406
View File
@@ -0,0 +1,406 @@
name: Nightly Test (NPU)
on:
schedule:
- cron: '0 18 * * *' # Execute at 2:00 a.m. Beijing Time every day
pull_request:
branches:
- main
paths:
- ".github/workflows/nightly-test-npu.yml"
workflow_dispatch:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
image_a2:
description: 'The a2 running docker image of the test task.'
required: false
type: string
default: 'swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-910b'
image_a3:
description: 'The a3 running docker image of the test task.'
required: false
type: string
default: 'swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-a3'
skip_install_flag:
description: 'Indicates whether to skip the installation of sglang, defaulting to false.'
required: false
type: string
default: 'true'
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
image_a2:
description: 'The a2 running docker image of the test task.'
required: false
type: string
default: ''
image_a3:
description: 'The a3 running docker image of the test task.'
required: false
type: string
default: ''
skip_install_flag:
description: 'Indicates whether to skip the installation of sglang, defaulting to false.'
required: false
type: string
default: ''
concurrency:
group: nightly-test-npu-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
set-image-config:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.set-vars.outputs.ref }}
job_filter: ${{ steps.set-vars.outputs.job_filter }}
image_a2: ${{ steps.set-vars.outputs.image_a2 }}
image_a3: ${{ steps.set-vars.outputs.image_a3 }}
skip_install_flag: ${{ steps.set-vars.outputs.skip_install_flag }}
steps:
# When triggered by PR, no inputs parameters are used. The latest community code is tested by default.
- name: Set image config
id: set-vars
run: |
if [ -z "${{ inputs.ref }}" ]; then
echo "ref=" >> $GITHUB_OUTPUT
else
echo "ref=${{ inputs.ref }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.job_filter }}" ]; then
echo "job_filter=all" >> $GITHUB_OUTPUT
else
echo "job_filter=${{ inputs.job_filter }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.image_a2 }}" ]; then
echo "image_a2=swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-910b" >> $GITHUB_OUTPUT
else
echo "image_a2=${{ inputs.image_a2 }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.image_a3 }}" ]; then
echo "image_a3=swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-a3" >> $GITHUB_OUTPUT
else
echo "image_a3=${{ inputs.image_a3 }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.skip_install_flag }}" ]; then
echo "skip_install_flag=false" >> $GITHUB_OUTPUT
else
echo "skip_install_flag=${{ inputs.skip_install_flag }}" >> $GITHUB_OUTPUT
fi
nightly-poc-single-node-a2-tests:
name: single-node-poc-a2
if: ${{ !cancelled() }}
needs: [ set-image-config ]
strategy:
fail-fast: false
max-parallel: 6
matrix:
test_config:
- name: qwen3_32b_w8a8_2p_in3k5_out1k5_50ms_a2
runner: linux-aarch64-a2-4
test_case: test/registered/ascend/performance/qwen3_32b/test_npu_qwen3_32b_w8a8_2p_in3k5_out1k5_50ms_a2.py
test_type: 'perf'
uses: ./.github/workflows/nightly-test-npu-e2e-single-node.yml
with:
runner: ${{ matrix.test_config.runner }}
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.image_a2 }}
install_sglang_from_source: false
transformers_version: ''
nightly-poc-single-node-tests:
name: single-node-poc
if: ${{ !cancelled() }}
needs: [set-image-config]
strategy:
fail-fast: false
max-parallel: 6
matrix:
test_config:
# qwen3_6_35b_a3b performance tests
- name: qwen3_6_35b_a3b_1p_in3k5_out1k5_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in3k5_out1k5_50ms.py
test_type: 'perf'
- name: qwen3_6_35b_a3b_1p_aime26
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_aime26.py
- name: qwen3_6_35b_a3b_1p_in64k_out1k_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in64k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_35b_a3b_1p_in128k_out1k_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in128k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_35b_a3b_1p_in64k_out1k_prefix90_50ms_aime26
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in64k_out1k_prefix90_50ms_aime26.py
test_type: 'perf'
- name: qwen3_6_35b_a3b_1p_in128k_out1k_prefix90_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in128k_out1k_prefix90_50ms.py
test_type: 'perf'
# qwen3_6_27b performance tests
- name: qwen3_6_27b_w8a8_1p_in3k5_out1k5_50ms_gpqa
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in3k5_out1k5_50ms_gpqa.py
test_type: 'perf'
- name: qwen3_6_27b_w8a8_2p_in16k_out1k_50ms
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_2p_in16k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_w8a8_1p_in64k_out1k_50ms
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_w8a8_2p_in128k_out1k_50ms
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_2p_in128k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_2p_in64k_out1k_prefix90_50ms
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_2p_in64k_out1k_prefix90_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_1p_in1024x1024_30_out1024_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_1p_in1024x1024_30_out1024_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_1p_in1080p_30_out256_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_1p_in1080p_30_out256_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_1p_gpqa
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/qwen3_6_27b/test_npu_qwen3_6_27b_1p_gpqa.py
# qwen3_32b performance tests
- name: qwen3_32b_w8a8_2p_in3k5_out1k5_50ms_gpqa
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_32b/test_npu_qwen3_32b_w8a8_2p_in3k5_out1k5_50ms_gpqa.py
test_type: 'perf'
- name: qwen3_32b_bf16_8p_in18k_out4k_6ms
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/performance/qwen3_32b/test_npu_qwen3_32b_bf16_8p_in18k_out4k_6ms.py
test_type: 'perf'
- name: qwen3_32b_bf16_8p_gpqa
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/accuracy/qwen3_32b/test_npu_qwen3_32b_bf16_8p_gpqa.py
# qwen3_30b_a3b performance tests
- name: qwen3_30b_w8a8_1p_in3k5_out1k5_50ms_aime25
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_30b_a3b/test_npu_qwen3_30b_w8a8_1p_in3k5_out1k5_50ms_aime25.py
test_type: 'perf'
# qwen3-8b performance tests
- name: qwen3_8b_w8a8_1p_in3k5_out1k5_50ms_gpqa
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3-8b/test_npu_qwen3_8b_w8a8_1p_in3k5_out1k5_50ms_gpqa.py
test_type: 'perf'
- name: qwen3_8b_w8a8_1p_in6k_out1k5_bs16_gpqa
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3-8b/test_npu_qwen3_8b_w8a8_1p_in6k_out1k5_bs16_gpqa.py
test_type: 'perf'
# qwen3_next_80b_a3b_instruct performance tests
- name: qwen3_next_80b_w8a8_2p_in6k_out1k5_bs16_aime25
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_next_80b_a3b_instruct/test_npu_qwen3_next_80b_w8a8_2p_in6k_out1k5_bs16_aime25.py
test_type: 'perf'
# minimax_m2_5 performance tests
- name: minimax_m2_5_w8a8_8p_in3k5_out1k5_50ms_gpqa
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/performance/minimax_m2_5/test_npu_minimax_m2_5_w8a8_8p_in3k5_out1k5_50ms_gpqa.py
test_type: 'perf'
- name: minimax_m2_5_w8a8_4p_in64k_out1k_prefix90_50ms_gpqa
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/performance/minimax_m2_5/test_npu_minimax_m2_5_w8a8_4p_in64k_out1k_prefix90_50ms_gpqa.py
test_type: 'perf'
# deepseek_v3_2 accuracy tests
- name: deepseek_v3_2_8p_aime25
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/accuracy/deepseek_v3_2/test_npu_deepseek_v3_2_8p_aime25.py
# glm4_7_flash accuracy tests
- name: glm4_7_flash_1p_aime25
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/glm4_7_flash/test_npu_glm4_7_flash_1p_aime25.py
# glm4_6v_flash accuracy tests
- name: glm4_6v_flash_1p_mmmu
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/glm4_6v_flash/test_npu_glm4_6v_flash_1p_mmmu.py
# qwen3_vl_8b_thinking accuracy tests
- name: qwen3_vl_8b_thinking_1p_mmmu
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/qwen3_vl_8b_thinking/test_npu_qwen3_vl_8b_thinking_1p_mmmu.py
# qwen3_vl_30b_a3b_thinking accuracy tests
- name: qwen3_vl_30b_a3b_thinking_1p_mmmu
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/qwen3_vl_30b_a3b_thinking/test_npu_qwen3_vl_30b_a3b_thinking_1p_mmmu.py
uses: ./.github/workflows/nightly-test-npu-e2e-single-node.yml
with:
runner: ${{ matrix.test_config.runner }}
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.image_a3 }}
install_sglang_from_source: false
transformers_version: ''
nightly-poc-multi-node-tests:
name: multi-node-poc
if: ${{ !cancelled() }}
needs: [set-image-config, nightly-poc-single-node-tests]
strategy:
fail-fast: false
max-parallel: 1
matrix:
test_config:
# glm5_1 performance tests
- name: glm5_1_w4a8_1p1d_32p_in64k_out1k_50ms_aime26
prefill_size: 2
decode_size: 2
router_size: 1
test_case: test/registered/ascend/performance/glm5_1/test_npu_glm5_1_w4a8_1p1d_32p_in64k_out1k_50ms_aime26.py
test_type: 'perf'
prefill_decode_deployment: 'separation'
uses: ./.github/workflows/nightly-test-npu-e2e-multi-node.yml
with:
runner: linux-amd64-cpu-8
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
prefill_size: ${{ matrix.test_config.prefill_size }}
decode_size: ${{ matrix.test_config.decode_size }}
router_size: ${{ matrix.test_config.router_size }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.image_a3 }}
install_sglang_from_source: false
prefill_decode_deployment: ${{ matrix.test_config.prefill_decode_deployment }}
transformers_version: ''
nightly-poc-multi-node-mix-tests:
name: multi-node-mix-poc
if: ${{ !cancelled() }}
needs: [set-image-config, nightly-poc-single-node-tests, nightly-poc-multi-node-tests]
strategy:
fail-fast: false
max-parallel: 1
matrix:
test_config:
# kimi_k2_6 performance tests
- name: kimi_k2_6_w4a8_16p_in64k_out1k_100ms_aime25
node_size: 2
test_case: test/registered/ascend/performance/kimi_k2_6/test_npu_kimi_k2_6_w4a8_16p_in64k_out1k_100ms_aime25.py
test_type: 'perf'
uses: ./.github/workflows/nightly-test-npu-e2e-multi-node.yml
with:
runner: linux-amd64-cpu-8
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
node_size: ${{ matrix.test_config.node_size }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.image_a3 }}
install_sglang_from_source: false
prefill_decode_deployment: 'mix'
transformers_version: ''
check-all-jobs:
if: ${{ !cancelled() }}
needs:
- nightly-poc-single-node-a2-tests
- nightly-poc-single-node-tests
- nightly-poc-multi-node-tests
- nightly-poc-multi-node-mix-tests
runs-on: ubuntu-latest
steps:
- name: Download all metrics
uses: actions/download-artifact@v4
with:
pattern: metrics-*
path: /tmp/metrics
merge-multiple: false
- name: Generate results table
run: |
status_emoji() {
case "$1" in
pass) echo "✅" ;;
fail) echo "❌" ;;
*) echo "❓" ;;
esac
}
single_result_a2="${{ needs.nightly-poc-single-node-a2-tests.result }}"
single_result="${{ needs.nightly-poc-single-node-tests.result }}"
multi_result="${{ needs.nightly-poc-multi-node-tests.result }}"
mix_result="${{ needs.nightly-poc-multi-node-mix-tests.result }}"
group_icon() {
case "$1" in
success) echo "✅" ;;
failure) echo "❌" ;;
cancelled) echo "⏭️" ;;
skipped) echo "⏭️" ;;
*) echo "❓" ;;
esac
}
echo "## Nightly Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Group | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| single-node-poc-a2 | $(group_icon ${single_result_a2}) ${single_result_a2} |" >> $GITHUB_STEP_SUMMARY
echo "| single-node-poc | $(group_icon ${single_result}) ${single_result} |" >> $GITHUB_STEP_SUMMARY
echo "| multi-node-poc | $(group_icon ${multi_result}) ${multi_result} |" >> $GITHUB_STEP_SUMMARY
echo "| multi-node-mix-poc | $(group_icon ${mix_result}) ${mix_result} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Per-Test Metrics" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
has_metrics=false
for dir in /tmp/metrics/metrics-*/; do
file="${dir}metrics.json"
if [ -f "$file" ]; then
has_metrics=true
echo "import json" > /tmp/parse_metrics.py
echo "with open('$file') as f:" >> /tmp/parse_metrics.py
echo " d = json.load(f)" >> /tmp/parse_metrics.py
echo "tc = d.get('test_case', '-')" >> /tmp/parse_metrics.py
echo "tp = d.get('test_type', '-')" >> /tmp/parse_metrics.py
echo "st = d.get('status', 'unknown')" >> /tmp/parse_metrics.py
echo "icon = '✅' if st == 'pass' else '❌'" >> /tmp/parse_metrics.py
echo "metrics = d.get('metrics', {})" >> /tmp/parse_metrics.py
echo "baselines = d.get('baselines', {})" >> /tmp/parse_metrics.py
echo "mstr = ', '.join(f'{k}={v}' for k,v in metrics.items()) if metrics else '-'" >> /tmp/parse_metrics.py
echo "bstr = ', '.join(f'{k}={v}' for k,v in baselines.items()) if baselines else '-'" >> /tmp/parse_metrics.py
echo "print(f'| {tc} | {tp} | {icon} {st} | {mstr} | {bstr} |')" >> /tmp/parse_metrics.py
python3 /tmp/parse_metrics.py
fi
done > /tmp/metrics_table.txt
if [ "${has_metrics}" = "true" ]; then
echo "| Test Case | Type | Status | Metrics | Baseline |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|------|--------|---------|----------|" >> $GITHUB_STEP_SUMMARY
cat /tmp/metrics_table.txt >> $GITHUB_STEP_SUMMARY
else
echo "No per-test metrics available (artifacts not found)." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
FAIL=0
if [ "${single_result}" != "success" ] && [ "${single_result}" != "skipped" ]; then FAIL=1; fi
exit $FAIL
+808
View File
@@ -0,0 +1,808 @@
name: Nightly Test (Nvidia)
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: choice
default: 'all'
options:
- 'all'
- 'nightly-test-general-1-gpu-h100'
- 'nightly-test-general-4-gpu-h100'
- 'nightly-test-general-8-gpu-h200'
- 'nightly-test-general-8-gpu-h20'
- 'nightly-test-general-8-gpu-b200'
- 'nightly-test-text-accuracy-2-gpu-h100'
- 'nightly-test-text-perf-2-gpu-h100'
- 'nightly-test-vlm-accuracy-2-gpu-h100'
- 'nightly-test-vlm-perf-2-gpu-h100'
- 'nightly-test-perf-4-gpu-b200'
- 'nightly-test-perf-8-gpu-b200'
- 'nightly-test-specialized-8-gpu-b200'
- 'nightly-test-perf-4-gpu-gb300'
- 'nightly-test-kernel-1-gpu-h100'
- 'nightly-test-diffusion'
- 'nightly-test-kernel-8-gpu-h200'
- 'nightly-test-precision-8-gpu-h200'
force_baseline_update:
description: 'precision job only: refresh the rolling baseline instead of comparing (sets SGLANG_PRECISION_FORCE_UPDATE=1). Dispatch once after an intentional forward-path dtype/precision change stales the baseline; later scheduled runs compare against it and go green.'
required: false
type: boolean
default: false
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
concurrency:
group: nightly-test-nvidia-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
env:
SGLANG_IS_IN_CI: true
SGLANG_ENABLE_ASYNC_ASSERT: true
SGLANG_CUDA_COREDUMP: "1"
HF_HUB_DOWNLOAD_TIMEOUT: 300
HF_HUB_ETAG_TIMEOUT: 300
jobs:
# General tests - 1 GPU
nightly-test-general-1-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-1-gpu-h100')
runs-on: 1-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-1-gpu --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# JIT kernel full unit tests (expanded parameter ranges via SGLANG_JIT_KERNEL_RUN_FULL_TESTS)
nightly-test-kernel-1-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-kernel-1-gpu-h100')
runs-on: 1-gpu-h100
timeout-minutes: 60
env:
# Full jit_kernel test grids (see sglang.jit_kernel.utils.should_run_full_tests)
SGLANG_JIT_KERNEL_RUN_FULL_TESTS: "1"
# Match pr-test-jit-kernel workflow for consistent JIT warmup behavior
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
# Allow maintenance bypass on default branch (same semantics as PR JIT workflow)
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
timeout-minutes: 20
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run jit kernel nightly suite
timeout-minutes: 60
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-kernel-1-gpu --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
nightly-test-kernel-8-gpu-h200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-kernel-8-gpu-h200')
runs-on: 8-gpu-h200
timeout-minutes: 240
env:
SGLANG_JIT_KERNEL_RUN_FULL_TESTS: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
timeout-minutes: 20
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run multi-GPU jit kernel nightly suite
timeout-minutes: 90
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-kernel-8-gpu-h200 --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# General tests - 4 GPU H100
nightly-test-general-4-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-4-gpu-h100')
runs-on: 4-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-4-gpu --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# General tests - 8 GPU H200
nightly-test-general-8-gpu-h200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-8-gpu-h200')
runs-on: 8-gpu-h200
strategy:
fail-fast: false
max-parallel: 2
matrix:
partition: [0, 1, 2, 3]
env:
RUNNER_LABELS: 8-gpu-h200
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run common 8-GPU model tests
if: always()
timeout-minutes: 300
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "8-gpu-h200"
IS_H200: "1"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-common --nightly --timeout-per-file=18000 --continue-on-error --auto-partition-id=${{ matrix.partition }} --auto-partition-size=4
- name: Publish traces to storage repo
if: always()
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
TRACE_ARGS=""
for dir in test/performance_profiles_*/; do
[ -d "$dir" ] && TRACE_ARGS="$TRACE_ARGS --traces-dir $dir"
done
if [ -n "$TRACE_ARGS" ]; then
python3 scripts/ci/utils/publish_traces.py $TRACE_ARGS
find test/performance_profiles_*/ -name '*.json.gz' -delete
else
echo "No trace directories found, skipping publish"
fi
- name: Run test
timeout-minutes: 30
env:
GPU_CONFIG: "8-gpu-h200"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-h200 --nightly --continue-on-error --auto-partition-id=${{ matrix.partition }} --auto-partition-size=4
- name: Collect performance metrics
if: always()
run: |
python3 scripts/ci/utils/save_metrics.py \
--gpu-config 8-gpu-h200 \
--partition ${{ matrix.partition }} \
--run-id ${{ github.run_id }} \
--output test/metrics-8gpu-h200-partition-${{ matrix.partition }}.json \
--search-dir test/performance_profiles_8_gpu \
--search-dir test
- name: Upload partition metrics
if: always()
uses: actions/upload-artifact@v4
with:
name: metrics-8gpu-h200-partition-${{ matrix.partition }}
path: test/metrics-8gpu-h200-partition-${{ matrix.partition }}.json
retention-days: 5
if-no-files-found: ignore
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.partition }}
# General tests - 8 GPU H20
nightly-test-general-8-gpu-h20:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-8-gpu-h20')
runs-on: 8-gpu-h20
env:
SGLANG_CI_RDMA_ALL_DEVICES: "mlx5_1,mlx5_2,mlx5_3,mlx5_4"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 30
env:
GPU_CONFIG: "8-gpu-h20"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-h20 --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# General tests - 8 GPU B200
nightly-test-general-8-gpu-b200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-8-gpu-b200')
runs-on: 8-gpu-b200
strategy:
fail-fast: false
max-parallel: 2
matrix:
partition: [0, 1, 2, 3]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run common 8-GPU model tests
if: always()
timeout-minutes: 200
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "8-gpu-b200"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-common --nightly --timeout-per-file=12000 --continue-on-error --auto-partition-id=${{ matrix.partition }} --auto-partition-size=4
- name: Publish traces to storage repo
if: always()
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
TRACE_ARGS=""
for dir in test/performance_profiles_*/; do
[ -d "$dir" ] && TRACE_ARGS="$TRACE_ARGS --traces-dir $dir"
done
if [ -n "$TRACE_ARGS" ]; then
python3 scripts/ci/utils/publish_traces.py $TRACE_ARGS
find test/performance_profiles_*/ -name '*.json.gz' -delete
else
echo "No trace directories found, skipping publish"
fi
- name: Collect performance metrics
if: always()
run: |
python3 scripts/ci/utils/save_metrics.py \
--gpu-config 8-gpu-b200 \
--partition ${{ matrix.partition }} \
--run-id ${{ github.run_id }} \
--output test/metrics-8gpu-b200-partition-${{ matrix.partition }}.json \
--search-dir test/performance_profiles_8_gpu \
--search-dir test
- name: Upload partition metrics
if: always()
uses: actions/upload-artifact@v4
with:
name: metrics-8gpu-b200-partition-${{ matrix.partition }}
path: test/metrics-8gpu-b200-partition-${{ matrix.partition }}.json
retention-days: 5
if-no-files-found: ignore
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.partition }}
# Text model accuracy tests
nightly-test-text-accuracy-2-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-text-accuracy-2-gpu-h100')
runs-on: 2-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run eval test for text models
timeout-minutes: 120
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-eval-text-2-gpu --nightly --continue-on-error --timeout-per-file 4500
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# Text model performance tests
nightly-test-text-perf-2-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-text-perf-2-gpu-h100')
runs-on: 2-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run performance test for text models
timeout-minutes: 30
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "2-gpu-h100"
run: |
cd test
rm -rf performance_profiles_text_models/
python3 run_suite.py --hw cuda --suite nightly-perf-text-2-gpu --nightly --continue-on-error --timeout-per-file 3600
- name: Publish traces to storage repo
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
python3 scripts/ci/utils/publish_traces.py --traces-dir test/performance_profiles_text_models
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# VLM accuracy tests
nightly-test-vlm-accuracy-2-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-vlm-accuracy-2-gpu-h100')
runs-on: 2-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run eval test for VLM models (fixed MMMU-100)
timeout-minutes: 120
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-eval-vlm-2-gpu --nightly --continue-on-error --timeout-per-file 9000
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# VLM performance tests
nightly-test-vlm-perf-2-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-vlm-perf-2-gpu-h100')
runs-on: 2-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run perf test for VLM models (MMMU)
timeout-minutes: 30
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "2-gpu-h100"
run: |
cd test
rm -rf performance_profiles_vlms/
python3 run_suite.py --hw cuda --suite nightly-perf-vlm-2-gpu --nightly --continue-on-error --timeout-per-file 3600
- name: Publish traces to storage repo
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
python3 scripts/ci/utils/publish_traces.py --traces-dir test/performance_profiles_vlms
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# B200 Performance tests - 4 GPU
nightly-test-perf-4-gpu-b200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-perf-4-gpu-b200')
runs-on: 4-gpu-b200
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 200
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "4-gpu-b200"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-4-gpu-b200 --nightly --continue-on-error --timeout-per-file 12000
- name: Publish traces to storage repo
if: always()
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
TRACE_ARGS=""
for dir in test/performance_profiles_*/; do
[ -d "$dir" ] && TRACE_ARGS="$TRACE_ARGS --traces-dir $dir"
done
if [ -n "$TRACE_ARGS" ]; then
python3 scripts/ci/utils/publish_traces.py $TRACE_ARGS
find test/performance_profiles_*/ -name '*.json.gz' -delete
else
echo "No trace directories found, skipping publish"
fi
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# GB300 (Grace-Blackwell NVL4) performance tests - 4 GPU (ARM64)
nightly-test-perf-4-gpu-gb300:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-perf-4-gpu-gb300')
name: nightly-test-perf-4-gpu-gb300 (${{ matrix.model }})
runs-on: 4-gpu-gb300-nightly
strategy:
fail-fast: false
matrix:
include:
- model: glm5-nvfp4
suite: nightly-4-gpu-gb300-glm5-nvfp4
- model: qwen35-fp8
suite: nightly-4-gpu-gb300-qwen35-fp8
- model: deepseek-v4-pro-fp4
suite: nightly-4-gpu-gb300-deepseek-v4-pro-fp4
- model: kimi-k25-nvfp4
suite: nightly-4-gpu-gb300-kimi-k25-nvfp4
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
env:
GRACE_BLACKWELL: "1"
run: |
bash scripts/ci/cuda/ci_install_deepep.sh
- name: Run test
timeout-minutes: 600
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "4-gpu-gb300"
run: |
cd test
python3 run_suite.py --hw cuda --suite ${{ matrix.suite }} --nightly --continue-on-error --timeout-per-file 7200
- name: Publish traces to storage repo
if: always()
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
TRACE_ARGS=""
for dir in test/performance_profiles_*/; do
[ -d "$dir" ] && TRACE_ARGS="$TRACE_ARGS --traces-dir $dir"
done
if [ -n "$TRACE_ARGS" ]; then
python3 scripts/ci/utils/publish_traces.py $TRACE_ARGS
find test/performance_profiles_*/ -name '*.json.gz' -delete
else
echo "No trace directories found, skipping publish"
fi
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# Specialized B200 tests - 8 GPU, for specific backends and configs
nightly-test-specialized-8-gpu-b200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-perf-8-gpu-b200' || inputs.job_filter == 'nightly-test-specialized-8-gpu-b200')
runs-on: 8-gpu-b200
env:
RUNNER_LABELS: 8-gpu-b200
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
env:
GPU_CONFIG: "8-gpu-b200"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-b200 --nightly --continue-on-error --timeout-per-file 2400
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# SGLang-Diffusion nightly benchmark
nightly-test-diffusion:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-diffusion')
runs-on: 4-gpu-h100
timeout-minutes: 300
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion benchmark
env:
GITHUB_SHA: ${{ github.sha }}
GITHUB_RUN_ID: ${{ github.run_id }}
PYTHONUNBUFFERED: "1"
timeout-minutes: 210
run: |
python3 -u scripts/ci/utils/diffusion/run_comparison.py \
--output comparison-results.json
- name: Generate dashboard
if: always()
env:
GH_PAT_FOR_NIGHTLY_CI_DATA: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GH_TOKEN: ${{ github.token }}
run: |
python3 scripts/ci/utils/diffusion/generate_diffusion_dashboard.py \
--results comparison-results.json \
--output dashboard.md \
--charts-dir comparison-charts \
--fetch-history \
--step-summary
- name: Publish to sglang-ci-data
if: always()
env:
GH_PAT_FOR_NIGHTLY_CI_DATA: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python3 scripts/ci/utils/diffusion/publish_comparison_results.py \
--results comparison-results.json \
--dashboard dashboard.md \
--charts-dir comparison-charts
- name: Upload benchmark artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-benchmark-${{ github.run_id }}
path: |
comparison-results.json
dashboard.md
comparison-charts/
comparison-logs/
retention-days: 90
if-no-files-found: ignore
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# Nightly precision regression - per-layer hidden state comparison
nightly-test-precision-8-gpu-h200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-precision-8-gpu-h200')
runs-on: 8-gpu-h200
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run precision regression test
timeout-minutes: 120
env:
SGLANG_PRECISION_BASELINE_DIR: /tmp/sglang_precision_baselines
# Required: the test errors if SGLANG_PRECISION_HF_REPO is unset (no
# local-only mode). Set the var + the HF_TOKEN_PRECISION_STORE secret.
SGLANG_PRECISION_HF_REPO: ${{ vars.SGLANG_PRECISION_HF_REPO }}
SGLANG_PRECISION_HF_REVISION: ${{ vars.SGLANG_PRECISION_HF_REVISION || 'main' }}
HF_TOKEN: ${{ secrets.HF_TOKEN_PRECISION_STORE }}
SGLANG_PRECISION_COMMIT: ${{ github.sha }}
# '0' on scheduled runs (input unset); '1' only on a manual baseline-refresh dispatch.
SGLANG_PRECISION_FORCE_UPDATE: ${{ inputs.force_baseline_update && '1' || '0' }}
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-precision-8-gpu-h200 --nightly --continue-on-error --timeout-per-file 3600
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# Consolidate performance metrics from all jobs
consolidate-metrics:
if: github.repository == 'sgl-project/sglang' && always()
needs:
- nightly-test-general-8-gpu-h200
- nightly-test-general-8-gpu-b200
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Download all partition metrics
uses: actions/download-artifact@v4
with:
pattern: "*metrics-*"
path: metrics/
merge-multiple: true
- name: List downloaded metrics
run: |
echo "Downloaded metrics files:"
find metrics/ -name "*.json" -type f 2>/dev/null || echo "No metrics files found"
- name: Merge metrics
run: |
python3 scripts/ci/utils/merge_metrics.py \
--input-dir metrics/ \
--output consolidated-metrics-${{ github.run_id }}.json \
--run-id ${{ github.run_id }} \
--commit-sha ${{ github.sha }} \
--branch ${{ github.ref_name }}
- name: Upload consolidated metrics
uses: actions/upload-artifact@v4
with:
name: consolidated-metrics-${{ github.run_id }}
path: consolidated-metrics-${{ github.run_id }}.json
retention-days: 90
if-no-files-found: warn
# Final check job
check-all-jobs:
if: github.repository == 'sgl-project/sglang' && always()
needs:
- nightly-test-general-1-gpu-h100
- nightly-test-general-4-gpu-h100
- nightly-test-general-8-gpu-h200
- nightly-test-general-8-gpu-h20
- nightly-test-general-8-gpu-b200
- nightly-test-text-accuracy-2-gpu-h100
- nightly-test-text-perf-2-gpu-h100
- nightly-test-vlm-accuracy-2-gpu-h100
- nightly-test-vlm-perf-2-gpu-h100
- nightly-test-perf-4-gpu-b200
- nightly-test-specialized-8-gpu-b200
- nightly-test-perf-4-gpu-gb300
- nightly-test-diffusion
- nightly-test-precision-8-gpu-h200
- consolidate-metrics
runs-on: ubuntu-latest
steps:
- name: Check if any job failed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more nightly test jobs failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more nightly test jobs were cancelled"
exit 1
fi
echo "All nightly test jobs passed"
@@ -0,0 +1,28 @@
name: Open A PR to Copy Code From OSS
on:
workflow_dispatch:
# schedule:
# - cron: '0 10 * * *'
permissions:
contents: write
jobs:
copy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: 'main'
- name: Install GitHub CLI (if not present)
run: |
bash scripts/code_sync/install_github_cli.sh
- name: Copy from OSS code
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_OPEN_PR_TO_PRIVATE }}
run: |
python3 scripts/code_sync/copy_from_oss.py
+31
View File
@@ -0,0 +1,31 @@
name: Open A PR to Copy Diff To OSS
on:
workflow_dispatch:
inputs:
commit_sha:
description: 'The commit SHA to copy. Defaults to LAST to copy the latest commit.'
required: false
default: 'LAST'
permissions:
contents: write
jobs:
copy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install GitHub CLI (if not present)
run: |
bash scripts/code_sync/install_github_cli.sh
- name: Copy to OSS code
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_OPEN_PR_TO_OSS }}
run: |
python3 scripts/code_sync/copy_to_oss.py --commit ${{ github.event.inputs.commit_sha }}
+118
View File
@@ -0,0 +1,118 @@
name: Patch Docker Image
on:
workflow_dispatch:
inputs:
pr_numbers:
description: "Comma-separated PR numbers to apply (e.g. 18962,19010)"
required: false
default: ""
image_tag:
description: "Base image tag to patch (e.g. dev-x86, dev-x86-cu13)"
required: true
concurrency:
group: patch-docker-${{ inputs.image_tag }}
cancel-in-progress: true
jobs:
patch:
if: github.repository == 'sgl-project/sglang'
runs-on: x64-docker-build-node
steps:
- name: Cleanup workspace (remove root-owned files from prior runs)
run: sudo rm -rf "$GITHUB_WORKSPACE"/* || true
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull base image and extract commit
run: |
IMAGE="lmsysorg/sglang:${{ inputs.image_tag }}"
docker pull "${IMAGE}"
if BASE_SHA=$(docker run --rm "${IMAGE}" git -C /sgl-workspace/sglang rev-parse HEAD 2>/dev/null); then
echo "Image built from commit: ${BASE_SHA}"
else
BASE_SHA=""
echo "::warning::Image has no .git directory — cannot extract base commit"
fi
echo "BASE_SHA=${BASE_SHA}" >> "$GITHUB_ENV"
- name: Generate patches
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git fetch origin main
mkdir -p /tmp/patch-ctx
if [ -n "${{ inputs.pr_numbers }}" ]; then
IFS=',' read -ra PRS <<< "${{ inputs.pr_numbers }}"
for pr in "${PRS[@]}"; do
pr=$(echo "${pr}" | xargs)
echo "Fetching PR #${pr}"
git fetch origin "pull/${pr}/head:pr-${pr}"
MERGE_BASE=$(git merge-base origin/main "pr-${pr}")
echo " PR #${pr}: merge-base=${MERGE_BASE}"
git diff "${MERGE_BASE}..pr-${pr}" > "/tmp/patch-ctx/${pr}.patch"
echo " PR #${pr}: $(wc -l < /tmp/patch-ctx/${pr}.patch) lines"
done
elif [ -n "${BASE_SHA}" ]; then
echo "Generating diff: image ${BASE_SHA} → latest main"
git fetch origin "${BASE_SHA}"
git diff "${BASE_SHA}..origin/main" > /tmp/patch-ctx/main.patch
echo " main: $(wc -l < /tmp/patch-ctx/main.patch) lines"
else
echo "::error::No PR numbers specified and image has no .git — cannot generate diff against main"
exit 1
fi
TOTAL=$(cat /tmp/patch-ctx/*.patch | wc -l)
if [ "${TOTAL}" -eq 0 ]; then
echo "::warning::All patches are empty — image is already up to date"
echo "SKIP_BUILD=true" >> "$GITHUB_ENV"
fi
- name: Build patched image
if: env.SKIP_BUILD != 'true'
run: |
IMAGE="lmsysorg/sglang:${{ inputs.image_tag }}"
cat <<'DOCKERFILE' > /tmp/patch-ctx/Dockerfile
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
COPY *.patch /tmp/patches/
RUN cd /sgl-workspace/sglang \
&& for p in /tmp/patches/*.patch; do \
if [ ! -s "${p}" ]; then \
echo "Skipping ${p} (empty)"; \
else \
echo "Applying ${p}..." \
&& patch -p1 --fuzz=2 --no-backup-if-mismatch -f < "${p}" \
|| { echo "ERROR: Failed to apply ${p}"; exit 1; }; \
fi; \
done \
&& rm -rf /tmp/patches
DOCKERFILE
docker build \
--no-cache \
--build-arg BASE_IMAGE="${IMAGE}" \
-t "${IMAGE}" \
/tmp/patch-ctx/
- name: Push patched image
if: env.SKIP_BUILD != 'true'
run: |
IMAGE="lmsysorg/sglang:${{ inputs.image_tag }}"
docker push "${IMAGE}"
echo "### Patched \`${IMAGE}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- **Base commit:** \`${BASE_SHA:-unknown (no .git)}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- **Source:** ${{ inputs.pr_numbers && format('PRs: {0}', inputs.pr_numbers) || 'latest main' }}" >> "$GITHUB_STEP_SUMMARY"
+198
View File
@@ -0,0 +1,198 @@
name: PR Benchmark (SMG Components)
on:
push:
branches: [ main ]
paths:
- "sgl-model-gateway/**"
pull_request:
branches: [ main ]
paths:
- "sgl-model-gateway/**"
workflow_dispatch:
concurrency:
group: pr-benchmark-rust-${{ github.ref }}
cancel-in-progress: true
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
permissions:
contents: read
pull-requests: write
issues: write
jobs:
benchmark-compile-check:
name: Benchmark Compilation Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_gateway_dependencies.sh
- name: Configure sccache
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: sgl-model-gateway
shared-key: "rust-cache"
save-if: true
cache-all-crates: true
cache-on-failure: true
- name: Check benchmarks compile
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
cargo check --benches
- name: Show sccache stats
if: always()
run: sccache --show-stats
benchmark:
name: Benchmark - ${{ matrix.name }}
if: |
github.repository == 'sgl-project/sglang' &&
(github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(contains(github.event.pull_request.labels.*.name, 'router-benchmark') &&
contains(github.event.pull_request.labels.*.name, 'run-ci')))
strategy:
fail-fast: false
matrix:
include:
- name: Request Processing
bench_name: request_processing
bench_args: "benchmark_summary --exact"
runner: ubuntu-latest
sccache_version: "v0.12.0"
artifact_name: request-processing-results
artifact_path: criterion/benchmark_summary/
- name: Manual Policy
bench_name: manual_policy_benchmark
bench_args: ""
runner: ubuntu-latest
sccache_version: "v0.12.0"
artifact_name: manual-policy-results
artifact_path: criterion/manual_policy*/
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 100
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_gateway_dependencies.sh
- name: Configure sccache
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: ${{ matrix.sccache_version }}
disable_annotations: true
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: sgl-model-gateway
shared-key: "rust-cache"
cache-all-crates: true
cache-on-failure: true
save-if: true
- name: Run benchmark
timeout-minutes: 30
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
if command -v sccache &> /dev/null; then
echo "Testing sccache availability..."
export RUSTC_WRAPPER=sccache
export SCCACHE_GHA_ENABLED="true"
if sccache --start-server 2>/dev/null && sccache --show-stats 2>/dev/null; then
echo "sccache is working, using it for compilation"
else
echo "sccache failed to start, falling back to regular cargo"
unset RUSTC_WRAPPER
unset SCCACHE_GHA_ENABLED
fi
else
echo "sccache not available, using regular cargo"
fi
cargo bench --bench ${{ matrix.bench_name }} -- ${{ matrix.bench_args }} 2>&1 | tee benchmark_output.txt
- name: Upload benchmark results
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}-${{ github.sha }}
path: |
sgl-model-gateway/target/${{ matrix.artifact_path }}
sgl-model-gateway/benchmark_output.txt
retention-days: 30
- name: Show sccache stats
if: always()
run: sccache --show-stats
benchmark-summary:
name: Benchmark Summary
needs: [benchmark]
if: always() && (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request')
runs-on: ubuntu-latest
steps:
- name: Download all benchmark results
uses: actions/download-artifact@v4
with:
pattern: '*-results-${{ github.sha }}'
path: benchmark-results
- name: Generate summary
run: |
generate_section() {
local title="$1" dir_name="$2" lines="${3:-100}"
local dir="benchmark-results/${dir_name}-${{ github.sha }}"
echo "### $title" >> summary.md
if [ -d "$dir" ]; then
echo "✅ **Completed**" >> summary.md
if [ -f "$dir/benchmark_output.txt" ]; then
echo -e "\n<details>\n<summary>View Results</summary>\n\n\`\`\`" >> summary.md
tail -"$lines" "$dir/benchmark_output.txt" >> summary.md
echo -e "\`\`\`\n</details>" >> summary.md
fi
else
echo "❌ Failed or skipped" >> summary.md
fi
echo "" >> summary.md
}
echo "## 🚀 Benchmark Results Summary" > summary.md
echo "" >> summary.md
generate_section "Request Processing" "request-processing-results" 60
generate_section "Manual Policy (Sticky Sessions)" "manual-policy-results" 100
echo -e "---\n_Generated at $(date -u '+%Y-%m-%d %H:%M:%S UTC')_" >> summary.md
cat summary.md
cat summary.md >> $GITHUB_STEP_SUMMARY
- name: Upload summary
uses: actions/upload-artifact@v4
with:
name: benchmark-summary-${{ github.sha }}
path: summary.md
retention-days: 30
+272
View File
@@ -0,0 +1,272 @@
on:
workflow_call:
inputs:
require-run-ci:
description: "Whether the PR must have the run-ci label"
type: boolean
default: true
require-run-ci-extra:
description: "Whether the PR must also have the run-ci-extra label (in addition to run-ci)"
type: boolean
default: false
cool-down-minutes:
description: "Cooldown period in minutes for low-permission users; 0 disables rate limiting"
type: number
default: 120
jobs:
pr-gate:
# 1. for commits on main: no gating needed
# 2. for workflow_dispatch: this can only be triggered by users with write access
runs-on: ubuntu-latest
steps:
- name: Fetch latest PR info
if: github.event_name == 'pull_request'
id: pr
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
core.setOutput("labels", JSON.stringify(pr.data.labels.map(l => l.name)));
core.setOutput("draft", pr.data.draft);
core.setOutput("user", pr.data.user.login);
- name: Log PR info
if: github.event_name == 'pull_request'
run: |
echo "===== PR Info ====="
echo "PR Event: ${{ github.event_name }}"
echo "PR Labels: ${{ steps.pr.outputs.labels }}"
echo "PR Draft: ${{ steps.pr.outputs.draft }}"
echo "PR User: ${{ steps.pr.outputs.user }}"
echo "Require run-ci: ${{ inputs.require-run-ci }}"
echo "Require run-ci-extra: ${{ inputs.require-run-ci-extra }}"
echo "Cool down minutes: ${{ inputs.cool-down-minutes }}"
echo "==================="
- name: Block draft PR
if: github.event_name == 'pull_request' && fromJson(steps.pr.outputs.draft)
run: |
echo "PR is draft. Blocking CI."
exit 1
- name: Require run-ci label (optional)
if: github.event_name == 'pull_request' && inputs.require-run-ci == true
run: |
labels='${{ steps.pr.outputs.labels }}'
if [[ "${{ contains(fromJson(steps.pr.outputs.labels), 'run-ci') }}" == "false" ]]; then
echo "Missing required label 'run-ci'. See https://docs.sglang.io/developer_guide/contribution_guide.html#how-to-trigger-ci-tests for more details."
exit 1
fi
# Live-fetch label gate for pr-test-extra.yml. Workflow-level `if`
# checks would read the frozen event payload, which doesn't update on
# rerun — so /tag-and-rerun-ci extra (slash handler adds the label
# then reruns) could never un-skip. A runtime step using gh-api-fetched
# labels lets reruns pick up the new label set.
- name: Require run-ci-extra label (optional)
if: github.event_name == 'pull_request' && inputs.require-run-ci-extra == true
run: |
if [[ "${{ contains(fromJson(steps.pr.outputs.labels), 'run-ci-extra') }}" == "false" ]]; then
echo "Missing required label 'run-ci-extra'. Add the label (e.g. via /tag-and-rerun-ci extra) to opt this PR into the extra test workflow."
exit 1
fi
- name: Enforce rate limit for low-permission actors (optional)
if: github.event_name == 'pull_request' && inputs.cool-down-minutes > 0
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const DEFAULT_MINUTES = Number("${{ inputs.cool-down-minutes }}");
const owner = context.repo.owner;
const repo = context.repo.repo;
const eventName = context.eventName;
const curRun = await github.rest.actions.getWorkflowRun({
owner, repo, run_id: context.runId
});
let triggeringActor = curRun.data.triggering_actor?.login || context.actor;
if (triggeringActor === "github-actions[bot]") {
triggeringActor = `${{ steps.pr.outputs.user }}`;
core.info(
`triggering_actor is github-actions[bot]; substituting PR author '${triggeringActor}'.`
);
}
async function hasHighPermission(username) {
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username });
const perm = data.permission || 'none';
return perm === 'write' || perm === 'maintain' || perm === 'admin';
} catch (e) {
if (e.status === 404 || e.status === 403) return false;
throw e;
}
}
if (await hasHighPermission(triggeringActor)) {
core.info(`Triggering user '${triggeringActor}' has high permission. No rate limit applied.`);
return;
}
let effectiveCooldownMinutes = DEFAULT_MINUTES;
let perUserCooldownMinutes = null;
try {
const contentResp = await github.rest.repos.getContent({
owner,
repo,
path: ".github/CI_PERMISSIONS.json",
ref: "main",
});
if (!Array.isArray(contentResp.data) && contentResp.data && "content" in contentResp.data) {
const raw = Buffer.from(
contentResp.data.content,
contentResp.data.encoding || "base64"
).toString();
const ciPermissions = JSON.parse(raw);
const userPerm = ciPermissions[triggeringActor];
if (userPerm && typeof userPerm.cooldown_interval_minutes === "number") {
perUserCooldownMinutes = userPerm.cooldown_interval_minutes;
core.info(
`Per-user cooldown for '${triggeringActor}' from CI_PERMISSIONS.json: ${perUserCooldownMinutes} minutes.`
);
} else {
core.info(`No per-user cooldown found for '${triggeringActor}' in CI_PERMISSIONS.json.`);
}
} else {
core.info("CI_PERMISSIONS.json content response is not a file; skipping per-user cooldown.");
}
} catch (e) {
core.info(`CI_PERMISSIONS.json not found or unreadable: ${e.message}. Using default rate limit only.`);
}
if (perUserCooldownMinutes !== null) {
effectiveCooldownMinutes = Math.min(effectiveCooldownMinutes, perUserCooldownMinutes);
}
if (effectiveCooldownMinutes <= 0) {
core.info(
`Effective cooldown for '${triggeringActor}' is 0 minutes; no rate limit enforced for this user.`
);
return;
}
const cutoff = new Date(Date.now() - effectiveCooldownMinutes * 60 * 1000);
core.info(
`Checking for workflow runs since ${cutoff.toISOString()} (last ${effectiveCooldownMinutes} minutes) for event '${eventName}'.`
);
const { data } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'pr-test.yml',
event: eventName,
per_page: 100,
});
const runs = data.workflow_runs || [];
// Rate Limiting Logic:
// We only count workflow runs that actually consumed CI resources (i.e., passed the gate).
// A run "passes the gate" if any jobs beyond the gate jobs (check-changes, pr-gate, call-gate)
// actually executed (not skipped/cancelled). This prevents scenarios where:
// - User has PR A with missing 'run-ci' label (fails at gate)
// - User opens PR B with 'run-ci' label
// - PR B should be able to run even though PR A triggered a run recently
// Helper function to check if a run passed the gate (i.e., actually consumed CI resources)
async function didRunPassGate(run) {
try {
// Note: Fetching up to 100 jobs (API maximum). If a workflow has >100 jobs,
// we may miss some, but this is unlikely in practice.
const { data: jobsData } = await github.rest.actions.listJobsForWorkflowRun({
owner, repo, run_id: run.id, per_page: 100
});
const jobs = jobsData.jobs || [];
// If no jobs exist yet, the run hasn't started consuming resources
if (jobs.length === 0) {
core.info(`Run ${run.id} has no jobs yet; not counting against rate limit.`);
return false;
}
// Gate jobs that don't consume significant CI resources
const gateJobs = ['check-changes', 'pr-gate', 'call-gate', 'pr-test-finish'];
const jobsBeyondGate = jobs.filter(j => !gateJobs.some(g => j.name === g || j.name.startsWith(g + ' ')));
// A job "ran" if it reached a terminal conclusion state that indicates actual execution
const ranStates = ['success', 'failure', 'timed_out', 'action_required'];
const hasJobsThatRan = jobsBeyondGate.some(j => j.conclusion && ranStates.includes(j.conclusion));
return hasJobsThatRan;
} catch (e) {
core.warning(`Could not check jobs for run ${run.id}: ${e.message}`);
// If it's a rate limit error, count it conservatively to prevent abuse
if (e.status === 429) {
core.warning(`Hit rate limit checking run ${run.id}; counting it to be safe.`);
return true;
}
// For cancelled/skipped runs, they likely didn't consume resources
if (run.conclusion === 'cancelled' || run.conclusion === 'skipped') {
return false;
}
// Default to counting it to prevent abuse
return true;
}
}
// Limit the number of runs we'll check in detail to avoid API rate limits
const MAX_RUNS_TO_CHECK = 5;
let runsChecked = 0;
let runsSkippedAtGate = 0;
let recentFound = null;
for (const run of runs) {
if (String(run.id) === String(context.runId)) continue;
if (new Date(run.created_at) < cutoff) continue;
const isUserRun = (run.actor?.login === triggeringActor) || (run.triggering_actor?.login === triggeringActor);
if (!isUserRun) continue;
runsChecked++;
core.info(`Checking run ${run.id} (created: ${run.created_at}, conclusion: ${run.conclusion})`);
// Safety limit: if we've checked too many runs, assume the next one passed to be conservative
if (runsChecked > MAX_RUNS_TO_CHECK) {
core.warning(`Checked ${MAX_RUNS_TO_CHECK} runs; assuming this one passed gate to avoid API limits.`);
recentFound = run;
break;
}
// Only count runs that actually passed the gate and consumed CI resources
if (await didRunPassGate(run)) {
recentFound = run;
core.info(`Found recent run ${run.id} that passed gate.`);
break;
} else {
runsSkippedAtGate++;
core.info(`Run ${run.id} failed at gate; not counting against rate limit.`);
}
}
core.info(`Rate limit check summary: checked ${runsChecked} runs, ${runsSkippedAtGate} failed at gate.`);
if (recentFound) {
core.setFailed(
`User '${triggeringActor}' already triggered '${context.workflow}' via '${eventName}' at ${recentFound.created_at}. ` +
`Please wait ${effectiveCooldownMinutes} minutes before triggering again.`
);
} else {
core.info(
`No recent runs detected for '${triggeringActor}' within the last ${effectiveCooldownMinutes} minutes; proceeding.`
);
}
+219
View File
@@ -0,0 +1,219 @@
name: PR States
# Maintains the CI-states block at the bottom of the PR body. Uses
# pull_request_target (not pull_request) for fork-PR write access; safe
# because we never check out PR head code, only API-read and PATCH the body.
#
# Three triggers, each covering a gap:
# - pull_request_target: push / open / label change (all PRs incl forks).
# - workflow_run: initial pr-test* completion (incl fork PRs; the
# notify-job below can't fire on forks — their pull_request
# GITHUB_TOKEN is forced read-only). Does NOT re-fire for rerun
# attempts, which is why workflow_dispatch is needed too.
# - workflow_dispatch: rerun completions for non-fork PRs, dispatched by
# pr-test*'s notify-pr-states job. workflow_dispatch is the only event
# GITHUB_TOKEN can cascade-create.
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled, unlabeled]
workflow_run:
workflows: ["PR Test Base", "PR Test Extra"]
types: [requested, completed]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number whose CI-states block should be refreshed.'
required: true
type: string
permissions:
pull-requests: write
actions: read
concurrency:
# Per-SHA dedupe (PR number for workflow_dispatch). Queues concurrent
# fires across the three triggers so they don't race on the body PATCH.
group: pr-states-${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha || inputs.pr_number }}
cancel-in-progress: false
jobs:
update-pr-body:
runs-on: ubuntu-latest
steps:
- name: Update CI states block in PR body
uses: actions/github-script@v8
with:
script: |
const RETRY_ATTEMPTS = 3;
const RETRY_DELAY_MS = 6000;
const LABEL_ON_PRESLEEP_MS = 3000;
const LABEL_OFF_PRESLEEP_MS = 6000;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Event-agnostic PR lookup.
// - pull_request_target: PR is in the payload.
// - workflow_run: pull_requests[] is populated for same-repo
// PRs; fork PRs need a head-ref reverse lookup
// ("forkOwner:branch") because
// listPullRequestsAssociatedWithCommit requires the commit to
// be in the default branch. Bail when no open PR matches.
// - workflow_dispatch: pr_number is an explicit input.
let prNumber;
if (context.payload.pull_request) {
prNumber = context.payload.pull_request.number;
} else if (context.eventName === 'workflow_run') {
const wr = context.payload.workflow_run;
if (wr.pull_requests && wr.pull_requests.length > 0) {
prNumber = wr.pull_requests[0].number;
} else if (wr.head_repository && wr.head_branch) {
const headSpec = `${wr.head_repository.owner.login}:${wr.head_branch}`;
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: headSpec,
});
// Multiple PRs could share a head ref if a branch was reused;
// match by head_sha to pick the right one.
const match = prs.find(p => p.head.sha === wr.head_sha) || prs[0];
if (!match) {
core.info(`No open PR for head=${headSpec}; skipping.`);
return;
}
prNumber = match.number;
} else {
core.info(`workflow_run has no PR linkage; skipping.`);
return;
}
} else if (context.eventName === 'workflow_dispatch') {
// Reject non-digit pr_number; parseInt('123abc', 10) returns
// 123 and would silently truncate trailing garbage.
const raw = context.payload.inputs.pr_number;
if (!/^\d+$/.test(raw)) {
core.setFailed(`workflow_dispatch: invalid pr_number input '${raw}' (must be digits only)`);
return;
}
prNumber = parseInt(raw, 10);
} else {
core.info(`Unsupported event '${context.eventName}'; skipping.`);
return;
}
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const sha = pr.head.sha;
const labels = pr.labels.map(l => l.name);
const hasCI = labels.includes('run-ci');
const hasExtra = labels.includes('run-ci-extra');
// Pre-sleep below only matters when a label was just removed;
// other no-label fires don't race against fresh indexing.
const justRemovedLabel = context.eventName === 'pull_request_target'
&& context.payload.action === 'unlabeled';
async function findRunBySha(workflowFile) {
const { data } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: workflowFile,
head_sha: sha,
per_page: 1,
});
return data.workflow_runs[0] || null;
}
async function findRun(workflowFile, { preSleepMs = 0, attempts = 1, delayMs = 0 }) {
if (preSleepMs > 0) await sleep(preSleepMs);
for (let i = 0; i < attempts; i++) {
const run = await findRunBySha(workflowFile);
if (run) return run;
if (i < attempts - 1) await sleep(delayMs);
}
return null;
}
// Query unconditionally: removing a label does NOT cancel an
// in-flight workflow, so a run from before removal can still be
// live. Label state only picks the fallback text. labelOnOpts
// covers fresh-push indexing race; labelOffOpts only pre-sleeps
// on just-removed-label to catch removal-after-push.
const labelOnOpts = { preSleepMs: LABEL_ON_PRESLEEP_MS, attempts: RETRY_ATTEMPTS, delayMs: RETRY_DELAY_MS };
const labelOffOpts = { preSleepMs: justRemovedLabel ? LABEL_OFF_PRESLEEP_MS : 0, attempts: 1 };
const ptRun = await findRun('pr-test.yml', hasCI ? labelOnOpts : labelOffOpts);
// pr-test-extra gates on BOTH labels (see check-changes there).
const peRun = await findRun('pr-test-extra.yml', (hasCI && hasExtra) ? labelOnOpts : labelOffOpts);
// Skipped run = "no real run" -- happens when a label is added
// after a commit, because GHA doesn't retrigger on `labeled`.
const isReal = (run) => run && run.conclusion !== 'skipped';
const missingCIText = ':x: **Missing `run-ci` label** -- add it to run CI tests.';
const peBlockedByCIText = ':x: **Blocked** -- `run-ci` is required first.';
const notExtraEnabledText = ':warning: **Not enabled** -- add `run-ci-extra` label to opt in.';
const stalePushText = ':warning: **Not run on latest push** -- push again to dispatch.';
// Status icon: body otherwise looks uniformly "dispatched" even
// for gate-failed runs (call-gate exits on missing label).
function runIcon(run) {
if (run.status !== 'completed') return ':hourglass_flowing_sand:';
switch (run.conclusion) {
case 'success': return ':white_check_mark:';
case 'failure': return ':x:';
case 'cancelled': return ':no_entry_sign:';
case 'timed_out': return ':alarm_clock:';
case 'action_required': return ':warning:';
case 'neutral': return ':grey_question:';
default: return ':grey_question:';
}
}
const runLink = (run) => `${runIcon(run)} [Run #${run.id}](${run.html_url})`;
const ptText = isReal(ptRun)
? runLink(ptRun)
: (!hasCI ? missingCIText : '_Not run yet_');
const peText = isReal(peRun)
? runLink(peRun)
: !hasCI
? peBlockedByCIText
: !hasExtra
? notExtraEnabledText
: stalePushText;
const outerStart = '<!-- pr-states:start -->';
const outerEnd = '<!-- pr-states:end -->';
const ptStart = '<!-- slot:pr-test:start -->';
const ptEnd = '<!-- slot:pr-test:end -->';
const peStart = '<!-- slot:pr-test-extra:start -->';
const peEnd = '<!-- slot:pr-test-extra:end -->';
const newBlock = [
outerStart,
'---',
'### CI States',
'',
`Latest PR Test (Base): ${ptStart}${ptText}${ptEnd}`,
`Latest PR Test (Extra): ${peStart}${peText}${peEnd}`,
outerEnd,
].join('\n');
const body = pr.body || '';
const blockRe = new RegExp(`(?:\\n+---\\n+)?${outerStart}[\\s\\S]*?${outerEnd}`);
let newBody;
if (blockRe.test(body)) {
newBody = body.replace(blockRe, `\n\n${newBlock}`);
} else {
const sep = body.length === 0
? ''
: (body.endsWith('\n') ? '\n' : '\n\n');
newBody = `${body}${sep}${newBlock}`;
}
if (newBody !== body) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
body: newBody,
});
}
+244
View File
@@ -0,0 +1,244 @@
name: PR Test Extra (AMD)
# Label-gated AMD extra CI workflow — the AMD mirror of pr-test-extra.yml.
#
# Adds AMD runtime to a PR only when the author opts in: the PR must carry
# BOTH `run-ci` (basic-CI prerequisite) and `run-ci-extra` (explicit opt-in).
# The label check happens at runtime in pr-gate.yml via a live
# `gh pr view`-style fetch, so reruns after adding the labels (e.g. via a
# slash command) pick up the new label set — a workflow-level `if` would read
# the frozen event payload, which never updates on rerun. The job graph also
# runs unconditionally on workflow_dispatch / workflow_call so it can be
# triggered manually or chained from the AMD scheduler.
#
# Stage: extra-a. Each job mirrors the container bring-up of pr-test-amd.yml
# and dispatches `run_suite.py --hw amd --suite extra-a-test-{config}-amd`:
# - 1-gpu-small: mock-model / kv_canary unit + single-GPU canary e2e tests
# - 1-gpu-large: single-GPU model e2e tests (quant fp8kv-triton,
# sessions streaming-session, spec standalone triton,
# kv_canary self-bench)
# - 2-gpu-large: multi-GPU (TP/PP/PD) mock-model + kv_canary e2e tests
# kv_canary e2e is registered to the same extra-a stage as its CUDA siblings
# (it now exercises the ROCm canary kernels end-to-end).
on:
pull_request:
# `labeled` lets the workflow re-fire when `run-ci-extra` (or `run-ci`)
# is added after the latest push. See call-gate.if for the matching guard
# that prevents unrelated label additions from dispatching a full run.
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
inputs:
runner_arch:
description: 'AMD runner pool to dispatch GPU jobs to'
required: false
type: choice
default: mi325
options:
- mi300
- mi325
rocm_version:
description: 'ROCm container variant (empty = Dockerfile default; rocm720 = ROCm 7.2.0)'
required: false
type: choice
default: ''
options:
- ''
- rocm720
aiter_ref:
description: 'Override AITER commit (optional, leave empty to use Dockerfile default)'
required: false
type: string
default: ''
continue_on_error:
description: 'Continue on error (do not fail the workflow on test failures)'
required: false
type: boolean
default: false
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
rocm_version:
description: 'ROCm container variant (empty = Dockerfile default; rocm720 = ROCm 7.2.0)'
required: false
type: string
default: ''
aiter_ref:
description: 'Override AITER commit (optional, leave empty to use Dockerfile default)'
required: false
type: string
default: ''
continue_on_error:
description: 'Continue on error (do not fail the workflow on test failures)'
required: false
type: boolean
default: false
env:
AITER_COMMIT_OVERRIDE: ${{ inputs.aiter_ref }}
DOCKERHUB_AMD_USERNAME: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
DOCKERHUB_AMD_TOKEN: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
concurrency:
# Mirror pr-test-amd.yml: schedule/dispatch runs key on run_id (unique group,
# never cancel each other); PR runs share a per-branch group so pushes cancel
# stale runs. (In a reusable workflow github.event_name is the originating
# event, never workflow_call, so the guard keys off the real events.)
group: pr-test-amd-extra-${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && format('full-{0}', github.run_id) || github.head_ref || github.ref_name || inputs.ref || 'default' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
actions: write
contents: read
issues: read
pull-requests: read
jobs:
# =============================================== PR Gate ====================================================
# Runtime live-fetch label gate (mirrors pr-test-extra.yml's call-gate):
# requires both `run-ci` and `run-ci-extra`. A failure here cascades to
# every test job via `needs`, so a PR without the labels ends in one red
# ~30s gate job plus a row of skipped jobs instead of consuming AMD runners.
#
# The job-level `if` only filters the `labeled` event type so that adding an
# unrelated label doesn't dispatch a full run; the actual label-presence
# gate is enforced at runtime inside pr-gate.yml.
call-gate:
if: |
github.event_name != 'pull_request' ||
github.event.action != 'labeled' ||
github.event.label.name == 'run-ci' ||
github.event.label.name == 'run-ci-extra'
uses: ./.github/workflows/pr-gate.yml
with:
require-run-ci: true
require-run-ci-extra: true
secrets: inherit
# =============================================== extra-a (1-gpu-small) ===============================================
# Single unpartitioned job: the 21 onboarded unit tests total ~233s, so the
# expensive per-job setup (container bring-up + sgl-kernel ROCm build + dep
# install, several minutes) dominates. Partitioning would multiply that
# setup across scarce AMD GPUs to shave only a couple minutes of test time,
# so one GPU running the whole suite sequentially is the better trade.
extra-a-test-1-gpu-small-amd:
name: ${{ format('extra-a-test-1-gpu-small-amd{0} (linux-{1}-1gpu-sglang)', inputs.rocm_version && format('-{0}', inputs.rocm_version) || '', inputs.runner_arch || 'mi325') }}
needs: [call-gate]
if: ${{ !cancelled() && needs.call-gate.result == 'success' }}
runs-on: ${{ format('linux-{0}-1gpu-sglang', inputs.runner_arch || 'mi325') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Ensure VRAM is clear
run: bash scripts/ci/amd/ensure_vram_clear.sh rocm
- name: Start CI container
# `rocm_version` (e.g. rocm720) selects an alternate ROCm container; empty uses the Dockerfile default.
run: bash scripts/ci/amd/amd_ci_start_container.sh ${{ inputs.rocm_version && format('--rocm-version {0}', inputs.rocm_version) || '' }}
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install dependencies
run: bash scripts/ci/amd/amd_ci_install_dependency.sh
- name: Run test
timeout-minutes: 45
run: |
bash scripts/ci/amd/amd_ci_exec.sh -w "/sglang-checkout/test" python3 run_suite.py --hw amd --suite extra-a-test-1-gpu-small-amd --timeout-per-file 1800 ${{ inputs.continue_on_error == true && '--continue-on-error' || '' }}
# =============================================== extra-a (1-gpu-large) ===============================================
# Single-GPU lora / spec / quant model e2e tests. Runs on the same 1-gpu
# pool as small (AMD GPUs carry enough VRAM that "large" here is a CUDA
# memory-tier label, not a separate AMD runner pool).
extra-a-test-1-gpu-large-amd:
name: ${{ format('extra-a-test-1-gpu-large-amd{0} (linux-{1}-1gpu-sglang)', inputs.rocm_version && format('-{0}', inputs.rocm_version) || '', inputs.runner_arch || 'mi325') }}
needs: [call-gate]
if: ${{ !cancelled() && needs.call-gate.result == 'success' }}
runs-on: ${{ format('linux-{0}-1gpu-sglang', inputs.runner_arch || 'mi325') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Ensure VRAM is clear
run: bash scripts/ci/amd/ensure_vram_clear.sh rocm
- name: Start CI container
run: bash scripts/ci/amd/amd_ci_start_container.sh ${{ inputs.rocm_version && format('--rocm-version {0}', inputs.rocm_version) || '' }}
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install dependencies
run: bash scripts/ci/amd/amd_ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
run: |
bash scripts/ci/amd/amd_ci_exec.sh -w "/sglang-checkout/test" python3 run_suite.py --hw amd --suite extra-a-test-1-gpu-large-amd --timeout-per-file 1800 ${{ inputs.continue_on_error == true && '--continue-on-error' || '' }}
# =============================================== extra-a (2-gpu-large) ===============================================
# Multi-GPU TP / PP / PD mock-model + kv_canary e2e tests. Mirrors CUDA's
# extra-a 2-gpu-large stage; runs on the 2-GPU AMD pool.
extra-a-test-2-gpu-large-amd:
name: ${{ format('extra-a-test-2-gpu-large-amd{0} (linux-{1}-2gpu-sglang)', inputs.rocm_version && format('-{0}', inputs.rocm_version) || '', inputs.runner_arch || 'mi325') }}
needs: [call-gate]
if: ${{ !cancelled() && needs.call-gate.result == 'success' }}
runs-on: ${{ format('linux-{0}-2gpu-sglang', inputs.runner_arch || 'mi325') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Ensure VRAM is clear
run: bash scripts/ci/amd/ensure_vram_clear.sh rocm
- name: Start CI container
run: bash scripts/ci/amd/amd_ci_start_container.sh ${{ inputs.rocm_version && format('--rocm-version {0}', inputs.rocm_version) || '' }}
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install dependencies
run: bash scripts/ci/amd/amd_ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
run: |
bash scripts/ci/amd/amd_ci_exec.sh -w "/sglang-checkout/test" python3 run_suite.py --hw amd --suite extra-a-test-2-gpu-large-amd --timeout-per-file 1800 ${{ inputs.continue_on_error == true && '--continue-on-error' || '' }}
# =============================================== aggregator ====================================================
# Single fan-in job so branch protection / notifications depend on one job
# rather than every matrix leg. Fails if any dependent failed or was
# cancelled; `skipped` (e.g. PR without the opt-in labels) is not a failure.
pr-test-amd-extra-finish:
needs:
[
call-gate,
extra-a-test-1-gpu-small-amd,
extra-a-test-1-gpu-large-amd,
extra-a-test-2-gpu-large-amd,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
json_needs='${{ toJson(needs) }}'
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
echo "$job: $result"
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
echo "All jobs completed successfully"
exit 0
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
name: PR Test (Arm64)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-arm64-${{ inputs.ref || github.ref }}
cancel-in-progress: false
jobs:
check-changes:
runs-on: ubuntu-latest
outputs:
main_package: ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests}}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_cpu.toml"
- "test/**/!(*.md)"
- "sgl-kernel/**/*.!(md|txt)"
- ".github/workflows/pr-test-arm64.yml"
- "docker/arm64.Dockerfile"
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.main_package == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
build-test:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Build container
run: |
PR_REPO=${{ github.event.pull_request.head.repo.clone_url }}
PR_HEAD_REF=${{ github.head_ref }}
docker build \
${PR_REPO:+--build-arg SGLANG_REPO=$PR_REPO} \
${PR_HEAD_REF:+--build-arg VER_SGLANG=$PR_HEAD_REF} \
. -f docker/arm64.Dockerfile -t sglang_arm64 --no-cache
- name: Run container
run: |
docker run -dt \
-v ${{ github.workspace }}:/sglang-checkout/ --ipc=host \
--name ci_sglang_arm64 \
sglang_arm64
- name: Arm sanity check
timeout-minutes: 5
run: |
docker exec -w /sglang-checkout/ ci_sglang_arm64 \
bash -c "source /opt/.venv/bin/activate && python3 -c 'import platform; import torch; import sgl_kernel; from sglang.srt.utils.common import is_host_cpu_arm64; assert platform.machine() in (\"aarch64\", \"arm64\"); assert is_host_cpu_arm64(); assert hasattr(torch.ops.sgl_kernel, \"decode_attention_cpu\"); assert hasattr(torch.ops.sgl_kernel, \"initialize\");'"
- name: Run unit tests
timeout-minutes: 36
run: |
docker exec -w /sglang-checkout/ ci_sglang_arm64 \
bash -c "source /opt/.venv/bin/activate && cd ./test && python3 run_suite.py --hw cpu --suite base-b-test-cpu-arm64 --timeout-per-file 1500"
- name: Change permission
timeout-minutes: 2
run: |
docker exec -u root ci_sglang_arm64 bash -c "
rm -rf /tmp/ci-home &&
chown -R $(id -u):$(id -g) /sglang-checkout/ 2>/dev/null || true
"
- name: Cleanup container
if: always()
run: |
docker rm -f ci_sglang_arm64 || true
+320
View File
@@ -0,0 +1,320 @@
name: PR Test Extra
# Label-gated extra CI workflow opted into per-PR via labels.
#
# Adds runtime to a PR only when the author asks for it: the PR must carry
# BOTH `run-ci` (basic-CI prerequisite) and `run-ci-extra` (explicit opt-in
# to this workflow). The label check happens at runtime in pr-gate.yml via
# live `gh pr view --json labels`, so reruns after adding the labels via
# slash command pick up the new label set (workflow-level `if` checks would
# read the frozen event payload, which never updates on rerun). The same
# job graph runs unconditionally on workflow_dispatch / workflow_call so it
# can be triggered manually or chained from another workflow.
#
# Stages: extra-a (1-/2-gpu) and extra-b (4-/8-gpu) caller stubs reuse
# `_pr-test-stage.yml` and `_pr-test-check-changes.yml` from pr-test.yml.
on:
pull_request:
# `labeled` lets the workflow re-fire when `run-ci-extra` (or `run-ci`)
# is added after the latest push — otherwise GHA leaves the stale
# skipped run in place and there's no way to enable extra without
# another push. See check-changes.if below for the matching guard.
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
inputs:
force_continue_on_error:
description: "Force continue-on-error (test scheduled CI behavior)"
required: false
type: boolean
default: false
test_parallel_dispatch:
description: "Test parallel dispatch behavior (simulates scheduled run)"
required: false
type: boolean
default: false
run_all_tests:
description: "Run all tests — bypasses paths-filter (needed when dispatching against long-lived branches whose diff against main can't resolve a merge-base in a shallow clone)"
required: false
type: boolean
default: false
workflow_call:
inputs:
git_ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
skip_pr_test_health_check:
description: "Skip PR test health check fast-fail (e.g. for release branch cuts)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-extra-${{ github.event_name }}-${{ github.head_ref || github.ref_name || 'default' }}-${{ inputs.git_ref || 'all' }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
SKIP_PR_TEST_HEALTH_CHECK: ${{ (inputs.skip_pr_test_health_check == true || inputs.run_all_tests == true) && 'true' || 'false' }}
FORCE_REBUILD_DEEPEP: '1'
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
USE_VENV: false
permissions:
actions: write
contents: read
issues: read
pull-requests: read
jobs:
# =============================================== check changes ====================================================
# The actual `run-ci` + `run-ci-extra` label gate now lives in the
# `call-gate` job below, which calls pr-gate.yml. pr-gate.yml live-fetches
# the PR's current labels at runtime, so a rerun (e.g. via
# /tag-and-rerun-ci extra, which adds labels via GITHUB_TOKEN — these
# don't cascade-trigger labeled events) picks up the new label set and
# passes. The previous workflow-level `if` checked the frozen event
# payload, which never updates on rerun and made the slash-add-label flow
# impossible to recover.
#
# We only keep one filter here: for `labeled` events, only proceed if the
# just-added label is one of the gating labels. This prevents every
# unrelated label addition from dispatching a full CI run.
check-changes:
if: |
github.event_name != 'pull_request' ||
github.event.action != 'labeled' ||
github.event.label.name == 'run-ci' ||
github.event.label.name == 'run-ci-extra'
uses: ./.github/workflows/_pr-test-check-changes.yml
with:
git_ref: ${{ inputs.git_ref || '' }}
run_all_tests: ${{ inputs.run_all_tests == true }}
force_continue_on_error: ${{ inputs.force_continue_on_error == true }}
pr_test_yml: '.github/workflows/pr-test-extra.yml'
secrets: inherit
# =============================================== PR Gate ====================================================
# Runtime live-fetch gate. Mirrors pr-test.yml's call-gate, but additionally
# requires `run-ci-extra`. Failure here cascades down to every test job
# via `needs`, so the workflow ends with one red gate job (~30s) plus a
# row of skipped test jobs instead of consuming a runner.
call-gate:
needs: check-changes
if: github.event_name != 'schedule' && needs.check-changes.result == 'success'
uses: ./.github/workflows/pr-gate.yml
with:
require-run-ci: true
require-run-ci-extra: true
secrets: inherit
# =============================================== sgl-kernel ====================================================
sgl-kernel-build-wheels:
needs: [check-changes, call-gate]
if: |
needs.check-changes.result == 'success' &&
(needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') &&
needs.check-changes.outputs.sgl_kernel == 'true'
uses: ./.github/workflows/_pr-test-sgl-kernel-build.yml
with:
runs_on: x64-kernel-build-node
job_display_name: Build Wheel
git_ref: ${{ inputs.git_ref || '' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
# =============================================== extra-a (1-/2-gpu) ===============================================
extra-a-test-1-gpu-small:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-a-test-1-gpu-small
runner_config: 1-gpu-small
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
extra-a-test-1-gpu-large:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-a-test-1-gpu-large
runner_config: 1-gpu-large
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
timeout_per_file: '1800'
secrets: inherit
extra-a-test-2-gpu-large:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-a-test-2-gpu-large
runner_config: 2-gpu-large
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
# =============================================== extra-b (4-/8-gpu) ===============================================
extra-b-test-4-gpu-h100:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-4-gpu-h100
runner_config: 4-gpu-h100
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
extra-b-test-4-gpu-b200:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-4-gpu-b200
runner_config: 4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
timeout_per_file: '1800'
secrets: inherit
extra-b-test-8-gpu-h200:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-8-gpu-h200
runner_config: 8-gpu-h200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
extra-b-test-deepep-4-gpu-h100:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-deepep-4-gpu-h100
runner_config: deepep-4-gpu-h100
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
extra-b-test-deepep-4-gpu-b200:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-deepep-4-gpu-b200
runner_config: deepep-4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
timeout_per_file: '1800'
secrets: inherit
extra-b-test-deepep-8-gpu-h200:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-deepep-8-gpu-h200
runner_config: deepep-8-gpu-h200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
# =============================================== aggregator ====================================================
# Mirrors pr-test.yml's `pr-test-finish` so notify-pr-states below only
# depends on one job rather than re-listing every stage. Fails if any
# dependent failed/cancelled.
pr-test-extra-finish:
needs:
[
check-changes,
call-gate,
sgl-kernel-build-wheels,
extra-a-test-1-gpu-small,
extra-a-test-1-gpu-large,
extra-a-test-2-gpu-large,
extra-b-test-4-gpu-h100,
extra-b-test-4-gpu-b200,
extra-b-test-8-gpu-h200,
extra-b-test-deepep-4-gpu-h100,
extra-b-test-deepep-4-gpu-b200,
extra-b-test-deepep-8-gpu-h200,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
json_needs='${{ toJson(needs) }}'
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
echo "$job: $result"
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
echo "All jobs completed successfully"
exit 0
# =============================================== notify pr-states ====================================================
# Dispatches pr-states.yml after every attempt (initial + every rerun).
# workflow_run.completed only fires on the initial completion (one
# workflow_run record across attempts), so we explicitly dispatch —
# workflow_dispatch is the only event GITHUB_TOKEN can cascade-create.
#
# Fork PRs are excluded: their pull_request GITHUB_TOKEN is forced
# read-only regardless of declared `actions: write`, so dispatch 403s.
# Fork PRs get pr-states updates via the workflow_run subscription
# there (initial completion) and pull_request_target (push / label).
notify-pr-states:
needs: [pr-test-extra-finish]
if: |
always() &&
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Dispatch pr-states refresh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh workflow run pr-states.yml \
--repo ${{ github.repository }} \
--ref main \
-f pr_number="$PR_NUMBER"
+197
View File
@@ -0,0 +1,197 @@
name: PR Test - JIT Kernel
on:
workflow_call:
inputs:
jit_kernel:
required: true
type: string
sgl_kernel:
required: true
type: string
runner_config:
required: true
type: string
runs_on_map:
required: true
type: string
git_ref:
required: false
type: string
default: ''
test_parallel_dispatch:
required: false
type: string
default: 'false'
skip_pr_test_health_check:
required: false
type: boolean
default: false
# Workflow-level env is NOT inherited from the caller in reusable workflows (verified by CI test).
# The github context (including github.event_name) IS inherited from the caller.
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
SKIP_PR_TEST_HEALTH_CHECK: ${{ inputs.skip_pr_test_health_check == true && 'true' || 'false' }}
jobs:
jit-kernel-unit-test:
# Runs whenever call-jit-kernel-tests dispatches this workflow. That caller is the
# single gate (PR jit_kernel changes, or scheduled/parallel-dispatch full runs), so
# the sub-jobs no longer re-exclude schedule/parallel-dispatch here.
runs-on: 1-gpu-h100
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
if: inputs.sgl_kernel == 'true'
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda13.0
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ inputs.sgl_kernel }} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run test
timeout-minutes: 30
run: |
cd test/
python3 run_suite.py --hw cuda --suite base-b-kernel-unit-test-1-gpu-large
jit-kernel-multigpu-unit-test:
# Runs whenever call-jit-kernel-tests dispatches this workflow. That caller is the
# single gate (PR jit_kernel changes, or scheduled/parallel-dispatch full runs), so
# the sub-jobs no longer re-exclude schedule/parallel-dispatch here.
runs-on: 8-gpu-h200
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-maintenance
- name: Cleanup
if: inputs.sgl_kernel == 'true'
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda13.0
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ inputs.sgl_kernel }} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run multi-GPU test
timeout-minutes: 45
run: |
cd test/
python3 run_suite.py --hw cuda --suite base-b-kernel-unit-test-8-gpu-h200
jit-kernel-benchmark-test:
# Runs whenever call-jit-kernel-tests dispatches this workflow. That caller is the
# single gate (PR jit_kernel changes, or scheduled/parallel-dispatch full runs), so
# the sub-jobs no longer re-exclude schedule/parallel-dispatch here.
runs-on: 1-gpu-h100
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
if: inputs.sgl_kernel == 'true'
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda13.0
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ inputs.sgl_kernel }} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run benchmark tests
timeout-minutes: 45
run: |
cd test/
python3 run_suite.py --hw cuda --suite base-b-kernel-benchmark-test-1-gpu-large
jit-kernel-b200-test:
# Runs whenever call-jit-kernel-tests dispatches this workflow. That caller is the
# single gate (PR jit_kernel changes, or scheduled/parallel-dispatch full runs), so
# the sub-jobs no longer re-exclude schedule/parallel-dispatch here.
runs-on: ${{ fromJson(inputs.runs_on_map)[inputs.runner_config] }}
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
if: inputs.sgl_kernel == 'true'
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda13.0
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ inputs.sgl_kernel }} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run B200 diffusion test
timeout-minutes: 30
run: |
cd test/
python3 run_suite.py --hw cuda --suite base-b-kernel-unit-test-4-gpu-b200
+184
View File
@@ -0,0 +1,184 @@
name: PR Test (MLX)
on:
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
# target_stage is the job id (e.g. stage-b-e2e-test-mlx), distinct from
# the run_suite.py suite name it dispatches (e.g. stage-b-e2e-mlx).
target_stage:
description: "Select a stage to run from dropdown (leave empty for auto-detect)"
required: false
type: choice
default: ''
options:
- ''
- stage-a-unit-test-mlx
- stage-b-e2e-test-mlx
concurrency:
group: pr-test-mlx-${{ github.ref }}
cancel-in-progress: true
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
changes_exist: ${{ steps.filter.outputs.main_package == 'true'
|| steps.filter.outputs.sgl_kernel == 'true'
|| inputs.target_stage != '' }}
main_package: ${{ steps.filter.outputs.main_package }}
sgl_kernel: ${{ steps.filter.outputs.sgl_kernel }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
with:
# Ignored on pull_request; workflow_dispatch has no PR base without it.
base: main
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_other.toml"
- "test/**/!(*.md)"
- ".github/workflows/pr-test-mlx.yml"
sgl_kernel:
- "sgl-kernel/**/!(*.md|THIRDPARTYNOTICES.txt|LICENSE)"
- ".github/workflows/pr-test-mlx.yml"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.changes_exist == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
stage-a-unit-test-mlx:
needs: [check-changes, pr-gate]
# !cancelled() lets dispatch past a skipped pr-gate; the pull_request
# path still requires pr-gate success, keeping the run-ci gate intact.
if: |
!cancelled() && (
(needs.pr-gate.result == 'success' &&
(needs.check-changes.outputs.main_package == 'true' ||
needs.check-changes.outputs.sgl_kernel == 'true')) ||
(github.event_name == 'workflow_dispatch' &&
inputs.target_stage == 'stage-a-unit-test-mlx')
)
runs-on: macos-26
timeout-minutes: 60
env:
SGLANG_IS_IN_CI: true
# use_mlx() needs this; without it the profiler takes the MPS branch.
SGLANG_USE_MLX: 1
# Forbid HF downloads to enforce the model-free guarantee.
HF_HUB_OFFLINE: 1
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: 'python/pyproject_other.toml'
- name: Verify Apple Silicon runner
run: |
echo "uname -m: $(uname -m)"
python3 -c "import platform; assert platform.machine()=='arm64', platform.machine(); print('machine:', platform.machine(), 'system:', platform.system())"
- name: Install dependencies (MLX / srt_mps extra)
timeout-minutes: 30
run: |
test -f python/pyproject_other.toml || { echo "alt pyproject_other.toml missing"; exit 1; }
# Swap in the Apple Silicon pyproject; srt_mps skips the all_mps diffusion chain.
rm -f python/pyproject.toml
mv python/pyproject_other.toml python/pyproject.toml
uv venv
uv pip install -e "python[srt_mps,test]"
- name: Report MLX / torch versions
run: |
uv run python -c "import mlx.core as mx; print('mlx', mx.__version__)"
uv run python -c "import torch; print('torch', torch.__version__)"
- name: Run model-free MLX unit tests
timeout-minutes: 15
run: |
uv run python test/run_suite.py --hw mlx --suite stage-a-unit-test-mlx
stage-b-e2e-test-mlx:
needs: [check-changes, pr-gate]
# Manual-only: no self hosted Apple Silicon runner is generally available; hosted runners are too small for e2e.
if: |
!cancelled() && inputs.target_stage == 'stage-b-e2e-test-mlx'
runs-on: [self-hosted, macOS, ARM64]
timeout-minutes: 120
env:
SGLANG_IS_IN_CI: true
SGLANG_USE_MLX: 1
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: 'python/pyproject_other.toml'
- name: Verify Apple Silicon runner
run: |
echo "uname -m: $(uname -m)"
python3 -c "import platform; assert platform.machine()=='arm64', platform.machine(); print('machine:', platform.machine(), 'system:', platform.system())"
- name: Install dependencies (MLX / srt_mps extra)
timeout-minutes: 30
run: |
test -f python/pyproject_other.toml || { echo "alt pyproject_other.toml missing"; exit 1; }
rm -f python/pyproject.toml
mv python/pyproject_other.toml python/pyproject.toml
uv venv --python 3.11
uv pip install -e "python[srt_mps,test]"
# Needs a warm HF cache or network and roughly 24 GB of unified memory.
- name: Run MLX e2e smoke tests
timeout-minutes: 90
run: |
uv run python test/run_suite.py --hw mlx --suite stage-b-e2e-mlx
pr-test-mlx-finish:
needs:
[
pr-gate,
check-changes,
stage-a-unit-test-mlx,
stage-b-e2e-test-mlx,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
json_needs='${{ toJson(needs) }}'
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
echo "$job: $result"
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
echo "All jobs completed successfully"
@@ -0,0 +1,505 @@
name: PR Test - Multimodal Gen
on:
workflow_call:
inputs:
multimodal_gen:
required: true
type: string
sgl_kernel:
required: true
type: string
runner_config:
required: true
type: string
runs_on_map:
required: true
type: string
continue_on_error:
required: false
type: string
default: 'false'
git_ref:
required: false
type: string
default: ''
test_parallel_dispatch:
required: false
type: string
default: 'false'
caller_needs_failure:
required: false
type: string
default: 'false'
skip_pr_test_health_check:
required: false
type: string
default: 'false'
# Workflow-level env is NOT inherited from the caller in reusable workflows.
# The github context (including github.event_name) IS inherited from the caller.
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
SKIP_PR_TEST_HEALTH_CHECK: ${{ inputs.skip_pr_test_health_check == 'true' }}
jobs:
compute-diffusion-partitions:
if: inputs.multimodal_gen == 'true'
runs-on: ubuntu-latest
outputs:
matrix-1gpu: ${{ steps.compute.outputs.matrix-1gpu }}
matrix-2gpu: ${{ steps.compute.outputs.matrix-2gpu }}
partition-count-1gpu: ${{ steps.compute.outputs['partition-count-1gpu'] }}
partition-count-2gpu: ${{ steps.compute.outputs['partition-count-2gpu'] }}
plan-1gpu: ${{ steps.compute.outputs.plan-1gpu }}
plan-2gpu: ${{ steps.compute.outputs.plan-2gpu }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Compute partitions
id: compute
run: |
python scripts/ci/utils/diffusion/compute_diffusion_partitions.py --min-time 1200 --target-time 1800 --max-time 2400 --max-partitions 10
multimodal-gen-test-1-gpu:
needs: compute-diffusion-partitions
if: |
always() &&
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-1gpu != '{"include":[]}' &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 1-gpu-h100
timeout-minutes: 240
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-1gpu) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion server tests
timeout-minutes: 240
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-1gpu }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite 1-gpu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-1gpu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
$CONTINUE_ON_ERROR_FLAG
- name: Upload execution report
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-report-1gpu-${{ matrix.part }}
path: python/sglang/multimodal_gen/test/execution_report_*.json
retention-days: 1
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-1gpu-${{ matrix.part }}-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.part }}
multimodal-gen-test-1-5090:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 1-gpu-5090
timeout-minutes: 180
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run 5090 diffusion canary tests
timeout-minutes: 120
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite 1-gpu-5090 \
$CONTINUE_ON_ERROR_FLAG
- name: Upload execution report
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-5090-report-${{ github.run_attempt }}
path: python/sglang/multimodal_gen/test/execution_report_*.json
retention-days: 7
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-5090-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: 1-gpu-5090
bcg-diffusion:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 1-gpu-h100
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run BCG diffusion tests
timeout-minutes: 60
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-bcg-artifacts
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite bcg-diffusion \
$CONTINUE_ON_ERROR_FLAG
- name: Upload BCG diffusion artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-bcg-artifacts-${{ github.run_attempt }}
path: diffusion-bcg-artifacts/
if-no-files-found: ignore
retention-days: 7
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: bcg-diffusion
multimodal-gen-test-2-gpu:
needs: compute-diffusion-partitions
if: |
always() &&
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-2gpu != '{"include":[]}' &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 2-gpu-h100
timeout-minutes: 240
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-2gpu) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion server tests
timeout-minutes: 240
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-2gpu }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite 2-gpu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-2gpu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
$CONTINUE_ON_ERROR_FLAG
- name: Upload execution report
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-report-2gpu-${{ matrix.part }}
path: python/sglang/multimodal_gen/test/execution_report_*.json
retention-days: 1
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-2gpu-${{ matrix.part }}-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.part }}
multimodal-gen-component-accuracy:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 2-gpu-h100
timeout-minutes: 240
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion component accuracy tests
timeout-minutes: 240
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite component-accuracy \
$CONTINUE_ON_ERROR_FLAG
- uses: ./.github/actions/upload-cuda-coredumps
if: always()
with:
artifact-suffix: component-accuracy
multimodal-gen-test-1-b200:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: ${{ fromJson(inputs.runs_on_map)[inputs.runner_config] }}
timeout-minutes: 240
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
env:
SGLANG_CI_EARLY_LD_LIBRARY_PATH: "1"
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion server tests
timeout-minutes: 240
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite 1-gpu-b200 \
$CONTINUE_ON_ERROR_FLAG
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-${{ github.job }}-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
multimodal-gen-unit-test:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 1-gpu-h100
timeout-minutes: 120
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion unit tests
timeout-minutes: 60
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py --suite unit
diffusion-coverage-check:
needs: [multimodal-gen-test-1-gpu, multimodal-gen-test-2-gpu]
if: |
always() &&
inputs.multimodal_gen == 'true' &&
(
needs.multimodal-gen-test-1-gpu.result == 'success' ||
needs.multimodal-gen-test-1-gpu.result == 'failure' ||
needs.multimodal-gen-test-2-gpu.result == 'success' ||
needs.multimodal-gen-test-2-gpu.result == 'failure'
)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Download all execution reports
uses: actions/download-artifact@v4
with:
path: reports/
pattern: diffusion-report-*
merge-multiple: true
- name: Verify coverage
run: |
python scripts/ci/utils/diffusion/verify_diffusion_coverage.py --reports-dir reports/
+264
View File
@@ -0,0 +1,264 @@
name: PR Test (MUSA)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
input:
target_stage:
description: "Specific test stage to run (Optional)"
required: false
type: string
default: ""
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-musa-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
changes_exist: ${{ steps.filter.outputs.main_package == 'true'
|| steps.filter.outputs.multimodal_gen == 'true'
|| steps.filter.outputs.sgl_kernel == 'true'
|| steps.run-mode.outputs.run_all_tests == 'true'
|| inputs.target_stage != '' }}
main_package: ${{ steps.filter.outputs.main_package == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
multimodal_gen: ${{ steps.filter.outputs.multimodal_gen == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
sgl_kernel: ${{ steps.filter.outputs.sgl_kernel == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
# Run all tests for workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.ref
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- ".github/workflows/pr-test-musa.yml"
- "python/pyproject_other.toml"
- "python/sglang/!(multimodal_gen)/**"
- "python/sglang/srt/hardware_backend/musa/**"
- "scripts/ci/musa/*"
- "scripts/ci/utils/*"
multimodal_gen:
- "python/pyproject_other.toml"
- "python/sglang/multimodal_gen/runtime/platforms/musa.py"
- "python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_rmsnorm.py"
- "python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_silu_and_mul.py"
- "python/sglang/multimodal_gen/test/server/musa/run_suite.py"
- "python/sglang/multimodal_gen/test/server/musa/**"
sgl_kernel:
- ".github/workflows/pr-test-musa.yml"
- "sgl-kernel/csrc/musa/**"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.changes_exist == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
# ==================== Multimodal Gen Tests ==================== #
multimodal-gen-test-1-gpu-musa:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.multimodal_gen == 'true' || inputs.target_stage == 'multimodal-gen-test-1-gpu-musa'
strategy:
fail-fast: false
matrix:
part: [0, 1]
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Checkout code
timeout-minutes: 10
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run diffusion server tests (1-GPU)
timeout-minutes: 30
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd python
python3 sglang/multimodal_gen/test/server/musa/run_suite.py \
--suite 1-gpu-musa \
--partition-id ${{ matrix.part }} \
--total-partitions 2
multimodal-gen-test-2-gpu-musa:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.multimodal_gen == 'true' || inputs.target_stage == 'multimodal-gen-test-2-gpu-musa'
runs-on: s5000-2-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Checkout code
timeout-minutes: 10
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run diffusion server tests (2-GPU)
timeout-minutes: 30
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd python
python3 sglang/multimodal_gen/test/server/musa/run_suite.py \
--suite 2-gpu-musa
multimodal-gen-layer-unit-test-musa:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.multimodal_gen == 'true' || inputs.target_stage == 'multimodal-gen-layer-unit-test-musa'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Checkout code
timeout-minutes: 10
uses: actions/checkout@v4
- name: Install dependencies
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run multimodal gen layer unit test
timeout-minutes: 30
run: |
pytest python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_rmsnorm.py
pytest python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_silu_and_mul.py
# =============================================== sgl-kernel ====================================================
sgl-kernel-unit-test-musa:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.sgl_kernel == 'true' || inputs.target_stage == 'sgl-kernel-unit-test-musa'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
TORCHADA_ENABLE_CPP_OPS: 1
HF_HUB_CACHE: /hf-cache/hub
steps:
- name: Checkout code
timeout-minutes: 10
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run sgl-kernel test
timeout-minutes: 20
run: |
pytest sgl-kernel/tests/test_dsv3_router_gemm.py
pytest sgl-kernel/tests/test_per_token_quant_fp8.py
pytest sgl-kernel/tests/speculative/test_eagle_utils.py
pytest sgl-kernel/tests/speculative/test_ngram_utils.py
pytest sgl-kernel/tests/speculative/test_speculative_sampling.py
pytest sgl-kernel/tests/test_torch_defaults_reset.py
pr-test-musa-finish:
needs:
[
pr-gate,
check-changes,
multimodal-gen-test-1-gpu-musa,
multimodal-gen-test-2-gpu-musa,
multimodal-gen-layer-unit-test-musa,
sgl-kernel-unit-test-musa,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
# Convert the 'needs' context to a JSON string
json_needs='${{ toJson(needs) }}'
# Get a list of all job names from the JSON keys
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
# For each job, extract its result
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
# Print the job name and its result
echo "$job: $result"
# Check for failure or cancellation and exit if found
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
# If the loop completes, all jobs were successful
echo "All jobs completed successfully"
exit
+478
View File
@@ -0,0 +1,478 @@
name: PR Test (NPU)
on:
push:
branches: [ main ]
pull_request:
workflow_dispatch:
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-npu-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
changes_exist: ${{ steps.filter.outputs.main_package == 'true' || steps.filter.outputs.multimodal_gen == 'true' || steps.run-mode.outputs.run_all_tests == 'true'}}
main_package: ${{ steps.filter.outputs.main_package == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
multimodal_gen: ${{ steps.filter.outputs.multimodal_gen == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
# Run all tests for workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.ref
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_npu.toml"
- "scripts/ci/npu/npu_ci_install_dependency.sh"
- "test/registered/ascend/**"
- ".github/workflows/pr-test-npu.yml"
multimodal_gen:
- "python/sglang/multimodal_gen/**/!(*.md|*.ipynb)"
- "python/sglang/jit_kernel/diffusion/triton/npu_fallback.py"
- "python/sglang/srt/**"
- "python/pyproject_npu.toml"
- "scripts/ci/npu/npu_ci_install_dependency.sh"
- ".github/workflows/pr-test-npu.yml"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.changes_exist == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
set-image-config:
runs-on: ubuntu-latest
outputs:
CANN_image_a3: ${{ steps.set-vars.outputs.CANN_image_a3 }}
CANN_image_910b: ${{ steps.set-vars.outputs.CANN_image_910b }}
steps:
# When triggered by PR, no inputs parameters are used. The latest community code is tested by default.
- name: Set image config
id: set-vars
run: |
echo "CANN_image_a3=swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-a3-ubuntu22.04-py3.11" >> $GITHUB_OUTPUT
echo "CANN_image_910b=swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-910b-ubuntu22.04-py3.11" >> $GITHUB_OUTPUT
stage-b-test-1-npu-a2:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: linux-aarch64-a2-1
strategy:
fail-fast: false
matrix:
part: [ 0, 1 ]
container:
image: ${{ needs.set-image-config.outputs.CANN_image_910b }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh 910b
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd test
python3 run_suite.py --hw npu --suite stage-b-test-1-npu-a2 --auto-partition-id ${{ matrix.part }} --auto-partition-size 2
stage-b-test-2-npu-a2:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: linux-aarch64-a2-2
strategy:
fail-fast: true
matrix:
part: [0, 1]
container:
image: ${{ needs.set-image-config.outputs.CANN_image_910b }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh 910b
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd test
python3 run_suite.py --hw npu --suite stage-b-test-2-npu-a2 --auto-partition-id ${{ matrix.part }} --auto-partition-size 2
stage-b-test-4-npu-a3:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: linux-aarch64-a3-4
container:
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd test
python3 run_suite.py --hw npu --suite stage-b-test-4-npu-a3 --timeout-per-file 3600
stage-b-test-16-npu-a3:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: linux-aarch64-a3-16
container:
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd test
python3 run_suite.py --hw npu --suite stage-b-test-16-npu-a3 --timeout-per-file 3600
multimodal-gen-test-1-npu-a3:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.multimodal_gen == 'true'
runs-on: linux-aarch64-a3-2
container:
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py --suite 1-npu
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-npu-1-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
multimodal-gen-test-2-npu-a3:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.multimodal_gen == 'true'
runs-on: linux-aarch64-a3-16
container:
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py --suite 2-npu
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-npu-2-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
pr-single-node-tests:
name: single-node-poc
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
strategy:
fail-fast: false
max-parallel: 6
matrix:
test_config:
# qwen3_6_27b performance tests
- name: qwen3_6_27b_w8a8_1p_in64k_out1k_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py
test_type: 'perf'
uses: ./.github/workflows/nightly-test-npu-e2e-single-node.yml
with:
runner: ${{ matrix.test_config.runner }}
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
install_sglang_from_source: false
install_sglang_deps: true
device_type_for_deps: 'a3'
transformers_version: ''
pr-test-npu-finish:
needs:
[
check-changes,
stage-b-test-1-npu-a2,
stage-b-test-2-npu-a2,
stage-b-test-4-npu-a3,
stage-b-test-16-npu-a3,
multimodal-gen-test-1-npu-a3,
multimodal-gen-test-2-npu-a3,
pr-single-node-tests,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
# Convert the 'needs' context to a JSON string
json_needs='${{ toJson(needs) }}'
# Get a list of all job names from the JSON keys
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
# For each job, extract its result
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
# Print the job name and its result
echo "$job: $result"
# Check for failure or cancellation and exit if found
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
# If the loop completes, all jobs were successful
echo "All jobs completed successfully"
exit 0
+456
View File
@@ -0,0 +1,456 @@
name: PR Test (SMG)
on:
push:
branches: [ main ]
paths:
- "sgl-model-gateway/**"
- ".github/workflows/pr-test-rust.yml"
- "scripts/ci/cuda/ci_install_dependency.sh"
- "scripts/ci/cuda/ci_install_gateway_dependencies.sh"
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened, labeled]
paths:
- "sgl-model-gateway/**"
- ".github/workflows/pr-test-rust.yml"
- "scripts/ci/cuda/ci_install_dependency.sh"
- "scripts/ci/cuda/ci_install_gateway_dependencies.sh"
workflow_dispatch:
concurrency:
group: gateway-tests-${{ github.ref }}
cancel-in-progress: true
env:
SGLANG_IS_IN_CI: true
jobs:
build-wheel:
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
# Pin to 22.04 so the wheel auditwheel-tags as manylinux_2_35; the
# self-hosted GPU runners are Ubuntu 22.04 (glibc 2.35) and reject
# manylinux_2_39 wheels produced on ubuntu-latest (Ubuntu 24.04).
runs-on: ubuntu-22.04
# sccache is only installed on the GitHub-hosted runners that run this
# job and `unit-tests`; setting RUSTC_WRAPPER workflow-wide leaks it to
# gateway-e2e on the self-hosted GPU runners (which don't have sccache),
# so any pip-install that compiles a Rust extension would fail with
# `could not execute process \`sccache rustc\``.
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install rust dependencies
run: |
bash scripts/ci/cuda/ci_install_gateway_dependencies.sh
- name: Configure sccache
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: sgl-model-gateway
shared-key: "rust-cache"
cache-all-crates: true
cache-on-failure: true
save-if: true
- name: Build python binding
run: |
source "$HOME/.cargo/env"
export RUSTC_WRAPPER=sccache
cd sgl-model-gateway/bindings/python
python3 -m pip install --upgrade pip maturin
maturin build --profile ci --features vendored-openssl --out dist
- name: List built wheel
run: ls -lh sgl-model-gateway/bindings/python/dist/
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: smg-wheel
path: sgl-model-gateway/bindings/python/dist/*.whl
retention-days: 1
- name: Test wheel install
run: |
pip install sgl-model-gateway/bindings/python/dist/*.whl
python3 -c "import sglang_router; print('Python package: OK')"
python3 -c "from sglang_router.sglang_router_rs import Router; print('Rust extension: OK')"
python3 -m sglang_router.launch_router --help > /dev/null && echo "Entry point: OK"
python-unit-tests:
needs: build-wheel
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: smg-wheel
path: dist/
- name: Install wheel
run: pip install dist/*.whl
- name: Run Python unit tests
run: |
cd bindings/python
python3 -m pip install pytest pytest-cov pytest-xdist
pytest -q tests --cov=sglang_router --cov-config=.coveragerc --cov-report=term-missing --cov-fail-under=80
unit-tests:
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-latest
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_gateway_dependencies.sh
- name: Configure sccache
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: sgl-model-gateway
shared-key: "rust-cache"
cache-all-crates: true
cache-on-failure: true
save-if: true
- name: Run lint
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
rustup component add clippy
cargo clippy --all-targets --all-features -- -D warnings
- name: Run fmt
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt
rustup toolchain install nightly --profile minimal
cargo +nightly fmt -- --check
- name: Generate vision golden fixtures
run: |
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
pip install transformers pillow numpy scipy
pip install transformers pillow numpy
cd sgl-model-gateway/
python scripts/generate_vision_golden.py
- name: Run Rust tests
timeout-minutes: 20
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
cargo test
- name: Show sccache stats
if: always()
run: sccache --show-stats
gateway-e2e:
name: ${{ matrix.name }}
needs: build-wheel
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
# The `responses` matrix entry is intentionally omitted. It needs
# `docker run gvenzl/oracle-xe` + `docker run shoofio/brave-search-mcp-sse`
# on the runner host, but the 2-/4-gpu-h100 runners are themselves
# containers without a Docker daemon. Re-enable by adding back:
# - name: responses
# runner: 2-gpu-h100
# timeout: 45
# test_dirs: "e2e_test/responses"
# extra_deps: ""
# env_vars: "SHOW_WORKER_LOGS=0 SHOW_ROUTER_LOGS=1"
# reruns: "--reruns 2 --reruns-delay 5"
# setup_oracle: true
# setup_brave: true
# parallel_opts: ""
# plus the Oracle Instant Client / `gvenzl/oracle-xe` /
# `shoofio/brave-search-mcp-sse` setup + cleanup steps (see commit
# cf346bb15 for the exact step bodies) once a runner with
# `docker.sock` (or binary-installed deps) is available.
strategy:
fail-fast: false
matrix:
include:
- name: benchmarks
# 4 GPUs: test_pd_perf.py uses workers(prefill=2, decode=2) and
# test_regular_perf.py uses workers(count=4) — both need tp*workers=4.
runner: 4-gpu-h100
timeout: 32
test_dirs: "e2e_test/benchmarks"
extra_deps: "genai-bench==0.0.3"
env_vars: ""
reruns: ""
upload_benchmarks: true
parallel_opts: "" # No parallel for benchmarks (performance measurement)
- name: e2e
runner: 2-gpu-h100
timeout: 45
test_dirs: "e2e_test/router e2e_test/embeddings"
extra_deps: ""
env_vars: "SHOW_WORKER_LOGS=0 SHOW_ROUTER_LOGS=1"
reruns: "--reruns 2 --reruns-delay 5"
# Run tests serially. pytest-parallel (unmaintained since 2019)
# has buggy fixture-finalize handling under thread dispatch:
# both class- and function-scoped fixture references leaked
# between tests, leaving model_pool instances pinned at
# _ref_count > 0 and deadlocking later tests that needed
# eviction (50+ min hangs). On a 2-GPU runner with 5 distinct
# model:mode combos in router+embeddings, the suite is
# eviction-bound anyway, so the parallel speedup was illusory.
parallel_opts: ""
- name: chat-completions
runner: 2-gpu-h100
timeout: 45
test_dirs: "e2e_test/chat_completions"
extra_deps: ""
env_vars: "SHOW_WORKER_LOGS=0 SHOW_ROUTER_LOGS=1"
reruns: "--reruns 2 --reruns-delay 5"
parallel_opts: ""
- name: chat-completions-4gpu
runner: 4-gpu-h100
timeout: 45
# qwen-30b (tp=4) tests can't fit on the 2-gpu-h100 matrix entries —
# they get skipped there by hooks.py. Run them here so coverage holds.
test_dirs: "e2e_test/chat_completions/test_enable_thinking.py"
extra_deps: ""
env_vars: "SHOW_WORKER_LOGS=0 SHOW_ROUTER_LOGS=1"
reruns: "--reruns 2 --reruns-delay 5"
parallel_opts: ""
runs-on: ${{ matrix.runner }}
timeout-minutes: ${{ matrix.timeout }}
# Self-hosted GPU runners are scarce; serialize per hardware type so
# 2-gpu-h100 and 4-gpu-h100 each run one job at a time across all
# in-flight PRs. Queue rather than cancel — different refs shouldn't
# interrupt each other. Include matrix.name in the group so siblings
# within the same run don't collide: GitHub keeps only one pending job
# per concurrency group, so matrix entries sharing a group (e.g.
# benchmarks + chat-completions-4gpu on 4-gpu-h100) would evict each
# other regardless of cancel-in-progress: false.
concurrency:
group: pr-test-rust-${{ matrix.runner }}-${{ matrix.name }}
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install SGLang dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: smg-wheel
path: wheel/
- name: Install wheel
run: |
pip uninstall -y sglang-router || true
pip install wheel/*.whl
- name: Install e2e test dependencies
run: |
python3 -m pip install pytest pytest-rerunfailures httpx openai grpcio grpcio-health-checking numpy
if [ -n "${{ matrix.extra_deps }}" ]; then
if echo "${{ matrix.extra_deps }}" | grep -q "genai-bench"; then
# genai-bench's transitive deps (oci/locust) pull
# transformers<5 which requires huggingface_hub<1.0 — that
# downgrades the 1.11.0 ci_install_dependency.sh settled on
# and breaks `kernels` (requires huggingface_hub>=1.3.0,<2.0).
# Install --no-deps and supply the runtime deps explicitly.
python3 -m pip --no-cache-dir install --no-deps ${{ matrix.extra_deps }}
python3 -m pip --no-cache-dir install \
locust click rich tenacity oci openpyxl gevent matplotlib
else
# Other extras with well-behaved transitive deps —
# normal --upgrade install path.
python3 -m pip --no-cache-dir install --upgrade ${{ matrix.extra_deps }}
fi
fi
- name: Run E2E tests
run: |
python3 python/sglang/cli/killall.py
cd sgl-model-gateway
${{ matrix.env_vars }} pytest ${{ matrix.reruns }} ${{ matrix.parallel_opts }} ${{ matrix.test_dirs }} -s -vv -o log_cli=true --log-cli-level=INFO
- name: Upload benchmark results
if: matrix.upload_benchmarks && success()
uses: actions/upload-artifact@v4
with:
name: genai-bench-results-all-policies
path: sgl-model-gateway/benchmark_**/
docker-build-test:
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image (no push)
uses: docker/build-push-action@v5
with:
context: .
file: docker/gateway.Dockerfile
push: false
tags: sgl-model-gateway:test
cache-from: type=gha
cache-to: type=gha,mode=max
k8s-integration:
# Runs SMG against a kind cluster with fake worker pods to exercise the
# K8s service discovery / reconciliation path. No GPU required (workers
# are python:3.12-slim mocks); the h100 matrix runners are unsuitable
# because they're containers without a Docker daemon.
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install kind and kubectl
run: |
curl -fsSLo /tmp/kind https://kind.sigs.k8s.io/dl/v0.24.0/kind-linux-amd64
chmod +x /tmp/kind && sudo mv /tmp/kind /usr/local/bin/kind
KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt)
curl -fsSLo /tmp/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
chmod +x /tmp/kubectl && sudo mv /tmp/kubectl /usr/local/bin/kubectl
kind --version
kubectl version --client
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build smg-gateway:test image
uses: docker/build-push-action@v5
with:
context: sgl-model-gateway
file: sgl-model-gateway/e2e_test/k8s_integration/Dockerfile.gateway
tags: smg-gateway:test
load: true
cache-from: type=gha,scope=k8s-integration
cache-to: type=gha,scope=k8s-integration,mode=max
- name: Install Python test dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install pytest httpx
- name: Set up kind cluster and deploy
env:
SKIP_DOCKER_BUILD: "1"
run: |
cd sgl-model-gateway
bash e2e_test/k8s_integration/setup.sh
- name: Run K8s integration tests
run: |
cd sgl-model-gateway
# confcutdir avoids loading the parent e2e_test/conftest.py, which
# pulls in heavy infra deps (requests, sglang_router) that this job
# intentionally doesn't install.
pytest e2e_test/k8s_integration/ \
--confcutdir=e2e_test/k8s_integration \
-v -s -o log_cli=true --log-cli-level=INFO
- name: Dump cluster state on failure
if: failure()
run: |
kubectl --context kind-smg-test get all -A || true
kubectl --context kind-smg-test -n smg-test describe pods || true
kubectl --context kind-smg-test -n smg-test logs deploy/smg-gateway --tail=200 || true
- name: Tear down kind cluster
if: always()
run: |
cd sgl-model-gateway
bash e2e_test/k8s_integration/setup.sh teardown || true
finish:
needs: [build-wheel, python-unit-tests, unit-tests, gateway-e2e, docker-build-test, k8s-integration]
runs-on: ubuntu-latest
steps:
- name: Finish
run: echo "This is an empty step to ensure that all jobs are completed."
summarize-benchmarks:
needs: gateway-e2e
runs-on: ubuntu-latest
if: success()
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download benchmark results
uses: actions/download-artifact@v4
with:
name: genai-bench-results-all-policies
- name: Create benchmark summary
run: python3 sgl-model-gateway/e2e_test/benchmarks/summarize.py .
+178
View File
@@ -0,0 +1,178 @@
name: PR Test - SGL Kernel
on:
workflow_call:
inputs:
sgl_kernel:
required: true
type: string
runner_config:
required: true
type: string
runs_on_map:
required: true
type: string
git_ref:
required: false
type: string
default: ''
skip_pr_test_health_check:
required: false
type: boolean
default: false
# Workflow-level env is NOT inherited from the caller in reusable workflows.
# The github context (including github.event_name) IS inherited from the caller.
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
SKIP_PR_TEST_HEALTH_CHECK: ${{ inputs.skip_pr_test_health_check == true && 'true' || 'false' }}
jobs:
sgl-kernel-unit-test:
runs-on: 1-gpu-h100
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run test
timeout-minutes: 30
run: |
cd sgl-kernel
pytest tests/
sgl-kernel-benchmark-test:
runs-on: 1-gpu-h100
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run benchmark tests
timeout-minutes: 45
run: |
cd sgl-kernel/benchmark
echo "Running sgl-kernel benchmark tests in CI mode..."
echo "CI environment variable: $CI"
echo "GITHUB_ACTIONS environment variable: $GITHUB_ACTIONS"
for bench_file in bench_*.py; do
echo "Testing $bench_file..."
timeout 60 python3 "$bench_file" || echo "Warning: $bench_file timed out or failed, continuing..."
echo "Completed $bench_file"
echo "---"
done
echo "All benchmark tests completed!"
sgl-kernel-b200-test:
runs-on: ${{ fromJson(inputs.runs_on_map)[inputs.runner_config] }}
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run sgl-kernel unit tests on B200
timeout-minutes: 30
run: |
cd sgl-kernel
pytest tests/
# Adding a single CUDA13 build-and-run check for the kernel
# TODO: Add back this test when it can pass on CI
# cuda13-kernel-build-check:
# if: inputs.sgl_kernel == 'true'
# runs-on: x64-cu13-kernel-tests
# steps:
# - uses: actions/checkout@v4
# - name: Cleanup
# run: |
# ls -alh sgl-kernel/dist || true
# rm -rf sgl-kernel/dist/* || true
# - name: Download CUDA 13.0 artifacts
# uses: actions/download-artifact@v4
# with:
# path: sgl-kernel/dist/
# merge-multiple: true
# pattern: wheel-python3.10-cuda*
# - name: Install dependencies
# run: |
# CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh
# - name: Run kernel unit tests
# timeout-minutes: 30
# run: |
# cd sgl-kernel
# pytest tests/
+434
View File
@@ -0,0 +1,434 @@
name: PR Test (sgl-router)
# Trigger contract — modeled on `pr-test.yml`:
#
# * No `paths:` filter at the trigger level. The workflow ALWAYS
# fires on every PR synchronize / push, so the run record always
# appears as a check on the PR (no more "workflow silently didn't
# fire" mystery debugging). Path-based skip decisions are made
# inside the `sgl-router-gate` job below, where we can log the
# reason. Same for the `run-ci` label gate — moved off the
# workflow trigger and into the gate job so its outcome is
# observable.
#
# * `push` on `main` keeps firing so post-merge runs still record
# against the default branch.
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
concurrency:
group: sgl-router-${{ github.event_name }}-${{ github.head_ref || github.ref_name || 'default' }}
cancel-in-progress: true
env:
SGLANG_IS_IN_CI: true
jobs:
# Stage 0 — decide whether to run the heavy tiers. Always runs
# (cheap, ubuntu-latest), and emits a single `should_run` output
# consumed by every tier below. Reasons it might evict downstream
# work:
# * `pull_request` event without the `run-ci` label (budget gate)
# * No files matching the sgl-router paths changed in this PR
# Either outcome is logged on the gate job's page, so operators can
# see *why* the tiers were skipped instead of guessing.
sgl-router-gate:
name: gate
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.decide.outputs.should_run }}
paths_changed: ${{ steps.paths.outputs.sgl_router }}
has_run_ci_label: ${{ steps.label.outputs.has_run_ci }}
steps:
- uses: actions/checkout@v4
# `dorny/paths-filter` computes the diff against the PR base
# (for pull_request) or the push's `before` SHA (for push) and
# exposes `sgl_router=true|false` on whether any tracked path
# changed. Replaces the old workflow-level `paths:` filter so
# the gate is observable.
- name: Detect sgl-router path changes
id: paths
uses: dorny/paths-filter@v3
with:
filters: |
sgl_router:
- 'experimental/sgl-router/**'
- '.github/workflows/pr-test-sgl-router.yml'
- 'scripts/ci/cuda/ci_install_dependency.sh'
# `run-ci` label gate. Cheap workflow runs (e.g. docs-only PRs
# that happen to touch the sgl-router dir) still need the
# opt-in label before consuming the H100 / kind tiers below.
# `push` events on main and `workflow_dispatch` skip this gate.
- name: Check run-ci label
id: label
run: |
if [[ "${{ github.event_name }}" != "pull_request" ]]; then
echo "has_run_ci=true" >> "$GITHUB_OUTPUT"
echo "Non-PR event (${{ github.event_name }}); label gate bypassed."
exit 0
fi
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'run-ci') }}" == "true" ]]; then
echo "has_run_ci=true" >> "$GITHUB_OUTPUT"
echo "PR has run-ci label; downstream tiers will run."
else
echo "has_run_ci=false" >> "$GITHUB_OUTPUT"
echo "::warning::PR is missing the 'run-ci' label; skipping sgl-router tiers. Apply the label to opt in."
fi
- name: Decide
id: decide
run: |
paths='${{ steps.paths.outputs.sgl_router }}'
label='${{ steps.label.outputs.has_run_ci }}'
if [[ "$paths" == "true" && "$label" == "true" ]]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "Both path filter and run-ci label match; running tiers."
else
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "Skipping tiers (paths_changed=$paths, has_run_ci=$label)."
fi
sgl-router-lint:
name: tier-1 — lint
needs: sgl-router-gate
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: ubuntu-latest
env:
# Scoped per-job (not workflow-wide) to avoid SMG's documented breakage
# of self-hosted-runner pip installs that compile Rust extensions.
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
# Supply-chain guard: deny.toml allowlists the dynamo git source but
# cannot pin a SHA. Bumps to this constant must be a deliberate PR.
DYNAMO_TOKENIZERS_EXPECTED_REV: "1efdd4dcb901caeae636131321094090d252c8d6"
steps:
- uses: actions/checkout@v4
- name: Verify dynamo-* SHA pin
run: |
set -euo pipefail
revs=$(grep -E '^dynamo-[a-z]+[[:space:]]*=.*\brev[[:space:]]*=' experimental/sgl-router/Cargo.toml \
| sed -E 's/.*rev[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/')
if [ -z "${revs:-}" ]; then
echo "::error::no dynamo-* git deps with rev= found in experimental/sgl-router/Cargo.toml"
exit 1
fi
count=0
while IFS= read -r r; do
count=$((count + 1))
if [ "$r" != "$DYNAMO_TOKENIZERS_EXPECTED_REV" ]; then
echo "::error::dynamo-* SHA pin mismatch in experimental/sgl-router/Cargo.toml"
echo " expected: $DYNAMO_TOKENIZERS_EXPECTED_REV"
echo " actual: $r"
echo "All revs found:"
echo "$revs" | sed 's/^/ /'
exit 1
fi
done <<< "$revs"
echo "dynamo-* SHA pin OK: $count entries all at $DYNAMO_TOKENIZERS_EXPECTED_REV"
# sccache install is best-effort: the GitHub release CDN intermittently
# returns 5xx and the action's built-in retry is shallow. If it fails,
# we unset RUSTC_WRAPPER below and proceed without compile caching —
# rust-cache still covers the bulk of warm-cache wins.
- name: Configure sccache
id: sccache
continue-on-error: true
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Disable sccache if install failed
if: steps.sccache.outcome != 'success'
run: |
echo "::warning::sccache install failed (${{ steps.sccache.outcome }}); proceeding without compile caching"
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: experimental/sgl-router
shared-key: "sgl-router-cache"
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo check
working-directory: experimental/sgl-router
run: cargo check --all-targets
- name: cargo clippy
working-directory: experimental/sgl-router
run: cargo clippy --all-targets -- -D warnings
- name: cargo fmt
working-directory: experimental/sgl-router
run: |
rustup toolchain install nightly --profile minimal --component rustfmt
cargo +nightly fmt -- --check
# cargo-deny 0.19.6+ required for CVSS 4.0 parsing.
- name: Install cargo-deny
run: cargo install --locked cargo-deny@0.19.6
- name: cargo deny check
working-directory: experimental/sgl-router
run: cargo deny check licenses bans sources advisories
- name: Show sccache stats
if: always() && steps.sccache.outcome == 'success'
run: sccache --show-stats
sgl-router-build-and-test:
name: tier-2 — build + test
needs: [sgl-router-gate, sgl-router-lint]
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: ubuntu-latest
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install OS deps (libssl, pkg-config)
run: |
sudo apt-get update
sudo apt-get install -y libssl-dev pkg-config
# sccache install is best-effort: the GitHub release CDN intermittently
# returns 5xx and the action's built-in retry is shallow. If it fails,
# we unset RUSTC_WRAPPER below and proceed without compile caching —
# rust-cache still covers the bulk of warm-cache wins.
- name: Configure sccache
id: sccache
continue-on-error: true
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Disable sccache if install failed
if: steps.sccache.outcome != 'success'
run: |
echo "::warning::sccache install failed (${{ steps.sccache.outcome }}); proceeding without compile caching"
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: experimental/sgl-router
shared-key: "sgl-router-cache"
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo build (record wall time)
working-directory: experimental/sgl-router
run: |
START=$(date +%s)
cargo build --release --all-targets
END=$(date +%s)
BUILD_SECONDS=$((END - START))
echo "tier_2_build_duration_seconds=${BUILD_SECONDS}" >> "$GITHUB_ENV"
echo "::notice title=Tier-2 build duration::${BUILD_SECONDS}s"
- name: cargo test
working-directory: experimental/sgl-router
# Skip the tokenizer parity_matrix test here: ubuntu-latest has
# no HuggingFace cache, so the matrix would hard-fail (see the
# test docstring). The e2e job runs it after pytest populates
# the Qwen3-0.6B tokenizer.json. Filter is matched against the
# full test path `tokenizer::parity::parity_matrix`; substring
# `parity_matrix` is unique to that test.
run: cargo test --release -- --skip parity_matrix
# Regenerate the cross-impl block-hash parity fixture and fail if it
# differs from the committed file. The Python script replicates
# SGLang's `radix_cache.hash_page` algorithm verbatim (no
# `import sglang`); a diff here means EITHER the Rust port's algorithm
# drifted from the script, OR the script drifted from SGLang's actual
# code — in both cases the operator must investigate and re-commit
# the fixture in the same PR as the algorithm change.
#
# `set -euo pipefail` makes a Python-side failure (syntax error,
# missing import) cause this step to fail instead of falling
# through to a no-diff "pass" when no fixture was actually
# regenerated.
- name: kv_events hash parity fixture (drift check)
working-directory: experimental/sgl-router
run: |
set -euo pipefail
python3 tests/scripts/generate_kv_events_hash_parity.py
if ! git diff --exit-code -- tests/fixtures/kv_events_hash_parity.json; then
echo "::error::kv_events hash parity fixture drifted." \
"Re-run experimental/sgl-router/tests/scripts/generate_kv_events_hash_parity.py" \
"and commit the updated tests/fixtures/kv_events_hash_parity.json."
exit 1
fi
- name: Show sccache stats
if: always() && steps.sccache.outcome == 'success'
run: sccache --show-stats
- name: Summary
if: always()
run: |
echo "## Tier-2 results" >> "$GITHUB_STEP_SUMMARY"
echo "- Build duration: \`${tier_2_build_duration_seconds:-(not recorded)}\` s" >> "$GITHUB_STEP_SUMMARY"
sgl-router-docker-build-test:
name: tier-3 — docker (placeholder)
needs: [sgl-router-gate, sgl-router-build-and-test]
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Placeholder
run: |
echo "docker-build-test not implemented yet."
exit 0
# tier-3 k8s integration is decoupled from tier-3 e2e: each runs on a
# different runner type (ubuntu kind cluster vs H100), each manages its
# own test scope (k8s_integration/ vs everything-else-in-e2e), and
# each is allowed to fail without blocking the other. They both still
# gate on tier-2's build+test passing — a Rust compile failure blocks
# the GPU/cluster runners from spinning up.
sgl-router-k8s-integration:
name: tier-3 — k8s integration
needs: [sgl-router-gate, sgl-router-build-and-test]
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Install kind
run: |
curl -Lo /tmp/kind https://kind.sigs.k8s.io/dl/v0.24.0/kind-linux-amd64
chmod +x /tmp/kind && sudo mv /tmp/kind /usr/local/bin/kind
- name: Install kubectl
run: |
curl -Lo /tmp/kubectl https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl
chmod +x /tmp/kubectl && sudo mv /tmp/kubectl /usr/local/bin/kubectl
- name: Build router image
run: docker build -t sgl-router:e2e -f experimental/sgl-router/tests/e2e/k8s_integration/Dockerfile.router .
- name: Build fake_worker image
run: |
docker build -t sgl-router-fake-worker:e2e \
-f experimental/sgl-router/tests/e2e/k8s_integration/Dockerfile.fake_worker \
experimental/sgl-router/tests/e2e/k8s_integration/
- name: Bootstrap kind + deploy
run: bash experimental/sgl-router/tests/e2e/k8s_integration/setup.sh
- name: Set up Python
run: |
python3 -m venv /tmp/e2e-venv
/tmp/e2e-venv/bin/pip install -r experimental/sgl-router/tests/e2e/k8s_integration/requirements.txt
- name: Run E2E
run: /tmp/e2e-venv/bin/pytest experimental/sgl-router/tests/e2e/k8s_integration/ -v --tb=short
- name: Dump cluster + router diagnostics on failure
if: failure()
run: |
set +e
kubectl -n sgl-router-test get pods -o wide
kubectl -n sgl-router-test describe pod -l app=sgl-router
kubectl -n sgl-router-test describe pod -l app=sglang
kubectl -n sgl-router-test logs deploy/sgl-router --tail=300
kubectl -n sgl-router-test logs deploy/sgl-router --tail=300 --previous
kubectl -n sgl-router-test get events --sort-by=.lastTimestamp
kubectl -n sgl-router-test get endpointslices -o wide
kubectl -n sgl-router-test get svc
true
sgl-router-e2e:
name: tier-3 — e2e
needs: [sgl-router-gate, sgl-router-build-and-test]
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: 2-gpu-h100
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: bash scripts/ci/utils/install_rust_protoc.sh
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: experimental/sgl-router
shared-key: "sgl-router-cache"
- name: cargo build (release)
working-directory: experimental/sgl-router
run: |
source "$HOME/.cargo/env"
cargo build --release
# Install SGLang from the local checkout in editable mode (no PyPI
# version pin) — mirrors `pr-test-rust.yml` so the router e2e runs
# against whatever SGLang version is on the branch. Replaces the
# previous `pip install sglang[srt]==X.Y.Z` pin, which forced the
# e2e to test against a stale wheel and would silently miss any
# new ServerArgs field (e.g. `disaggregation_mode` /
# `disaggregation_bootstrap_port`) added in the branch.
- name: Install SGLang dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Install e2e test dependencies
run: |
python3 -m pip install -r experimental/sgl-router/tests/e2e/requirements.txt
# IMPORTANT: --ignore=tests/e2e/k8s_integration. The k8s integration
# suite is owned by the `sgl-router-k8s-integration` job which has
# kind + kubectl installed and a real cluster running. The e2e GPU
# runner has neither, so pytest's recursive collection of
# tests/e2e/ would ERROR every k8s test at setup. Both tiers must
# be allowed to fail independently — k8s flakes (cluster boot,
# image pull) must not surface as e2e failures on H100, and e2e
# flakes (model load, HF auth) must not surface as k8s failures.
- name: Run e2e
working-directory: experimental/sgl-router
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
python3 -m pytest tests/e2e/ -v -s --tb=short \
--ignore=tests/e2e/k8s_integration
- name: Tokenizer parity matrix (uses HF cache populated by e2e)
working-directory: experimental/sgl-router
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
source "$HOME/.cargo/env"
cargo test --release --test component tokenizer::parity
sgl-router-finish:
name: finish
needs:
- sgl-router-gate
- sgl-router-lint
- sgl-router-build-and-test
- sgl-router-docker-build-test
- sgl-router-k8s-integration
- sgl-router-e2e
# `always()` lets `finish` run after upstream `skipped` outcomes
# (when the gate decided to skip). The downstream-skipped path is
# an explicit success — the workflow was correctly bypassed for a
# PR that didn't need full CI. The downstream-failure /
# downstream-cancelled path is a real fail. We also accept the
# gate's `skipped` (impossible today, kept for symmetry) and
# require the gate itself to have succeeded.
if: |
always() &&
needs.sgl-router-gate.result == 'success' &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled')
runs-on: ubuntu-latest
steps:
- name: All required checks completed
run: echo "OK"
+148
View File
@@ -0,0 +1,148 @@
name: PR Test (Xeon)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-xeon-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
main_package: ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests}}
env:
PYTHONDONTWRITEBYTECODE: 1
steps:
- name: clean up
run: |
sudo rm -rf ${{ github.workspace }} || true
sudo mkdir -p ${{ github.workspace }}
sudo chown -R $(id -u):$(id -g) ${{ github.workspace }}
- name: Checkout code
uses: actions/checkout@v4
with:
clean: false
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
# Run all tests for workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.ref
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_cpu.toml"
- "test/**/!(*.md)"
- "sgl-kernel/**/!(*.md|THIRDPARTYNOTICES.txt|LICENSE)"
- ".github/workflows/pr-test-xeon.yml"
- "docker/xeon.Dockerfile"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.main_package == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
build-test:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- runner: xeon-gnr
suite: base-b-test-cpu
partition_args: ""
steps:
- name: Cleanup workspace
run: |
sudo rm -rf ${{ github.workspace }} || true
sudo mkdir -p ${{ github.workspace }}
sudo chown $(id -u):$(id -g) ${{ github.workspace }}
- name: Checkout repository
uses: actions/checkout@v4
with:
clean: false
ref: ${{ inputs.ref || github.ref }}
- name: Build and Push
run: |
version=$(cat python/sglang/version.py | cut -d'"' -f2)
tag=v${version}-xeon
PR_REPO=${{ github.event.pull_request.head.repo.clone_url }}
PR_HEAD_REF=${{ github.head_ref }}
docker build \
${PR_REPO:+--build-arg SGLANG_REPO=$PR_REPO} \
${PR_HEAD_REF:+--build-arg VER_SGLANG=$PR_HEAD_REF} \
. -f docker/xeon.Dockerfile -t sglang_xeon --no-cache
- name: Run container
run: |
docker run -dt \
-v ${{ github.workspace }}:/sglang-checkout/ --ipc=host \
-v $HOME/.cache/huggingface:/root/.cache/huggingface \
-e HF_TOKEN="$(cat ~/huggingface_token.txt)" \
--name ci_sglang_${{ matrix.runner }} \
sglang_xeon
- name: Check AMX support
id: check_amx
timeout-minutes: 5
run: |
docker exec -w /sglang-checkout/ ci_sglang_${{ matrix.runner }} \
bash -c "source /opt/.venv/bin/activate && python3 -c 'import torch; import sgl_kernel; assert torch.cpu._is_amx_tile_supported(); assert hasattr(torch.ops.sgl_kernel, \"convert_weight_packed\"); '"
- name: Run unit tests
timeout-minutes: 120
run: |
docker exec -w /sglang-checkout/ ci_sglang_${{ matrix.runner }} \
bash -c "source /opt/.venv/bin/activate && cd ./test && python3 run_suite.py --hw cpu --suite ${{ matrix.suite }} ${{ matrix.partition_args }}"
- name: Change permission
timeout-minutes: 2
run: |
docker exec -u root ci_sglang_${{ matrix.runner }} bash -c "
rm -rf /tmp/ci-home &&
chown -R $(id -u):$(id -g) /sglang-checkout/ 2>/dev/null || true
"
- name: Cleanup container
if: always()
run: |
docker rm -f ci_sglang_${{ matrix.runner }} || true
+238
View File
@@ -0,0 +1,238 @@
name: PR Test (XPU)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-xpu-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
main_package: ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
# Run all tests for workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.ref
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_xpu.toml"
- "test/**/!(*.md)"
- "sgl-kernel/**/!(*.md|THIRDPARTYNOTICES.txt|LICENSE)"
- ".github/workflows/pr-test-xpu.yml"
- "docker/xpu.Dockerfile"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.main_package == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
# ==================== Stage A ==================== #
stage-a-test-1-gpu-xpu:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: intel-bmg
env:
DOCKERHUB_INTEL_USERNAME: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
DOCKERHUB_INTEL_TOKEN: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
steps:
- name: Reset workspace ownership
run: |
# Prior runs install sglang as root inside the CI container against
# the bind-mounted workspace, leaving root-owned egg-info on the host.
# actions/checkout's pre-clean step can't unlink those as the runner
# user, so reset ownership before checkout runs.
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.ref }}
- name: Start CI container
run: |
export HF_TOKEN="$(cat ~/huggingface_token.txt)"
bash scripts/ci/xpu/xpu_ci_start_container.sh
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install Dependency
timeout-minutes: 60
run: |
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --upgrade pip
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install pytest expecttest ray huggingface_hub tabulate
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip uninstall -y flashinfer-python sgl-kernel sglang
docker exec ci_sglang_xpu cp /sglang-checkout/python/pyproject_xpu.toml /sglang-checkout/python/pyproject.toml
docker exec -w /sglang-checkout/python ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir . --extra-index-url https://download.pytorch.org/whl/xpu
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir --no-deps xgrammar==0.1.33
docker exec ci_sglang_xpu /bin/bash -c '/opt/venv/bin/hf auth login --token ${HF_TOKEN}'
- name: Run stage-a tests
timeout-minutes: 30
run: |
docker exec ci_sglang_xpu bash -c "source /opt/venv/bin/activate && cd /sglang-checkout/test && python3 run_suite.py --hw xpu --suite stage-a-test-1-gpu-xpu"
- name: Cleanup container
if: always()
run: |
# pip install ran as root inside the container against the
# bind-mounted workspace, so build artifacts are root-owned on
# the host. Chown them back before rm to avoid Permission denied.
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
# Wipe everything the run wrote into the workspace so the next
# job starts from a clean tree.
rm -rf \
python/build \
python/dist \
python/sglang.egg-info \
python/sglang/*.egg-info \
test/result.jsonl \
test/results \
test/.pytest_cache \
.pytest_cache || true
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
find . -type f -name "*.pyc" -delete || true
docker rm -f ci_sglang_xpu || true
if [[ -n "${CI_SGLANG_XPU_IMAGE:-}" ]]; then
docker rmi -f "${CI_SGLANG_XPU_IMAGE}" || true
fi
# ==================== Wait for Stage A ==================== #
wait-for-stage-a:
needs: [stage-a-test-1-gpu-xpu]
runs-on: ubuntu-latest
steps:
- run: echo "stage-a passed"
# ==================== Stage B ==================== #
stage-b-test-1-gpu-xpu:
needs: [check-changes, pr-gate, wait-for-stage-a]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: intel-bmg
env:
DOCKERHUB_INTEL_USERNAME: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
DOCKERHUB_INTEL_TOKEN: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
steps:
- name: Reset workspace ownership
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.ref }}
- name: Start CI container
run: |
export HF_TOKEN="$(cat ~/huggingface_token.txt)"
bash scripts/ci/xpu/xpu_ci_start_container.sh
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install Dependency
timeout-minutes: 60
run: |
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --upgrade pip
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install pytest expecttest ray huggingface_hub tabulate
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip uninstall -y flashinfer-python sgl-kernel sglang
docker exec ci_sglang_xpu cp /sglang-checkout/python/pyproject_xpu.toml /sglang-checkout/python/pyproject.toml
docker exec -w /sglang-checkout/python ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir . --extra-index-url https://download.pytorch.org/whl/xpu
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir --no-deps xgrammar==0.1.33
docker exec ci_sglang_xpu /bin/bash -c '/opt/venv/bin/hf auth login --token ${HF_TOKEN}'
- name: Run stage-b tests
timeout-minutes: 60
run: |
docker exec ci_sglang_xpu bash -c "source /opt/venv/bin/activate && cd /sglang-checkout/test && python3 run_suite.py --hw xpu --suite stage-b-test-1-gpu-xpu"
- name: Cleanup container
if: always()
run: |
# pip install ran as root inside the container against the
# bind-mounted workspace, so build artifacts are root-owned on
# the host. Chown them back before rm to avoid Permission denied.
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
# Wipe everything the run wrote into the workspace so the next
# job starts from a clean tree.
rm -rf \
python/build \
python/dist \
python/sglang.egg-info \
python/sglang/*.egg-info \
test/result.jsonl \
test/results \
test/.pytest_cache \
.pytest_cache || true
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
find . -type f -name "*.pyc" -delete || true
docker rm -f ci_sglang_xpu || true
if [[ -n "${CI_SGLANG_XPU_IMAGE:-}" ]]; then
docker rmi -f "${CI_SGLANG_XPU_IMAGE}" || true
fi
finish:
if: always()
needs: [stage-a-test-1-gpu-xpu, stage-b-test-1-gpu-xpu, pr-gate]
runs-on: ubuntu-latest
steps:
- name: Check job status
run: |
stage_a="${{ needs.stage-a-test-1-gpu-xpu.result }}"
stage_b="${{ needs.stage-b-test-1-gpu-xpu.result }}"
if [ "$stage_a" != "success" ] && [ "$stage_a" != "skipped" ]; then
echo "stage-a failed with result: $stage_a"
exit 1
fi
if [ "$stage_b" != "success" ] && [ "$stage_b" != "skipped" ]; then
echo "stage-b failed with result: $stage_b"
exit 1
fi
echo "All jobs completed successfully"
exit 0
+620
View File
@@ -0,0 +1,620 @@
name: PR Test Base
run-name: >-
${{
github.event_name == 'schedule' && 'Scheduled Full Run'
|| github.event_name == 'workflow_dispatch' && format('Manual run by {0}', github.actor)
|| github.event_name == 'pull_request' && format('PR #{0} - {1}', github.event.pull_request.number, github.event.pull_request.title)
|| 'PR Test Base'
}}
on:
schedule:
- cron: '0 11,23 * * *' # Run 2x daily, 12h apart, off PR-push peaks
pull_request:
workflow_dispatch:
inputs:
force_continue_on_error:
description: "Force continue-on-error (test scheduled CI behavior)"
required: false
type: boolean
default: false
test_parallel_dispatch:
description: "Test parallel dispatch behavior (simulates scheduled run)"
required: false
type: boolean
default: false
run_all_tests:
description: "Run all tests (mirrors the scheduled cron's behavior — combine with test_parallel_dispatch for full sim)"
required: false
type: boolean
default: false
workflow_call:
inputs:
git_ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
skip_pr_test_health_check:
description: "Skip PR test health check fast-fail (e.g. for release branch cuts)"
required: false
type: boolean
default: false
concurrency:
# Concurrency group structure: pr-test-{event}-{branch}-{git_ref}
# - event_name prevents scheduled runs from colliding with fork PRs whose branch is named 'main'
# (without it, both resolve the branch segment to 'main' and block each other)
# - github.head_ref (pull_request) or github.ref_name (workflow_dispatch) normalizes to branch name
group: pr-test-${{ github.event_name }}-${{ github.head_ref || github.ref_name || 'default' }}-${{ inputs.git_ref || 'all' }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
SKIP_PR_TEST_HEALTH_CHECK: ${{ (inputs.skip_pr_test_health_check == true || inputs.test_parallel_dispatch == true || inputs.run_all_tests == true) && 'true' || 'false' }}
# Schedule / main-branch dispatch / workflow_call from main use refs/heads/main; PR events use refs/pull/*/merge
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
USE_VENV: false
permissions:
actions: write
contents: read
issues: read
pull-requests: read
jobs:
# =============================================== check changes ====================================================
check-changes:
uses: ./.github/workflows/_pr-test-check-changes.yml
with:
git_ref: ${{ inputs.git_ref || '' }}
run_all_tests: ${{ inputs.run_all_tests == true }}
force_continue_on_error: ${{ inputs.force_continue_on_error == true }}
secrets: inherit
call-pr-test-extra:
if: github.event_name == 'schedule' || inputs.test_parallel_dispatch == true || inputs.run_all_tests == true
uses: ./.github/workflows/pr-test-extra.yml
with:
git_ref: ${{ inputs.git_ref || '' }}
run_all_tests: true
secrets: inherit
# =============================================== Wait Jobs for Sequential PR Execution ====================================================
# These jobs poll GitHub API to wait for previous stages to complete.
# For PR runs: wait jobs run and enforce sequential execution via polling.
# For scheduled runs: wait jobs are skipped, enabling parallel execution for easier retry.
# For PRs with the `bypass-fastfail` label: wait jobs run but return success immediately
# (handled inside the wait-for-jobs action), so downstream stages dispatch in parallel.
wait-for-base-a:
needs: [check-changes, call-gate]
if: |
always() &&
!cancelled() &&
github.event_name == 'pull_request' &&
inputs.test_parallel_dispatch != true &&
(needs.check-changes.outputs.main_package == 'true' || needs.check-changes.outputs.sgl_kernel == 'true') &&
(needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped')
runs-on: ubuntu-latest
outputs:
base_a_result: ${{ steps.wait.outputs.result }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/check-maintenance
- uses: ./.github/actions/wait-for-jobs
id: wait
with:
stage-name: base-a
jobs: |
[
{"prefix": "base-a-test-1-gpu-small", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-1-gpu-small'].size }}},
{"prefix": "base-a-test-cpu", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-cpu'].size }}}
]
max-wait-minutes: '240'
wait-for-base-b:
needs: [check-changes, call-gate, wait-for-base-a]
if: |
always() &&
!cancelled() &&
github.event_name == 'pull_request' &&
inputs.test_parallel_dispatch != true &&
(needs.check-changes.outputs.main_package == 'true' || needs.check-changes.outputs.sgl_kernel == 'true') &&
(needs.wait-for-base-a.result == 'success' || needs.wait-for-base-a.result == 'skipped') &&
(needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped')
runs-on: ubuntu-latest
outputs:
base_b_result: ${{ steps.wait.outputs.result }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/check-maintenance
- uses: ./.github/actions/wait-for-jobs
id: wait
with:
stage-name: base-b
jobs: |
[
{"prefix": "base-b-test-1-gpu-small", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-b-test-1-gpu-small'].size }}},
{"prefix": "base-b-test-1-gpu-large", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-b-test-1-gpu-large'].size }}},
{"prefix": "base-b-test-2-gpu-large", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-b-test-2-gpu-large'].size }}},
{"prefix": "base-b-test-4-gpu-b200", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-b-test-4-gpu-b200'].size }}}
]
max-wait-minutes: '480'
# =============================================== PR Gate ====================================================
call-gate:
needs: check-changes
# Skip for scheduled runs (they run all tests)
if: |
github.event_name != 'schedule' &&
inputs.test_parallel_dispatch != true &&
(
needs.check-changes.outputs.main_package == 'true' ||
needs.check-changes.outputs.sgl_kernel == 'true' ||
needs.check-changes.outputs.jit_kernel == 'true' ||
needs.check-changes.outputs.multimodal_gen == 'true'
)
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
# =============================================== sgl-kernel ====================================================
sgl-kernel-build-wheels:
needs: [check-changes, call-gate]
# Skip for scheduled runs (they run stages independently).
if: |
github.event_name != 'schedule' &&
inputs.test_parallel_dispatch != true &&
needs.check-changes.result == 'success' &&
needs.check-changes.outputs.sgl_kernel == 'true' &&
needs.call-gate.result == 'success'
uses: ./.github/workflows/_pr-test-sgl-kernel-build.yml
with:
runs_on: x64-kernel-build-node
job_display_name: Build Wheel
git_ref: ${{ inputs.git_ref || '' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
sgl-kernel-build-wheels-arm:
needs: [check-changes, call-gate]
# Skip for scheduled runs (they run stages independently).
if: |
github.event_name != 'schedule' &&
inputs.test_parallel_dispatch != true &&
needs.check-changes.result == 'success' &&
needs.check-changes.outputs.sgl_kernel == 'true' &&
needs.call-gate.result == 'success'
uses: ./.github/workflows/_pr-test-sgl-kernel-build.yml
with:
runs_on: arm-kernel-build-node
job_display_name: Build Wheel Arm
arch_suffix: '-aarch64'
git_ref: ${{ inputs.git_ref || '' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
call-sgl-kernel-tests:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: |
github.event_name != 'schedule' &&
inputs.test_parallel_dispatch != true &&
needs.check-changes.outputs.sgl_kernel == 'true'
uses: ./.github/workflows/pr-test-sgl-kernel.yml
with:
runner_config: 4-gpu-b200
runs_on_map: ${{ needs.check-changes.outputs.runs_on_map }}
sgl_kernel: ${{ needs.check-changes.outputs.sgl_kernel }}
git_ref: ${{ inputs.git_ref || '' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
# =============================================== jit-kernel ====================================================
call-jit-kernel-tests:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
# Run on scheduled/parallel-dispatch runs (same pattern as the base-* stages) so the
# jit_kernel suite is exercised on main 3x daily, not only on PRs that touch jit_kernel/**.
# check-changes already forces jit_kernel='true' on scheduled runs (run_all_tests).
if: |
always() &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == true) || (!failure() && !cancelled())) &&
needs.check-changes.outputs.jit_kernel == 'true'
uses: ./.github/workflows/pr-test-jit-kernel.yml
with:
runner_config: 4-gpu-b200
runs_on_map: ${{ needs.check-changes.outputs.runs_on_map }}
jit_kernel: ${{ needs.check-changes.outputs.jit_kernel }}
# On scheduled/parallel-dispatch runs sgl-kernel-build-wheels is skipped, so the wheel
# artifact does not exist. Force sgl_kernel='false' there so the jit-kernel jobs skip the
# wheel download and run against the released/pinned sgl-kernel instead of a fresh build.
sgl_kernel: ${{ (github.event_name == 'schedule' || inputs.test_parallel_dispatch == true) && 'false' || needs.check-changes.outputs.sgl_kernel }}
git_ref: ${{ inputs.git_ref || '' }}
test_parallel_dispatch: ${{ inputs.test_parallel_dispatch == true && 'true' || 'false' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
# =============================================== primary ====================================================
# Runs on 5090 (32GB, SM120)
base-a-test-1-gpu-small:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-a-test-1-gpu-small
runner_config: 1-gpu-small
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '10'
secrets: inherit
base-a-test-cpu:
needs: [check-changes, call-gate]
if: |
always() &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == true) || (!failure() && !cancelled())) &&
(needs.check-changes.outputs.main_package == 'true')
runs-on: ubuntu-latest
timeout-minutes: 240
env:
HF_HOME: ${{ github.workspace }}/.hf-cache
strategy:
fail-fast: false
max-parallel: ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-cpu'].max_parallel }}
matrix:
partition: ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-cpu'].arr }}
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
df -h
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- 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 + Rust toolchain
timeout-minutes: 10
run: bash scripts/ci/utils/install_rust_protoc.sh
- name: Rust cache (sglang-grpc)
uses: Swatinem/rust-cache@v2
with:
workspaces: rust/sglang-grpc
shared-key: "sglang-grpc-cpu"
save-if: ${{ matrix.partition == 0 }}
# uv pip targets a venv by default; setup-python has no venv — install into that interpreter (see UV_SYSTEM_PYTHON in https://docs.astral.sh/uv/guides/integration/github/)
- name: Install dependencies
timeout-minutes: 20
env:
UV_SYSTEM_PYTHON: "1"
run: |
uv pip install -e "python[dev]" --index-strategy unsafe-best-match --prerelease allow
# Hosted runners are ephemeral, so models are re-fetched every run and the
# Hub occasionally returns 429s. Persist the HF cache in GitHub's cache
# storage (rolling key + restore-keys) so warm runs never hit the network.
- name: Cache HF hub
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.hf-cache
key: hf-cpu-${{ matrix.partition }}-${{ github.run_id }}
restore-keys: hf-cpu-${{ matrix.partition }}-
- name: Run test
timeout-minutes: 10
env:
CONTINUE_ON_ERROR_FLAG: ${{ needs.check-changes.outputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
run: |
cd test/
python3 run_suite.py --hw cpu --suite base-a-test-cpu --auto-partition-id ${{ matrix.partition }} --auto-partition-size ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-cpu'].size }} $CONTINUE_ON_ERROR_FLAG
# Runs on 5090 (32GB, SM120)
base-b-test-1-gpu-small:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-b-test-1-gpu-small
runner_config: 1-gpu-small
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
secrets: inherit
# Runs on H100 (80GB, SM90) - tests that don't pass on 5090 (FA3, FP8, high VRAM, etc.)
base-b-test-1-gpu-large:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-b-test-1-gpu-large
runner_config: 1-gpu-large
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
base-b-test-2-gpu-large:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-b-test-2-gpu-large
runner_config: 2-gpu-large
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
secrets: inherit
base-b-test-4-gpu-b200:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-b-test-4-gpu-b200
runner_config: 4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '40'
secrets: inherit
call-multimodal-gen-tests:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: |
always() &&
!cancelled() &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == true) || (!failure() && !cancelled())) &&
needs.check-changes.outputs.multimodal_gen == 'true'
uses: ./.github/workflows/pr-test-multimodal-gen.yml
with:
runner_config: 4-gpu-b200
runs_on_map: ${{ needs.check-changes.outputs.runs_on_map }}
multimodal_gen: ${{ needs.check-changes.outputs.multimodal_gen }}
sgl_kernel: ${{ needs.check-changes.outputs.sgl_kernel }}
continue_on_error: ${{ needs.check-changes.outputs.continue_on_error }}
git_ref: ${{ inputs.git_ref || '' }}
test_parallel_dispatch: ${{ inputs.test_parallel_dispatch == true && 'true' || 'false' }}
caller_needs_failure: ${{ (needs.call-gate.result == 'failure' || needs.sgl-kernel-build-wheels.result == 'failure' || needs.check-changes.result == 'failure') && 'true' || 'false' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true && 'true' || 'false' }}
secrets: inherit
base-c-test-4-gpu-h100:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-4-gpu-h100
runner_config: 4-gpu-h100
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
secrets: inherit
base-c-test-8-gpu-h200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-8-gpu-h200
runner_config: 8-gpu-h200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
# Per-model TP must match the test's launch in test/registered/ -- see
# FALLBACK_ARGS in scripts/ci/cuda/warmup_deep_gemm.py for extra dp/ep
# flags. Only models that actually invoke DeepGEMM kernels at runtime
# are listed. Cold-cache ~13 min; warm-cache <=30 s via marker file.
# Server CUDA Graph warmup is combined into this step (warmup_server_models unset).
warmup_deep_gemm_models: 'deepseek-ai/DeepSeek-V3-0324:8 deepseek-ai/DeepSeek-V3.2:8 zai-org/GLM-5-FP8:8 XiaomiMiMo/MiMo-V2-Flash:4 XiaomiMiMo/MiMo-V2.5:8'
warmup_timeout_minutes: '60'
secrets: inherit
base-c-test-8-gpu-h20:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-8-gpu-h20
runner_config: 8-gpu-h20
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
secrets: inherit
base-c-test-deepep-4-gpu-h100:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-deepep-4-gpu-h100
runner_config: deepep-4-gpu-h100
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
warmup_deep_gemm_models: 'lmsys/sglang-ci-dsv3-test:4'
warmup_server_models: 'lmsys/sglang-ci-dsv3-test:4'
secrets: inherit
base-c-test-deepep-4-gpu-b200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-deepep-4-gpu-b200
runner_config: deepep-4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
base-c-test-deepep-8-gpu-h200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-deepep-8-gpu-h200
runner_config: deepep-8-gpu-h200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
base-c-test-4-gpu-b200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-4-gpu-b200
runner_config: 4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
base-c-test-4-gpu-gb300:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-4-gpu-gb300
runner_config: 4-gpu-gb300
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
pr-test-finish:
needs:
[
call-gate,
check-changes,
sgl-kernel-build-wheels,
sgl-kernel-build-wheels-arm,
call-sgl-kernel-tests,
wait-for-base-a,
wait-for-base-b,
call-jit-kernel-tests,
call-multimodal-gen-tests,
base-a-test-1-gpu-small,
base-a-test-cpu,
base-b-test-1-gpu-small,
base-b-test-1-gpu-large,
base-b-test-2-gpu-large,
base-b-test-4-gpu-b200,
base-c-test-4-gpu-h100,
base-c-test-8-gpu-h20,
base-c-test-8-gpu-h200,
base-c-test-deepep-4-gpu-h100,
base-c-test-deepep-4-gpu-b200,
base-c-test-deepep-8-gpu-h200,
base-c-test-4-gpu-b200,
base-c-test-4-gpu-gb300,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
# Convert the 'needs' context to a JSON string
json_needs='${{ toJson(needs) }}'
# Get a list of all job names from the JSON keys
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
# For each job, extract its result
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
# Print the job name and its result
echo "$job: $result"
# Check for failure or cancellation and exit if found
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
# If the loop completes, all jobs were successful
echo "All jobs completed successfully"
exit 0
# =============================================== notify pr-states ====================================================
# Dispatches pr-states.yml after every attempt (initial + every rerun).
# workflow_run.completed only fires on the initial completion (one
# workflow_run record across attempts), so we explicitly dispatch —
# workflow_dispatch is the only event GITHUB_TOKEN can cascade-create.
#
# Fork PRs are excluded: their pull_request GITHUB_TOKEN is forced
# read-only regardless of declared `actions: write`, so dispatch 403s.
# Fork PRs get pr-states updates via the workflow_run subscription
# there (initial completion) and pull_request_target (push / label).
notify-pr-states:
needs: [pr-test-finish]
if: |
always() &&
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Dispatch pr-states refresh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh workflow run pr-states.yml \
--repo ${{ github.repository }} \
--ref main \
-f pr_number="$PR_NUMBER"
+233
View File
@@ -0,0 +1,233 @@
name: Release Branch Cut
on:
workflow_dispatch:
inputs:
branch_name:
description: 'Branch name to create (e.g., release/v0.5.7)'
required: true
type: string
commit_sha:
description: 'Commit SHA from main to cut the release branch from (defaults to latest main)'
required: false
type: string
default: ''
permissions:
actions: write
contents: write
issues: read
pull-requests: read
jobs:
cut-release-branch:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
environment: 'prod'
outputs:
branch_name: ${{ steps.set_output.outputs.branch_name }}
branch_exists: ${{ steps.check_exists.outputs.branch_exists }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Validate branch name
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
if [ -z "$BRANCH_NAME" ]; then
echo "::error::Branch name is required"
exit 1
fi
# Validate branch name format (should start with release/)
if [[ ! "$BRANCH_NAME" =~ ^release/ ]]; then
echo "::warning::Branch name '$BRANCH_NAME' does not follow convention 'release/vX.Y.Z'"
fi
echo "Branch name: $BRANCH_NAME"
- name: Validate commit SHA
id: validate
run: |
COMMIT_SHA="${{ github.event.inputs.commit_sha }}"
# If no commit SHA provided, use latest main
if [ -z "$COMMIT_SHA" ]; then
COMMIT_SHA=$(git rev-parse HEAD)
echo "No commit SHA provided, using latest main: $COMMIT_SHA"
fi
# Verify the commit exists and is on main
if ! git cat-file -t "$COMMIT_SHA" > /dev/null 2>&1; then
echo "::error::Commit SHA '$COMMIT_SHA' does not exist"
exit 1
fi
# Check if commit is an ancestor of main (i.e., is on main branch)
if ! git merge-base --is-ancestor "$COMMIT_SHA" main; then
echo "::error::Commit SHA '$COMMIT_SHA' is not on the main branch"
exit 1
fi
echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "Validated commit SHA: $COMMIT_SHA"
- name: Check if branch already exists
id: check_exists
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then
echo "Branch '$BRANCH_NAME' already exists, skipping creation and proceeding to downstream tests"
echo "branch_exists=true" >> $GITHUB_OUTPUT
else
echo "Branch '$BRANCH_NAME' does not exist, proceeding with creation"
echo "branch_exists=false" >> $GITHUB_OUTPUT
fi
- name: Create release branch
if: steps.check_exists.outputs.branch_exists != 'true'
run: |
COMMIT_SHA="${{ steps.validate.outputs.COMMIT_SHA }}"
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
# Create branch from the specified commit
git checkout -b "$BRANCH_NAME" "$COMMIT_SHA"
echo "Successfully created branch '$BRANCH_NAME' from commit '$COMMIT_SHA'"
- name: Update version references in documentation
if: steps.check_exists.outputs.branch_exists != 'true'
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
# Extract version from branch name (e.g., release/v0.5.8 -> v0.5.8)
VERSION=$(echo "$BRANCH_NAME" | sed 's/release\///')
# Update git clone version references in docs
sed -i "s/git clone -b v[0-9]\+\.[0-9]\+\.[0-9]\+\.\?post\?[0-9]*/git clone -b $VERSION/" docs/get_started/install.md
sed -i "s/git clone -b v[0-9]\+\.[0-9]\+\.[0-9]\+\.\?post\?[0-9]*/git clone -b $VERSION/" docs/platforms/amd_gpu.md
# Check if any changes were made
if git diff --quiet; then
echo "No version references needed updating"
else
git add docs/get_started/install.md docs/platforms/amd_gpu.md
git commit -m "docs: update version references to $VERSION"
echo "Updated version references to $VERSION"
fi
- name: Push release branch
if: steps.check_exists.outputs.branch_exists != 'true'
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
git push origin "$BRANCH_NAME"
echo "Successfully pushed branch '$BRANCH_NAME'"
- name: Emit branch_name output
id: set_output
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Summary
run: |
COMMIT_SHA="${{ steps.validate.outputs.COMMIT_SHA }}"
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
BRANCH_EXISTS="${{ steps.check_exists.outputs.branch_exists }}"
if [ "$BRANCH_EXISTS" = "true" ]; then
STATUS="Branch already existed — creation skipped, downstream tests will run"
else
STATUS="Newly created"
fi
echo "## Release Branch Cut Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`$BRANCH_NAME\` |" >> $GITHUB_STEP_SUMMARY
echo "| Commit | \`$COMMIT_SHA\` |" >> $GITHUB_STEP_SUMMARY
echo "| Status | $STATUS |" >> $GITHUB_STEP_SUMMARY
echo "| Triggered by | @${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Next Steps" >> $GITHUB_STEP_SUMMARY
echo "1. Tests are automatically triggered on the release branch" >> $GITHUB_STEP_SUMMARY
echo "2. Apply any hotfixes if needed" >> $GITHUB_STEP_SUMMARY
echo "3. Create a tag to trigger release: \`gh workflow run release-tag.yml -f version=X.Y.Z -f ref=$BRANCH_NAME\`" >> $GITHUB_STEP_SUMMARY
run-pr-tests-nvidia:
needs: cut-release-branch
uses: ./.github/workflows/pr-test.yml
with:
git_ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
skip_pr_test_health_check: true
secrets: inherit
run-pr-tests-amd:
needs: cut-release-branch
uses: ./.github/workflows/pr-test-amd.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
secrets: inherit
run-pr-test-npu:
needs: cut-release-branch
uses: ./.github/workflows/pr-test-npu.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
secrets: inherit
run-pr-tests-xeon:
needs: cut-release-branch
uses: ./.github/workflows/pr-test-xeon.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
secrets: inherit
run-pr-tests-xpu:
needs: cut-release-branch
uses: ./.github/workflows/pr-test-xpu.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
secrets: inherit
run-nightly-tests-nvidia:
needs: cut-release-branch
uses: ./.github/workflows/nightly-test-nvidia.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
secrets: inherit
run-nightly-tests-amd:
needs: cut-release-branch
uses: ./.github/workflows/nightly-test-amd.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
secrets: inherit
run-nightly-tests-npu:
needs: cut-release-branch
uses: ./.github/workflows/nightly-test-npu.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
secrets: inherit
run-nightly-tests-intel:
needs: cut-release-branch
uses: ./.github/workflows/nightly-test-intel.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
secrets: inherit
@@ -0,0 +1,63 @@
name: Release Docker Images Nightly Miles ROCm7.0 (AMD)
on:
workflow_dispatch:
schedule:
- cron: "0 12 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: amd-docker-scale
environment: "prod"
strategy:
fail-fast: false
matrix:
include:
- gpu_arch: gfx950
rocm_tag: rocm700-mi35x
sglang_image_repo: rocm/sgl-dev
sglang_image_tag: v0.5.14-rocm700-mi35x-20260627
- gpu_arch: gfx942
rocm_tag: rocm700-mi30x
sglang_image_tag: v0.5.10-rocm700-mi30x
steps:
- name: Checkout miles repository
uses: actions/checkout@v4
with:
repository: radixark/miles
ref: main
fetch-depth: 0
- name: Set date
run: echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
- name: Record miles commit
id: miles
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Build and push miles image to rocm/sgl-dev
run: |
base="miles-${{ matrix.rocm_tag }}"
dated="${base}-${{ env.DATE }}"
echo "IMAGE_TAG=${dated}" >> $GITHUB_ENV
echo "Building rocm/sgl-dev:${dated} from miles main @ ${{ steps.miles.outputs.sha }}"
docker build . \
-f docker/Dockerfile.rocm \
--build-arg GPU_ARCH=${{ matrix.gpu_arch }} \
--build-arg SGLANG_IMAGE_REPO=${{ matrix.sglang_image_repo || 'lmsysorg/sglang' }} \
--build-arg SGLANG_IMAGE_TAG=${{ matrix.sglang_image_tag }} \
--build-arg MILES_COMMIT=${{ steps.miles.outputs.sha }} \
-t rocm/sgl-dev:${dated} \
--no-cache
docker push rocm/sgl-dev:${dated}
@@ -0,0 +1,58 @@
name: Release Docker Images Nightly Miles ROCm7.2 (AMD)
on:
workflow_dispatch:
schedule:
- cron: "0 12 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: amd-docker-scale
environment: "prod"
strategy:
fail-fast: false
matrix:
include:
- gpu_arch: gfx950
rocm_tag: rocm720-mi35x
sglang_image_tag: v0.5.10-rocm720-mi35x
steps:
- name: Checkout miles repository
uses: actions/checkout@v4
with:
repository: radixark/miles
ref: main
fetch-depth: 0
- name: Set date
run: echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
- name: Record miles commit
id: miles
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Build and push miles image to rocm/sgl-dev
run: |
base="miles-${{ matrix.rocm_tag }}"
dated="${base}-${{ env.DATE }}"
echo "IMAGE_TAG=${dated}" >> $GITHUB_ENV
echo "Building rocm/sgl-dev:${dated} from miles main @ ${{ steps.miles.outputs.sha }}"
docker build . \
-f docker/Dockerfile.rocm \
--build-arg GPU_ARCH=${{ matrix.gpu_arch }} \
--build-arg SGLANG_IMAGE_TAG=${{ matrix.sglang_image_tag }} \
--build-arg MILES_COMMIT=${{ steps.miles.outputs.sha }} \
-t rocm/sgl-dev:${dated} \
--no-cache
docker push rocm/sgl-dev:${dated}
@@ -0,0 +1,252 @@
name: Release Docker Images Nightly ROCm7.0 (AMD)
on:
workflow_dispatch:
schedule:
- cron: '0 12 * * *'
concurrency:
# A PR number if a pull request and otherwise the commit hash. This cancels
# queued and in-progress runs for the same PR (presubmit) or commit
# (postsubmit). The workflow name is prepended to avoid conflicts between
# different workflows.
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: amd-docker-scale
environment: 'prod'
strategy:
fail-fast: false
matrix:
gpu_arch: ['gfx942', 'gfx950']
build_type: ['all']
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git describe to find tags
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: "Set Date"
run: |
echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
- name: Get version from latest tag
id: version
run: |
# Use the shared helper so stable/post releases sort above rc tags.
VERSION=$(python3 python/tools/get_version_tag.py --tag-only | sed 's/^v//')
if [ -z "$VERSION" ]; then
echo "::error::Could not determine version from git tags"
exit 1
fi
# Get short commit hash of current HEAD
COMMIT_HASH=$(git rev-parse --short HEAD)
# Compose pretend version for setuptools_scm: e.g., 0.5.8.dev20260129+g1a2b3c4
PRETEND_VERSION="${VERSION}.dev${{ env.DATE }}+g${COMMIT_HASH}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "pretend_version=${PRETEND_VERSION}" >> $GITHUB_OUTPUT
echo "Detected version: ${VERSION}"
echo "Pretend version for pip: ${PRETEND_VERSION}"
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Build and Push to rocm/sgl-dev
run: |
version=${{ steps.version.outputs.version }}
pretend_version=${{ steps.version.outputs.pretend_version }}
echo "Version: ${version}"
echo "Pretend version: ${pretend_version}"
if [ "${{ matrix.gpu_arch }}" = "gfx942" ]; then
rocm_tag="rocm700-mi30x"
elif [ "${{ matrix.gpu_arch }}" = "gfx950" ]; then
rocm_tag="rocm700-mi35x"
else
echo "Unsupported gfx arch"
exit 1
fi
tag=v${version}-${rocm_tag}
echo "IMAGE_TAG=${tag}-${{ env.DATE }}" >> $GITHUB_ENV
# remove --build-arg NIC_BACKEND=ainic for auto detection nic support in mori
# UBUNTU_MIRROR forces apt over HTTPS to dodge port-80 reachability flakes
# to Canonical's archive.ubuntu.com mirror IPs from the amd-docker-scale runner.
docker build . -f docker/rocm.Dockerfile --build-arg SGL_BRANCH=${{ github.ref_name }} --build-arg BUILD_TYPE=${{ matrix.build_type }} --build-arg GPU_ARCH=${{ matrix.gpu_arch }} --build-arg ENABLE_MORI=1 --build-arg ENABLE_NIXL=1 --build-arg SETUPTOOLS_SCM_PRETEND_VERSION=${pretend_version} --build-arg UBUNTU_MIRROR=https://archive.ubuntu.com -t rocm/sgl-dev:${tag}-${{ env.DATE }} --no-cache
docker push rocm/sgl-dev:${tag}-${{ env.DATE }}
# Persist the tag right after rocm/sgl-dev push succeeds so the local
# registry mirror can run even if a later step in this job (lmsys push)
# fails. By default this step only runs when the previous step succeeded,
# so the artifact only exists when an image actually landed on Docker Hub.
- name: Save published image tag
run: |
mkdir -p image-tag
echo "${{ env.IMAGE_TAG }}" > "image-tag/${{ matrix.gpu_arch }}.txt"
- name: Upload image tag artifact
uses: actions/upload-artifact@v4
with:
name: image-tag-${{ matrix.gpu_arch }}
path: image-tag/${{ matrix.gpu_arch }}.txt
retention-days: 1
- name: Login to Docker Hub (lmsys)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to lmsysorg/sglang-rocm
run: |
docker tag rocm/sgl-dev:${{ env.IMAGE_TAG }} lmsysorg/sglang-rocm:${{ env.IMAGE_TAG }}
docker push lmsysorg/sglang-rocm:${{ env.IMAGE_TAG }}
# Mirror the freshly published rocm/sgl-dev image to the in-network Docker
# registry so AMD CI runners can pull without hitting Docker Hub rate limits.
# The tag is read verbatim from the publish job's artifact so this job uses
# exactly the same tag that publish pushed (only the registry prefix differs).
# `!cancelled()` lets us still mirror successful matrix legs when other legs
# of publish failed; legs without an artifact will fail at download and be
# the only ones marked red.
push_local_registry:
if: ${{ !cancelled() && github.repository == 'sgl-project/sglang' }}
runs-on: linux-mi300-1gpu-sglang
environment: 'prod'
needs: publish
strategy:
fail-fast: false
matrix:
gpu_arch: ['gfx942', 'gfx950']
steps:
- name: Download image tag artifact
uses: actions/download-artifact@v4
with:
name: image-tag-${{ matrix.gpu_arch }}
- name: Read image tag
run: |
image_tag=$(tr -d '[:space:]' < "${{ matrix.gpu_arch }}.txt")
if [ -z "${image_tag}" ]; then
echo "::error::Image tag artifact is empty"
exit 1
fi
echo "IMAGE_TAG=${image_tag}" >> $GITHUB_ENV
echo "Resolved IMAGE_TAG=${image_tag}"
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Mirror rocm/sgl-dev to local registry
run: |
src="rocm/sgl-dev:${{ env.IMAGE_TAG }}"
dst="10.44.14.109:5000/rocm/sgl-dev:${{ env.IMAGE_TAG }}"
docker pull "${src}"
docker tag "${src}" "${dst}"
docker push "${dst}"
# Temporarily disable docker cache seeding until performant storage is in place
# cache:
# if: false
# # if: always() && github.repository == 'sgl-project/sglang'
# runs-on: linux-mi300-gpu-1
# environment: 'prod'
# needs: publish
# strategy:
# fail-fast: false
# matrix:
# gpu_arch: ['gfx942']
# build_type: ['all']
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
# with:
# fetch-depth: 0 # Required for git describe to find tags
# - name: "Set Date"
# run: |
# echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
# - name: Get version from latest tag
# id: version
# run: |
# # Use the shared helper so stable/post releases sort above rc tags.
# VERSION=$(python3 python/tools/get_version_tag.py --tag-only | sed 's/^v//')
# if [ -z "$VERSION" ]; then
# echo "::error::Could not determine version from git tags"
# exit 1
# fi
# echo "version=${VERSION}" >> $GITHUB_OUTPUT
# echo "Detected version: ${VERSION}"
# - name: Login to Docker Hub
# uses: docker/login-action@v2
# with:
# username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
# password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
# - name: Pull and Save Docker Image to Cache
# run: |
# set -euxo pipefail
# version=${{ steps.version.outputs.version }}
# echo "Version: ${version}"
# if [ "${{ matrix.gpu_arch }}" = "gfx942" ]; then
# rocm_tag="rocm700-mi30x"
# else
# echo "Unsupported gfx arch"
# exit 1
# fi
# tag=v${version}-${rocm_tag}
# if [ "${{ matrix.build_type }}" = "all" ]; then
# tag_suffix=""
# else
# echo "Unsupported build type"
# exit 1
# fi
# image="rocm/sgl-dev:${tag}-${{ env.DATE }}${tag_suffix}"
# # Determine target cache file name based on ROCm variant
# if [[ "${rocm_tag}" == rocm700* ]]; then
# final_path="/home/runner/sgl-data/docker/image-700.tar"
# else
# echo "Unexpected ROCm tag: ${rocm_tag}"
# exit 1
# fi
# tmp_path="${final_path}.tmp"
# echo "Pulling image: ${image}"
# docker pull "${image}"
# echo "Saving to temp file: ${tmp_path}"
# docker save "${image}" -o "${tmp_path}"
# echo "Moving to final path: ${final_path}"
# mv -f "${tmp_path}" "${final_path}"
# echo "Cache populated successfully at ${final_path}"
@@ -0,0 +1,173 @@
name: Release Docker Images Nightly ROCm7.2 (AMD)
on:
workflow_dispatch:
inputs:
job_select:
description: 'Select which release job to run'
required: false
type: choice
default: 'all'
options:
- 'all'
- publish
schedule:
- cron: '0 12 * * *'
concurrency:
# A PR number if a pull request and otherwise the commit hash. This cancels
# queued and in-progress runs for the same PR (presubmit) or commit
# (postsubmit). The workflow name is prepended to avoid conflicts between
# different workflows.
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: True
jobs:
publish:
if: github.repository == 'sgl-project/sglang' && (github.event_name != 'workflow_dispatch' || inputs.job_select == 'all' || inputs.job_select == 'publish')
runs-on: amd-docker-scale
environment: 'prod'
strategy:
fail-fast: false
matrix:
gpu_arch: ['gfx942-rocm720', 'gfx950-rocm720']
build_type: ['all']
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git describe to find tags
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: "Set Date"
run: |
echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
- name: Get version from latest tag
id: version
run: |
# Use the shared helper so stable/post releases sort above rc tags.
VERSION=$(python3 python/tools/get_version_tag.py --tag-only | sed 's/^v//')
if [ -z "$VERSION" ]; then
echo "::error::Could not determine version from git tags"
exit 1
fi
# Get short commit hash of current HEAD
COMMIT_HASH=$(git rev-parse --short HEAD)
# Compose pretend version for setuptools_scm: e.g., 0.5.8.post1.dev20260211+g1a2b3c4
PRETEND_VERSION="${VERSION}.dev${{ env.DATE }}+g${COMMIT_HASH}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "pretend_version=${PRETEND_VERSION}" >> $GITHUB_OUTPUT
echo "Detected version: ${VERSION}"
echo "Pretend version for pip: ${PRETEND_VERSION}"
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Build and Push to rocm/sgl-dev
run: |
version=${{ steps.version.outputs.version }}
pretend_version=${{ steps.version.outputs.pretend_version }}
echo "Version: ${version}"
echo "Pretend version: ${pretend_version}"
if [ "${{ matrix.gpu_arch }}" = "gfx942-rocm720" ]; then
rocm_tag="rocm720-mi30x"
elif [ "${{ matrix.gpu_arch }}" = "gfx950-rocm720" ]; then
rocm_tag="rocm720-mi35x"
else
echo "Unsupported gfx arch"
exit 1
fi
tag=v${version}-${rocm_tag}
echo "IMAGE_TAG=${tag}-${{ env.DATE }}" >> $GITHUB_ENV
# remove --build-arg NIC_BACKEND=ainic for auto detection nic support in mori
# UBUNTU_MIRROR forces apt over HTTPS to dodge port-80 reachability flakes
# to Canonical's archive.ubuntu.com mirror IPs from the amd-docker-scale runner.
docker build . -f docker/rocm.Dockerfile --build-arg SGL_BRANCH=${{ github.ref_name }} --build-arg BUILD_TYPE=${{ matrix.build_type }} --build-arg GPU_ARCH=${{ matrix.gpu_arch }} --build-arg ENABLE_MORI=1 --build-arg ENABLE_NIXL=1 --build-arg SETUPTOOLS_SCM_PRETEND_VERSION=${pretend_version} --build-arg UBUNTU_MIRROR=https://archive.ubuntu.com -t rocm/sgl-dev:${tag}-${{ env.DATE }} --no-cache
docker push rocm/sgl-dev:${tag}-${{ env.DATE }}
# Persist the tag right after rocm/sgl-dev push succeeds so the local
# registry mirror can run even if a later step in this job (lmsys push)
# fails. By default this step only runs when the previous step succeeded,
# so the artifact only exists when an image actually landed on Docker Hub.
- name: Save published image tag
run: |
mkdir -p image-tag
echo "${{ env.IMAGE_TAG }}" > "image-tag/${{ matrix.gpu_arch }}.txt"
- name: Upload image tag artifact
uses: actions/upload-artifact@v4
with:
name: image-tag-${{ matrix.gpu_arch }}
path: image-tag/${{ matrix.gpu_arch }}.txt
retention-days: 1
- name: Login to Docker Hub (lmsys)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to lmsysorg/sglang-rocm
run: |
docker tag rocm/sgl-dev:${{ env.IMAGE_TAG }} lmsysorg/sglang-rocm:${{ env.IMAGE_TAG }}
docker push lmsysorg/sglang-rocm:${{ env.IMAGE_TAG }}
# Mirror the freshly published rocm/sgl-dev image to the in-network Docker
# registry so AMD CI runners can pull without hitting Docker Hub rate limits.
# The tag is read verbatim from the publish job's artifact so this job uses
# exactly the same tag that publish pushed (only the registry prefix differs).
# `!cancelled()` lets us still mirror successful matrix legs when other legs
# of publish failed; legs without an artifact will fail at download and be
# the only ones marked red.
push_local_registry:
if: ${{ !cancelled() && github.repository == 'sgl-project/sglang' && (github.event_name != 'workflow_dispatch' || inputs.job_select == 'all' || inputs.job_select == 'publish') }}
runs-on: linux-mi300-1gpu-sglang
environment: 'prod'
needs: publish
strategy:
fail-fast: false
matrix:
gpu_arch: ['gfx942-rocm720', 'gfx950-rocm720']
steps:
- name: Download image tag artifact
uses: actions/download-artifact@v4
with:
name: image-tag-${{ matrix.gpu_arch }}
- name: Read image tag
run: |
image_tag=$(tr -d '[:space:]' < "${{ matrix.gpu_arch }}.txt")
if [ -z "${image_tag}" ]; then
echo "::error::Image tag artifact is empty"
exit 1
fi
echo "IMAGE_TAG=${image_tag}" >> $GITHUB_ENV
echo "Resolved IMAGE_TAG=${image_tag}"
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Mirror rocm/sgl-dev to local registry
run: |
src="rocm/sgl-dev:${{ env.IMAGE_TAG }}"
dst="10.44.14.109:5000/rocm/sgl-dev:${{ env.IMAGE_TAG }}"
docker pull "${src}"
docker tag "${src}" "${dst}"
docker push "${dst}"
+91
View File
@@ -0,0 +1,91 @@
name: Release Docker Images (AMD)
on:
push:
tags:
- 'v[0-9]+.*'
workflow_dispatch:
inputs:
version:
description: 'Version to build (without v prefix, e.g., 0.5.7)'
required: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: amd-docker-scale
environment: 'prod'
strategy:
matrix:
rocm_version: ['rocm700', 'rocm720']
gpu_arch: ['gfx942', 'gfx950']
build_type: ['all']
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get version from tag
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Extract version from tag (e.g., v0.5.7 -> 0.5.7)
VERSION="${GITHUB_REF_NAME#v}"
fi
# Validate version format
if [ -z "$VERSION" ]; then
echo "::error::Version is empty"
exit 1
fi
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version format: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Build and Push
run: |
version=${{ steps.version.outputs.version }}
echo "Version: ${version}"
gpu_arch_suffix=""
if [ "${{ matrix.rocm_version }}" = "rocm700" ]; then
if [ "${{ matrix.gpu_arch }}" = "gfx942" ]; then
rocm_tag="rocm700-mi30x"
elif [ "${{ matrix.gpu_arch }}" = "gfx950" ]; then
rocm_tag="rocm700-mi35x"
else
echo "Unsupported gfx arch"
exit 1
fi
elif [ "${{ matrix.rocm_version }}" = "rocm720" ]; then
gpu_arch_suffix="-${{ matrix.rocm_version }}"
if [ "${{ matrix.gpu_arch }}" = "gfx942" ]; then
rocm_tag="rocm720-mi30x"
elif [ "${{ matrix.gpu_arch }}" = "gfx950" ]; then
rocm_tag="rocm720-mi35x"
else
echo "Unsupported gfx arch"
exit 1
fi
else
echo "Unsupported rocm version"
exit 1
fi
tag=v${version}-${rocm_tag}
# rocm.Dockerfile expects SGL_BRANCH with 'v' prefix for git tag checkout
# remove --build-arg NIC_BACKEND=ainic for auto detection nic support in mori
# UBUNTU_MIRROR forces apt over HTTPS to dodge port-80 reachability flakes
# to Canonical's archive.ubuntu.com mirror IPs from the amd-docker-scale runner.
docker build . -f docker/rocm.Dockerfile --build-arg BUILD_TYPE=${{ matrix.build_type }} --build-arg GPU_ARCH=${{ matrix.gpu_arch }}${gpu_arch_suffix} --build-arg SGL_BRANCH=v${version} --build-arg ENABLE_MORI=1 --build-arg UBUNTU_MIRROR=https://archive.ubuntu.com -t lmsysorg/sglang:${tag} --no-cache
docker push lmsysorg/sglang:${tag}
+200
View File
@@ -0,0 +1,200 @@
name: Build and Push Development Docker Images
on:
workflow_dispatch:
inputs:
pr_number:
description: "PR number to build from (leave empty to use current branch)"
required: false
default: ""
tag:
description: "Custom tag suffix (overrides pr_number in tag). E.g. 'my-test' → dev-my-test, dev-cu13-my-test, etc."
required: false
default: ""
image_repo:
description: "Docker Hub repo to push to. Use lmsysorg/sglang-staging for testing."
required: false
default: "lmsysorg/sglang"
overlay_dockerfile:
description: "Optional extra Dockerfile lines appended after FROM <base> to build a layered image (e.g. 'RUN pip install ...'). Note: this job has no repo checkout, so only FROM + RUN work — you cannot COPY files from this repo. Leave empty to skip overlay."
required: false
default: ""
overlay_cudas:
description: "Comma-separated cuda variants to overlay onto (cu12, cu13). Default 'cu13'."
required: false
default: "cu13"
overlay_tag_suffix:
description: "Overlay output suffix appended to the base tag. EMPTY = overwrite the base tag(s); non-empty = append (e.g. 'msa')."
required: false
default: ""
schedule:
- cron: "0 0 * * *"
concurrency:
group: release-docker-dev-${{ inputs.tag || inputs.pr_number || 'nightly' }}
cancel-in-progress: true
jobs:
prepare:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
checkout_ref: ${{ steps.config.outputs.checkout_ref }}
extra_build_args: ${{ steps.config.outputs.extra_build_args }}
tag_config: ${{ steps.config.outputs.tag_config }}
overlay_cudas_json: ${{ steps.config.outputs.overlay_cudas_json }}
overlay_tags_json: ${{ steps.config.outputs.overlay_tags_json }}
steps:
- name: Compute build configuration
id: config
env:
# Inject multi-line/free-text inputs via env to avoid ${{ }} injection
# breaking the bash script on quotes or $ in the Dockerfile snippet.
OVERLAY_DOCKERFILE_INPUT: ${{ inputs.overlay_dockerfile }}
OVERLAY_CUDAS_INPUT: ${{ inputs.overlay_cudas }}
OVERLAY_SUFFIX_INPUT: ${{ inputs.overlay_tag_suffix }}
run: |
# Determine checkout ref
if [ -n "${{ inputs.pr_number }}" ]; then
echo "checkout_ref=refs/pull/${{ inputs.pr_number }}/head" >> $GITHUB_OUTPUT
else
echo "checkout_ref=" >> $GITHUB_OUTPUT
fi
# Determine extra build args. INSTALL_DYNAMO=1 installs the ai-dynamo
# nightly here only; release-docker.yml / release-docker-runtime.yml
# leave it at the Dockerfile default (0), keeping it out of releases.
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "extra_build_args=--build-arg USE_LATEST_SGLANG=1 --build-arg INSTALL_DYNAMO=1 --build-arg CMAKE_BUILD_PARALLEL_LEVEL=\$(nproc)" >> $GITHUB_OUTPUT
else
echo "extra_build_args=--build-arg BRANCH_TYPE=local --build-arg INSTALL_DYNAMO=1 --build-arg CMAKE_BUILD_PARALLEL_LEVEL=\$(nproc)" >> $GITHUB_OUTPUT
fi
# Determine tag suffix
SUFFIX=""
if [ -n "${{ inputs.tag }}" ]; then
SUFFIX="-${{ inputs.tag }}"
elif [ -n "${{ inputs.pr_number }}" ]; then
SUFFIX="-pr-${{ inputs.pr_number }}"
fi
# Build tag config. dev-cu13 / nightly-dev-cu13 are published as
# aliases on the cu130 image for backwards compatibility with
# consumers pinned to the pre-flip names.
if [ -z "${SUFFIX}" ]; then
# Nightly: include dated tags
TAG_CONFIG='[{"cuda":"cu129","tags":["dev-cu12","nightly-dev-cu12-{date}-{short_sha}"]},{"cuda":"cu130","tags":["dev","dev-cu13","nightly-dev-{date}-{short_sha}","nightly-dev-cu13-{date}-{short_sha}"]}]'
else
TAG_CONFIG="[{\"cuda\":\"cu129\",\"tags\":[\"dev-cu12${SUFFIX}\"]},{\"cuda\":\"cu130\",\"tags\":[\"dev${SUFFIX}\",\"dev-cu13${SUFFIX}\"]}]"
fi
echo "tag_config=${TAG_CONFIG}" >> $GITHUB_OUTPUT
# Overlay configuration (optional). When overlay_dockerfile is set,
# build one or more layered images on top of the just-built base.
# SUFFIX was computed above from inputs.tag / inputs.pr_number.
if [ -n "${OVERLAY_DOCKERFILE_INPUT}" ]; then
# Refuse to overlay without a tag/pr_number: SUFFIX would be empty,
# so base would point at the moving 'dev'/'dev-cu12'/'dev-cu13'
# latest tags and overwrite mode would clobber them.
if [ -z "${SUFFIX}" ]; then
echo "overlay_dockerfile is set but neither 'tag' nor 'pr_number' was given." >&2
echo "Set one so the overlay targets a specific tag, not the moving 'dev' latest." >&2
exit 1
fi
CUDAS_INPUT="${OVERLAY_CUDAS_INPUT}"
[ -z "${CUDAS_INPUT}" ] && CUDAS_INPUT="cu13"
OVERLAY_TAGS='{}'
IFS=',' read -ra CUDAS <<< "${CUDAS_INPUT}"
for C in "${CUDAS[@]}"; do
C="$(echo "${C}" | xargs)"
if [ "${C}" = "cu13" ]; then
BASES='["dev'"${SUFFIX}"'","dev-cu13'"${SUFFIX}"'"]'
elif [ "${C}" = "cu12" ]; then
BASES='["dev-cu12'"${SUFFIX}"'"]'
else
echo "Unknown overlay cuda variant: ${C} (expected cu12 or cu13)" >&2
exit 1
fi
if [ -n "${OVERLAY_SUFFIX_INPUT}" ]; then
# Append mode: primary base tag + suffix.
PRIMARY="$(echo "${BASES}" | jq -r '.[0]')"
OUTS='["'"${PRIMARY}-${OVERLAY_SUFFIX_INPUT}"'"]'
else
# Overwrite mode: replace every base alias.
OUTS="${BASES}"
fi
OVERLAY_TAGS="$(echo "${OVERLAY_TAGS}" | jq -c \
--arg c "${C}" --argjson b "${BASES}" --argjson o "${OUTS}" \
'.[$c] = {bases:$b, outs:$o}')"
done
CUDAS_JSON="$(echo "${CUDAS_INPUT}" | jq -c -R 'split(",") | map(gsub("^\\s+|\\s+$";""))')"
echo "overlay_cudas_json=${CUDAS_JSON}" >> $GITHUB_OUTPUT
echo "overlay_tags_json=${OVERLAY_TAGS}" >> $GITHUB_OUTPUT
else
echo "overlay_cudas_json=[]" >> $GITHUB_OUTPUT
echo "overlay_tags_json={}" >> $GITHUB_OUTPUT
fi
build-and-publish:
needs: prepare
uses: ./.github/workflows/_docker-build-and-publish.yml
with:
docker_target: framework_final
checkout_ref: ${{ needs.prepare.outputs.checkout_ref }}
extra_build_args: ${{ needs.prepare.outputs.extra_build_args }}
tag_config: ${{ needs.prepare.outputs.tag_config }}
image_repo: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
secrets: inherit
cleanup-nightly:
needs: build-and-publish
if: ${{ !inputs.tag && !inputs.pr_number }}
uses: ./.github/workflows/_docker-cleanup-nightly.yml
with:
tag_prefixes: '["nightly-dev", "nightly-dev-cu12", "nightly-dev-cu13"]'
image_repo: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
secrets: inherit
build-overlay:
needs: [prepare, build-and-publish]
if: ${{ inputs.overlay_dockerfile != '' && github.repository == 'sgl-project/sglang' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cuda: ${{ fromJson(needs.prepare.outputs.overlay_cudas_json) }}
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push overlay image (amd64 + arm64)
env:
REPO: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
TAGS: ${{ needs.prepare.outputs.overlay_tags_json }}
CUDA: ${{ matrix.cuda }}
# Injected via env to keep Dockerfile quotes/$ verbatim.
SNIPPET: ${{ inputs.overlay_dockerfile }}
run: |
set -euo pipefail
BASE="$(printf '%s' "$TAGS" | jq -r --arg c "$CUDA" '.[$c].bases[0]')"
mapfile -t OUTS < <(printf '%s' "$TAGS" | jq -r --arg c "$CUDA" '.[$c].outs[]')
{
printf 'FROM %s:%s\n' "$REPO" "$BASE"
printf '%s\n' "$SNIPPET"
} > Dockerfile.overlay
echo "::group::overlay Dockerfile ($CUDA)"
cat Dockerfile.overlay
echo "::endgroup::"
OUT_ARGS=""
for o in "${OUTS[@]}"; do OUT_ARGS="$OUT_ARGS -t $REPO:$o"; done
docker buildx build \
--platform linux/amd64,linux/arm64 \
-f Dockerfile.overlay \
$OUT_ARGS \
--push .
@@ -0,0 +1,39 @@
name: Release SGLang Model Gateway Docker Image
on:
push:
branches:
- main
paths:
- sgl-model-gateway/bindings/python/pyproject.toml
workflow_dispatch:
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push
run: |
version=$(cat sgl-model-gateway/bindings/python/src/sglang_router/version.py | cut -d'"' -f2)
tag=v${version}
docker buildx build . -f docker/gateway.Dockerfile \
--platform linux/amd64,linux/arm64 \
-t lmsysorg/sgl-model-gateway:${tag} \
-t lmsysorg/sgl-model-gateway:latest \
--push
@@ -0,0 +1,59 @@
name: Release Docker Images Nightly (Intel XPU)
on:
workflow_dispatch:
schedule:
- cron: '0 12 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: intel-bmg
environment: 'prod'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
path: sglang-nightly-build
fetch-depth: 0
- name: Set date and commit hash
id: image_meta
run: |
cd sglang-nightly-build
date_tag=$(date +%Y%m%d)
commit_hash=$(git rev-parse --short=7 HEAD)
if [ -z "${commit_hash}" ]; then
echo "::error::Could not resolve commit hash"
exit 1
fi
image_tag="nightly-dev-xpu-bmg-${date_tag}-${commit_hash}"
echo "DATE=${date_tag}" >> $GITHUB_ENV
echo "commit_hash=${commit_hash}" >> $GITHUB_OUTPUT
echo "IMAGE_TAG=${image_tag}" >> $GITHUB_ENV
echo "Image tag: ${image_tag}"
- name: Login to Docker Hub (Intel)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
password: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
- name: Build intel/sglang-dev
run: |
echo "Building intel/sglang-dev:${{ env.IMAGE_TAG }}"
docker build sglang-nightly-build \
-f sglang-nightly-build/docker/xpu.Dockerfile \
--build-arg SG_LANG_REPO=https://github.com/sgl-project/sglang.git \
--build-arg SG_LANG_BRANCH=main \
--no-cache --progress=plain \
-t "intel/sglang-dev:${{ env.IMAGE_TAG }}" \
-t "intel/sglang-dev:latest"
- name: Push intel/sglang-dev
run: |
docker push "intel/sglang-dev:${{ env.IMAGE_TAG }}"
docker push "intel/sglang-dev:latest"
@@ -0,0 +1,85 @@
name: Release Docker Images Nightly (NPU)
on:
pull_request:
branches:
- 'main'
paths:
- '.github/workflows/release-docker-npu-nightly.yml'
- 'docker/npu.Dockerfile'
workflow_dispatch:
schedule:
- cron: "0 16 * * *" # Execute at 0:00 a.m. Beijing Time every day
concurrency:
group: ${{ github.workflow }}-${{ github.sha }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-22.04-arm
strategy:
matrix:
cann_version: ["9.0.0"]
device_type: ["910b", "a3"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Free up disk space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: true
docker-images: false
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
lmsysorg/sglang
# push with schedule event
# push with workflow_dispatch event
tags: |
type=ref,event=pr
type=ref,event=branch
type=schedule,pattern=main
flavor: |
latest=false
suffix=-cann${{ matrix.cann_version }}-${{ matrix.device_type }},onlatest=true
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into docker hub
uses: docker/login-action@v3
if: ${{ github.repository == 'sgl-project/sglang' && github.event_name != 'pull_request' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Enable Docker multi-architecture build environment
# Emulate non-native architectures
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Required for building and pushing multi-arch Docker images
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
context: docker
file: docker/npu.Dockerfile
platforms: linux/arm64,linux/amd64
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }}
push: ${{ github.repository == 'sgl-project/sglang' && github.event_name != 'pull_request' }}
provenance: false
build-args: |
SGLANG_KERNEL_NPU_TAG=2026.6.2
CANN_VERSION=${{ matrix.cann_version }}
DEVICE_TYPE=${{ matrix.device_type }}
+93
View File
@@ -0,0 +1,93 @@
name: Release Docker Images (NPU)
on:
push:
tags:
- 'v[0-9]+.*'
workflow_dispatch:
inputs:
version:
description: 'Version to build (without v prefix, e.g., 0.5.7)'
required: true
jobs:
build:
runs-on: ubuntu-22.04-arm
strategy:
matrix:
cann_version: ["9.0.0"]
device_type: ["910b", "a3"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Free up disk space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: true
docker-images: false
# push with tag
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
lmsysorg/sglang
tags: |
type=ref,event=pr
flavor: |
latest=false
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Login to Docker Hub
uses: docker/login-action@v2
if: ${{ github.repository == 'sgl-project/sglang' && github.event_name != 'pull_request' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get version from tag
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Extract version from tag (e.g., v0.5.7 -> 0.5.7)
VERSION="${GITHUB_REF_NAME#v}"
fi
# Validate version format
if [ -z "$VERSION" ]; then
echo "::error::Version is empty"
exit 1
fi
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version format: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=v${VERSION}" >> $GITHUB_OUTPUT
echo "TAG=lmsysorg/sglang:v${VERSION}-cann${{ matrix.cann_version }}-${{ matrix.device_type }}" >> $GITHUB_OUTPUT
# Enable Docker multi-architecture build environment
# Emulate non-native architectures
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Required for building and pushing multi-arch Docker images
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
context: docker
file: docker/npu.Dockerfile
platforms: linux/arm64,linux/amd64
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags || steps.version.outputs.TAG }}
push: ${{ github.repository == 'sgl-project/sglang' && github.event_name != 'pull_request' }}
provenance: false
build-args: |
SGLANG_KERNEL_NPU_TAG=2026.6.2
CANN_VERSION=${{ matrix.cann_version }}
DEVICE_TYPE=${{ matrix.device_type }}
SGLANG_TAG=${{ steps.version.outputs.version }}
@@ -0,0 +1,55 @@
name: Release Docker Runtime Images
#
# Builds and publishes runtime Docker images (production-optimized, ~50% smaller):
# - lmsysorg/sglang:v{version}-runtime, lmsysorg/sglang:latest-runtime
# - lmsysorg/sglang:v{version}-cu129-runtime, lmsysorg/sglang:latest-cu129-runtime
#
on:
push:
tags:
- "v[0-9]+.*"
workflow_dispatch:
inputs:
version:
description: "Version to build (without v prefix, e.g., 0.5.7)"
required: true
image_repo:
description: "Docker Hub repo to push to. Use lmsysorg/sglang-staging for testing."
required: false
default: "lmsysorg/sglang"
jobs:
resolve-version:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#v}"
fi
if [ -z "$VERSION" ] || ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
build-and-publish:
needs: resolve-version
uses: ./.github/workflows/_docker-build-and-publish.yml
with:
docker_target: runtime
sgl_version: ${{ needs.resolve-version.outputs.version }}
use_environment: prod
image_repo: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
tag_config: |
[
{"cuda": "cu130", "tags": ["v${{ needs.resolve-version.outputs.version }}-runtime", "latest-runtime", "v${{ needs.resolve-version.outputs.version }}-cu130-runtime", "latest-cu130-runtime"]},
{"cuda": "cu129", "tags": ["v${{ needs.resolve-version.outputs.version }}-cu129-runtime", "latest-cu129-runtime"]}
]
secrets: inherit
+62
View File
@@ -0,0 +1,62 @@
name: Release Docker Xeon Images
on:
push:
tags:
- 'v[0-9]+.*'
workflow_dispatch:
inputs:
version:
description: 'Version to build (without v prefix, e.g., 0.5.7)'
required: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-24.04
environment: 'prod'
strategy:
matrix:
build_type: ['all']
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get version from tag
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Extract version from tag (e.g., v0.5.7 -> 0.5.7)
VERSION="${GITHUB_REF_NAME#v}"
fi
# Validate version format
if [ -z "$VERSION" ]; then
echo "::error::Version is empty"
exit 1
fi
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version format: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Build and Push
run: |
version=${{ steps.version.outputs.version }}
tag=v${version}-xeon
docker build . -f docker/xeon.Dockerfile \
--build-arg VER_SGLANG=v${version} \
-t lmsysorg/sglang:${tag} \
--no-cache
docker push lmsysorg/sglang:${tag}
+55
View File
@@ -0,0 +1,55 @@
name: Release Docker Images
#
# Builds and publishes framework Docker images (full development environment):
# - lmsysorg/sglang:v{version}, lmsysorg/sglang:latest (cuda 13)
# - lmsysorg/sglang:v{version}-cu129, lmsysorg/sglang:latest-cu129
#
on:
push:
tags:
- "v[0-9]+.*"
workflow_dispatch:
inputs:
version:
description: "Version to build (without v prefix, e.g., 0.5.7)"
required: true
image_repo:
description: "Docker Hub repo to push to. Use lmsysorg/sglang-staging for testing."
required: false
default: "lmsysorg/sglang"
jobs:
resolve-version:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#v}"
fi
if [ -z "$VERSION" ] || ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
build-and-publish:
needs: resolve-version
uses: ./.github/workflows/_docker-build-and-publish.yml
with:
docker_target: framework_final
sgl_version: ${{ needs.resolve-version.outputs.version }}
use_environment: prod
image_repo: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
tag_config: |
[
{"cuda": "cu130", "tags": ["v${{ needs.resolve-version.outputs.version }}", "latest", "v${{ needs.resolve-version.outputs.version }}-cu130", "latest-cu130"]},
{"cuda": "cu129", "tags": ["v${{ needs.resolve-version.outputs.version }}-cu129", "latest-cu129"]}
]
secrets: inherit
+167
View File
@@ -0,0 +1,167 @@
name: Release SGLang Model Gateway to PyPI
on:
push:
branches:
- main
paths:
- sgl-model-gateway/bindings/python/pyproject.toml
workflow_dispatch:
jobs:
build:
name: build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }})
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
target: [x86_64, aarch64]
manylinux: [auto]
include:
- os: ubuntu
platform: linux
- os: windows
ls: dir
target: x86_64
python-architecture: x64
interpreter: 3.9 3.10 3.11 3.12 3.13
- os: macos
target: aarch64
interpreter: 3.9 3.10 3.11 3.12 3.13
- os: ubuntu
platform: linux
target: aarch64
# musllinux
- os: ubuntu
platform: linux
target: x86_64
manylinux: musllinux_1_1
- os: ubuntu
platform: linux
target: aarch64
manylinux: musllinux_1_1
exclude:
- os: windows
target: aarch64
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root and delete sglang-repo
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
ls -alt
shell: bash
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
architecture: ${{ matrix.python-architecture || 'x64' }}
- name: Install twine
run: pip install -U twine
- name: Install protoc (macOS)
if: matrix.os == 'macos'
run: brew install protobuf
- name: Install protoc (Windows)
if: matrix.os == 'windows'
run: choco install protoc -y
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
working-directory: bindings/python
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux || 'auto' }}
args: --release --out dist --features vendored-openssl --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 3.14' }}
rust-toolchain: stable
docker-options: -e CI -e CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc -e CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
before-script-linux: |
# Install build dependencies (perl/make for vendored OpenSSL, protoc for gRPC)
if command -v yum &> /dev/null; then
yum update -y && yum install -y wget unzip gcc gcc-c++ perl-core make
# Install cross-compilation toolchain for aarch64 if needed
if [ "${{ matrix.target }}" = "aarch64" ]; then
yum install -y gcc-aarch64-linux-gnu gcc-c++-aarch64-linux-gnu || true
fi
elif command -v apt-get &> /dev/null; then
apt-get update && apt-get install -y wget unzip gcc g++ perl make
# Install cross-compilation toolchain for aarch64 if needed
if [ "${{ matrix.target }}" = "aarch64" ]; then
apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu || true
fi
fi
(cd /tmp && \
wget https://github.com/protocolbuffers/protobuf/releases/download/v32.0/protoc-32.0-linux-x86_64.zip && \
unzip protoc-32.0-linux-x86_64.zip -d /usr/local && \
rm protoc-32.0-linux-x86_64.zip)
protoc --version
- name: List built packages
run: ${{ matrix.ls || 'ls -lh' }} bindings/python/dist/
- name: Check packages
run: twine check --strict bindings/python/dist/*
- uses: actions/upload-artifact@v4
with:
name: packages-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'auto' }}
path: bindings/python/dist/
build-sdist:
name: Build SDist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root and delete sglang-repo
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
ls -alt
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Build SDist
uses: PyO3/maturin-action@v1
with:
working-directory: bindings/python
command: sdist
args: --out dist
rust-toolchain: stable
- uses: actions/upload-artifact@v4
with:
name: sdist
path: bindings/python/dist/*.tar.gz
upload:
name: Upload to PyPI
if: github.repository == 'sgl-project/sglang' # Ensure this job only runs for the sgl-project/sglang repository
needs: [build, build-sdist]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Upload to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN_ROUTER }}
run: |
pip install twine
twine upload dist/* --verbose
+175
View File
@@ -0,0 +1,175 @@
name: Release PyPI Nightly Wheels
on:
# Run daily at 2 AM UTC
schedule:
- cron: '0 2 * * *'
# Triggered by nightly Docker workflow to use same commit
repository_dispatch:
types: [nightly-release]
# Manual trigger for testing
workflow_dispatch:
inputs:
commit_sha:
description: 'Specific commit SHA to build (leave empty for latest)'
required: false
type: string
concurrency:
group: release-pypi-nightly-${{ github.ref }}
cancel-in-progress: true
jobs:
build-nightly-wheel:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
nightly_version: ${{ steps.build.outputs.nightly_version }}
commit_hash: ${{ steps.build.outputs.commit_hash }}
build_date: ${{ steps.build.outputs.build_date }}
steps:
- uses: actions/checkout@v4
with:
# Use commit from: 1) Docker workflow, 2) manual input, 3) latest main
ref: ${{ github.event.client_payload.commit_sha || inputs.commit_sha || github.sha }}
fetch-depth: 0 # Need full history for setuptools-scm
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install build dependencies
run: |
pip install build wheel setuptools setuptools-scm
# Needed by setuptools-rust to build the bundled native gRPC extension
# (rust/sglang-grpc) when `python -m build` builds the sglang wheel.
- name: Install protoc
run: sudo bash scripts/ci/utils/install_protoc.sh
- name: Build wheel
id: build
run: |
cd python
cp ../README.md ../LICENSE .
TAG=$(python3 ../python/tools/get_version_tag.py)
HASH="g$(git rev-parse --short HEAD)"
BUILD_DATE=$(date -u +%Y%m%d)
# Increment patch version for nightlies (e.g., v0.5.9 -> 0.5.10)
# Must always increment so nightly > latest tag per PEP 440 ordering:
# X.Y.Z.devN < X.Y.Z.rcN < X.Y.Z < X.Y.(Z+1).devN
VERSION=${TAG#v} # Remove 'v' prefix
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
PATCH_RAW=$(echo "$VERSION" | cut -d. -f3)
# Strip pre-release suffixes (rc0, post1, etc.) to get numeric patch
PATCH=$(echo "$PATCH_RAW" | sed 's/[^0-9].*//')
NEXT_PATCH=$((PATCH + 1))
NEXT_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}"
# Use date-based dev number for correct chronological sorting
# e.g., 0.5.9.dev20260215+g4cf4f0859 > 0.5.9.dev20260214+g45a4697d4
FORCE_VERSION="${NEXT_VERSION}.dev${BUILD_DATE}+${HASH}"
echo "Forcing nightly version to: $FORCE_VERSION"
export SETUPTOOLS_SCM_PRETEND_VERSION="$FORCE_VERSION"
# Build wheel
python3 -m build --wheel
# Extract version from built wheel filename
WHEEL_FILE=$(ls dist/*.whl)
NIGHTLY_VERSION=$(echo "$WHEEL_FILE" | sed 's/.*sglang-\(.*\)-py3.*/\1/')
# Get commit info
COMMIT_HASH=$(git rev-parse --short HEAD)
BUILD_DATE=$(date -u +%Y-%m-%d)
echo "Built wheel: $WHEEL_FILE"
echo "Nightly version: ${NIGHTLY_VERSION}"
echo "Commit: ${COMMIT_HASH}"
echo "Build date: ${BUILD_DATE}"
echo "nightly_version=${NIGHTLY_VERSION}" >> $GITHUB_OUTPUT
echo "commit_hash=${COMMIT_HASH}" >> $GITHUB_OUTPUT
echo "build_date=${BUILD_DATE}" >> $GITHUB_OUTPUT
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: nightly-wheel
path: python/dist/*.whl
retention-days: 7
release-nightly:
needs: build-nightly-wheel
runs-on: ubuntu-latest
environment: 'prod'
strategy:
fail-fast: false
# The wheel is CUDA-agnostic and built once — we just register the same
# artifact under cu129/sglang/ and cu130/sglang/ wheel indexes so users
# can install via either --extra-index-url. Serialize because both matrix
# runs clone and push to the same sgl-whl branch.
max-parallel: 1
matrix:
cuda_version: ['129', '130']
steps:
- uses: actions/checkout@v4
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: nightly-wheel
path: dist/
- name: List downloaded wheels
run: |
echo "Downloaded wheel:"
ls -lh dist/
- name: Create GitHub Release for nightly wheel
uses: softprops/action-gh-release@v2
with:
tag_name: nightly-${{ needs.build-nightly-wheel.outputs.build_date }}-${{ needs.build-nightly-wheel.outputs.commit_hash }}
name: Nightly Build ${{ needs.build-nightly-wheel.outputs.build_date }} (${{ needs.build-nightly-wheel.outputs.commit_hash }})
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
prerelease: true
body: |
Nightly build from commit ${{ github.sha }}
Build date: ${{ needs.build-nightly-wheel.outputs.build_date }}
Version: ${{ needs.build-nightly-wheel.outputs.nightly_version }}
files: |
dist/*.whl
- name: Clone wheel index repository
run: |
git clone https://oauth2:${WHL_TOKEN}@github.com/sgl-project/whl.git sgl-whl
cd sgl-whl
git config --local user.name "sglang-bot"
git config --local user.email "sglangbot@gmail.com"
env:
WHL_TOKEN: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Update wheel index
run: |
python3 scripts/update_nightly_whl_index.py \
--commit-hash ${{ needs.build-nightly-wheel.outputs.commit_hash }} \
--nightly-version ${{ needs.build-nightly-wheel.outputs.nightly_version }} \
--cuda-version ${{ matrix.cuda_version }} \
--build-date ${{ needs.build-nightly-wheel.outputs.build_date }}
- name: Push wheel index
run: |
cd sgl-whl
git add -A
git diff --staged --quiet || git commit -m "Update cu${{ matrix.cuda_version }} nightly wheel index for commit ${{ needs.build-nightly-wheel.outputs.commit_hash }}"
git push
+178
View File
@@ -0,0 +1,178 @@
name: Release PyPI PR Wheels
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to build wheel for (works with both internal and fork PRs)'
required: true
type: string
concurrency:
group: build-pr-wheel-${{ github.event.inputs.pr_number }}
cancel-in-progress: true
jobs:
build-pr-wheel:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
wheel_version: ${{ steps.gen_version.outputs.wheel_version }}
commit_hash: ${{ steps.gen_version.outputs.commit_hash }}
build_date: ${{ steps.gen_version.outputs.build_date }}
steps:
- uses: actions/checkout@v4
with:
ref: refs/pull/${{ inputs.pr_number }}/head
fetch-depth: 0 # Need full history for version generation
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Generate PR wheel version
id: gen_version
run: |
LATEST_TAG=$(python3 python/tools/get_version_tag.py)
BASE_VERSION=${LATEST_TAG#v}
echo "Latest release tag: ${LATEST_TAG}"
# Get commit info
COMMIT_HASH=$(git rev-parse --short HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
# Get current date in YYYY-MM-DD format
BUILD_DATE=$(date -u +%Y-%m-%d)
# Always use pr-{number} format for suffix
SUFFIX="pr-${{ inputs.pr_number }}"
# Generate PR wheel version following PEP 440
# Format: {base_version}.dev{commit_count}+pr-{number}.g{commit_hash}
WHEEL_VERSION="${BASE_VERSION}.dev${COMMIT_COUNT}+${SUFFIX}.g${COMMIT_HASH}"
echo "Base version: ${BASE_VERSION}"
echo "PR wheel version: ${WHEEL_VERSION}"
echo "Commit: ${COMMIT_HASH}"
echo "Build date: ${BUILD_DATE}"
echo "wheel_version=${WHEEL_VERSION}" >> $GITHUB_OUTPUT
echo "commit_hash=${COMMIT_HASH}" >> $GITHUB_OUTPUT
echo "base_version=${BASE_VERSION}" >> $GITHUB_OUTPUT
echo "build_date=${BUILD_DATE}" >> $GITHUB_OUTPUT
- name: Update pyproject.toml with PR wheel version
run: |
cd python
WHEEL_VERSION="${{ steps.gen_version.outputs.wheel_version }}"
# Update pyproject.toml to use static version instead of dynamic
# Remove 'version' from dynamic list and add static version
sed -i 's/dynamic = \["version"\]/dynamic = []/' pyproject.toml
sed -i "/^name = \"sglang\"/a version = \"${WHEEL_VERSION}\"" pyproject.toml
# Verify update
echo "Updated version in pyproject.toml:"
grep "^version" pyproject.toml
grep "^dynamic" pyproject.toml
- name: Install build dependencies
run: |
cd python
pip install build wheel setuptools
- name: Build wheel
run: |
cd python
cp ../README.md ../LICENSE .
python3 -m build --wheel
# List built wheels
echo "Built wheel:"
ls -lh dist/
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: pr-wheel-${{ inputs.pr_number }}
path: python/dist/*.whl
retention-days: 30
release-pr-wheel:
needs: build-pr-wheel
runs-on: ubuntu-latest
environment: 'prod'
steps:
- uses: actions/checkout@v4
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: pr-wheel-${{ inputs.pr_number }}
path: dist/
- name: List downloaded wheels
run: |
echo "Downloaded wheel:"
ls -lh dist/
- name: Create GitHub Release for PR wheel
uses: softprops/action-gh-release@v2
with:
tag_name: pr-${{ inputs.pr_number }}-${{ needs.build-pr-wheel.outputs.build_date }}-${{ needs.build-pr-wheel.outputs.commit_hash }}
name: "PR #${{ inputs.pr_number }} Build (${{ needs.build-pr-wheel.outputs.commit_hash }})"
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
prerelease: true
body: |
PR wheel build from PR #${{ inputs.pr_number }}
Commit: ${{ github.sha }}
Build date: ${{ needs.build-pr-wheel.outputs.build_date }}
Version: ${{ needs.build-pr-wheel.outputs.wheel_version }}
**Installation via index (pip):**
```bash
pip install sglang==${{ needs.build-pr-wheel.outputs.wheel_version }} --index-url https://sgl-project.github.io/whl/pr/
```
**Installation via index (uv):**
```bash
uv pip install sglang==${{ needs.build-pr-wheel.outputs.wheel_version }} --index-url https://sgl-project.github.io/whl/pr/ --extra-index-url https://pypi.org/simple --index-strategy unsafe-best-match
```
**Direct installation:**
```bash
pip install https://github.com/sgl-project/whl/releases/download/pr-${{ inputs.pr_number }}-${{ needs.build-pr-wheel.outputs.build_date }}-${{ needs.build-pr-wheel.outputs.commit_hash }}/sglang-${{ needs.build-pr-wheel.outputs.wheel_version }}-py3-none-any.whl
```
files: |
dist/*.whl
- name: Clone wheel index repository
run: |
git clone https://oauth2:${WHL_TOKEN}@github.com/sgl-project/whl.git sgl-whl
cd sgl-whl
git config --local user.name "sglang-bot"
git config --local user.email "sglangbot@gmail.com"
env:
WHL_TOKEN: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Update wheel index
run: |
python3 scripts/update_pr_whl_index.py \
--pr-number ${{ inputs.pr_number }} \
--commit-hash ${{ needs.build-pr-wheel.outputs.commit_hash }} \
--wheel-version ${{ needs.build-pr-wheel.outputs.wheel_version }} \
--build-date ${{ needs.build-pr-wheel.outputs.build_date }}
- name: Push wheel index
run: |
cd sgl-whl
git add -A
git diff --staged --quiet || git commit -m "Update PR wheel index for PR #${{ inputs.pr_number }} (commit ${{ needs.build-pr-wheel.outputs.commit_hash }})"
git push
+116
View File
@@ -0,0 +1,116 @@
name: Release PyPI
on:
push:
tags:
- 'v[0-9]+.*'
workflow_dispatch:
inputs:
version:
description: 'Release version to publish (e.g. v0.5.11). Overrides setuptools-scm.'
required: true
type: string
jobs:
build:
if: github.repository == 'sgl-project/sglang'
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
arch: [x86_64, aarch64]
include:
- arch: x86_64
runner: x64-docker-build-node
- arch: aarch64
runner: arm-docker-build-node
runs-on: ${{ matrix.runner }}
steps:
# Self-hosted build nodes retain the workspace across jobs. Prior builds
# leave root-owned artifacts behind that actions/checkout cannot remove,
# causing EACCES on rmdir. Wipe them via a throwaway root container.
- name: Clean workspace (remove root-owned files from prior runs)
run: |
docker run --rm -v "${{ github.workspace }}:/workspace" alpine:3 \
sh -c 'rm -rf /workspace/..?* /workspace/.[!.]* /workspace/*' || true
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for setuptools-scm to determine version from tags
submodules: "recursive"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
# Needed by setuptools-rust to build the bundled native gRPC extension
# (rust/sglang-grpc) when `python -m build` builds the sglang wheel.
- name: Install protoc
run: sudo bash scripts/ci/utils/install_protoc.sh
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build wheel
env:
# When triggered manually, pin the wheel version to the supplied
# input (strip leading `v`) so setuptools-scm doesn't derive a
# `.devN+gHASH` version from the branch HEAD.
RELEASE_VERSION: ${{ inputs.version }}
run: |
cd python
cp ../README.md ../LICENSE .
pip install build wheel setuptools setuptools-scm setuptools-rust
if [ -n "$RELEASE_VERSION" ]; then
export SETUPTOOLS_SCM_PRETEND_VERSION="${RELEASE_VERSION#v}"
echo "Pinning wheel version to $SETUPTOOLS_SCM_PRETEND_VERSION"
fi
python3 -m build --wheel
# PyPI rejects plain `linux_x86_64` / `linux_aarch64` platform tags;
# auditwheel rewrites the wheel's platform tag to a `manylinux_*` tag
# and bundles any external native deps. The runner's glibc determines
# the lowest acceptable manylinux policy.
- name: Repair wheel for manylinux
run: |
cd python
pip install auditwheel patchelf
mkdir -p dist-repaired
python3 -m auditwheel repair dist/*.whl -w dist-repaired/
rm dist/*.whl
mv dist-repaired/*.whl dist/
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: wheel-py${{ matrix.python-version }}-${{ matrix.arch }}
path: python/dist/*.whl
publish:
needs: build
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
environment: "prod"
steps:
- name: Download all wheels
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: wheel-*
- name: List wheels
run: ls -lh dist/
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Upload to pypi
run: |
pip install twine
# --skip-existing makes partial reruns safe (e.g. one arch failed,
# rerun only the failed shard without re-uploading already-published wheels).
python3 -m twine upload --skip-existing dist/*.whl -u __token__ -p ${{ secrets.PYPI_TOKEN }}
+68
View File
@@ -0,0 +1,68 @@
name: Release Tag
# Creates a git tag to trigger release workflows (PyPI, Docker)
# Use this after testing on a release branch is complete
on:
workflow_dispatch:
inputs:
version:
description: 'Version to tag (without v prefix, e.g., 0.5.7)'
required: true
type: string
ref:
description: 'Branch or commit to tag (e.g., release/v0.5.7, main, or commit SHA)'
required: false
default: 'main'
type: string
permissions:
contents: write
jobs:
create-tag:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
environment: 'prod'
steps:
- name: Validate version format
run: |
VERSION="${{ github.event.inputs.version }}"
if [ -z "$VERSION" ]; then
echo "::error::Version is required"
exit 1
fi
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version format: $VERSION (expected: X.Y.Z or X.Y.Z.postN)"
exit 1
fi
echo "Version validated: v$VERSION"
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check if tag already exists
run: |
TAG="v${{ github.event.inputs.version }}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG already exists"
exit 1
fi
echo "Tag $TAG does not exist, proceeding..."
- name: Create and push tag
run: |
TAG="v${{ github.event.inputs.version }}"
REF="${{ github.event.inputs.ref }}"
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
echo "Creating tag $TAG on ref $REF (commit: $(git rev-parse HEAD))"
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
echo "::notice::Successfully created and pushed tag $TAG"
echo "This will trigger the release workflows (PyPI, Docker)"
+330
View File
@@ -0,0 +1,330 @@
name: Release sgl-deep-gemm
on:
workflow_dispatch:
inputs:
version:
description: "Wheel version (e.g. 0.1.0, 0.1.1rc0)"
type: string
required: true
target:
type: choice
description: "Build target (default: all)"
required: false
default: 'all'
options:
- 'all'
- 'cu129'
- 'cu130'
branch:
description: "DeepGEMM branch to build from (default: dev)"
type: string
required: false
default: 'dev'
concurrency:
group: release-sgl-deepgemm-${{ github.ref }}
cancel-in-progress: true
env:
# Must match TORCH_VER in docker/sgl-deep-gemm.Dockerfile — the wheel's
# pre-compiled _C.so links against this torch ABI.
TORCH_VER: "2.11.0"
jobs:
build-cu129-matrix:
if: |
github.repository == 'sgl-project/sglang' &&
(github.event.inputs.target == 'all' || github.event.inputs.target == 'cu129')
strategy:
matrix:
python-version: ["3.12"]
cuda-version: ["12.9"]
arch: [x86_64, aarch64]
include:
- arch: x86_64
runner: x64-kernel-build-node
- arch: aarch64
runner: arm-kernel-build-node
runs-on: ${{ matrix.runner }}
steps:
- name: Clean workspace (remove root-owned files from prior runs)
run: |
docker run --rm -v "${{ github.workspace }}:/workspace" alpine:3 \
sh -c 'rm -rf /workspace/..?* /workspace/.[!.]* /workspace/*' || true
- uses: actions/checkout@v4
- name: Checkout DeepGEMM
uses: actions/checkout@v4
with:
repository: sgl-project/DeepGEMM
ref: ${{ inputs.branch || 'dev' }}
path: DeepGEMM
submodules: recursive
- name: Set wheel version
run: |
echo -n "${{ inputs.version }}" > DeepGEMM/sgl_deep_gemm/VERSION
cat DeepGEMM/sgl_deep_gemm/VERSION
- name: Build wheel
run: |
chmod +x ./scripts/build_sgl_deep_gemm.sh ./scripts/rename_sgl_deep_gemm_whl.sh
./scripts/build_sgl_deep_gemm.sh "${{ matrix.python-version }}" "${{ matrix.cuda-version }}" "${{ github.workspace }}/DeepGEMM" "${{ matrix.arch }}"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: deepgemm-wheel-cuda${{ matrix.cuda-version }}-${{ matrix.arch }}
path: DeepGEMM/dist/*.whl
test-cu129:
needs: build-cu129-matrix
strategy:
fail-fast: false
matrix:
include:
- arch_label: sm90
runner: 8-gpu-h200
wheel_arch: x86_64
- arch_label: sm100
runner: 8-gpu-b200
wheel_arch: x86_64
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Checkout DeepGEMM
uses: actions/checkout@v4
with:
repository: sgl-project/DeepGEMM
ref: ${{ inputs.branch || 'dev' }}
path: DeepGEMM
submodules: recursive
- name: Download wheel
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: deepgemm-wheel-cuda12.9-${{ matrix.wheel_arch }}
- name: Install wheel and deps
env:
CU_TAG: cu129
run: |
python3 -m pip install --upgrade pip
# deep_gemm imports torch before loading _C.so, so torch must preload the
# libcudart the cu129 wheel links. Pin the CUDA-matched torch (see sgl-deep-gemm.Dockerfile).
python3 -m pip install "torch==${TORCH_VER}" --index-url "https://download.pytorch.org/whl/${CU_TAG}" --force-reinstall
python3 -m pip install numpy
python3 -m pip install dist/*.whl
python3 -c "import deep_gemm; print('deep_gemm:', deep_gemm.__file__)"
- name: Run DeepGEMM test suite
run: |
chmod +x "${{ github.workspace }}/DeepGEMM/sgl_deep_gemm/run_tests.sh"
"${{ github.workspace }}/DeepGEMM/sgl_deep_gemm/run_tests.sh"
release-cu129:
needs: [build-cu129-matrix, test-cu129]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: deepgemm-wheel-cuda12.9-*
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ inputs.version }}
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
files: |
dist/*
- name: Clone wheel index
run: git clone https://oauth2:${WHL_TOKEN}@github.com/sgl-project/whl.git sgl-whl
env:
WHL_TOKEN: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
- name: Update wheel index
run: python3 scripts/update_deepgemm_whl_index.py --cuda 129
- name: Push wheel index
run: |
cd sgl-whl
git config --local user.name "sglang-bot"
git config --local user.email "sglangbot@gmail.com"
git add -A
git commit -m "update sgl-deep-gemm whl index for v${{ inputs.version }}"
git push
build-cu130-matrix:
if: |
github.repository == 'sgl-project/sglang' &&
(github.event.inputs.target == 'all' || github.event.inputs.target == 'cu130')
strategy:
matrix:
python-version: ["3.12"]
cuda-version: ["13.0"]
arch: [x86_64, aarch64]
include:
- arch: x86_64
runner: x64-kernel-build-node
- arch: aarch64
runner: arm-kernel-build-node
runs-on: ${{ matrix.runner }}
steps:
- name: Clean workspace (remove root-owned files from prior runs)
run: |
docker run --rm -v "${{ github.workspace }}:/workspace" alpine:3 \
sh -c 'rm -rf /workspace/..?* /workspace/.[!.]* /workspace/*' || true
- uses: actions/checkout@v4
- name: Checkout DeepGEMM
uses: actions/checkout@v4
with:
repository: sgl-project/DeepGEMM
ref: ${{ inputs.branch || 'dev' }}
path: DeepGEMM
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set wheel version
run: |
echo -n "${{ inputs.version }}" > DeepGEMM/sgl_deep_gemm/VERSION
cat DeepGEMM/sgl_deep_gemm/VERSION
- name: Build wheel
run: |
chmod +x ./scripts/build_sgl_deep_gemm.sh ./scripts/rename_sgl_deep_gemm_whl.sh
./scripts/build_sgl_deep_gemm.sh "${{ matrix.python-version }}" "${{ matrix.cuda-version }}" "${{ github.workspace }}/DeepGEMM" "${{ matrix.arch }}"
# PyPI upload moved to release-cu130 so it is gated on test-cu130.
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: deepgemm-wheel-cuda${{ matrix.cuda-version }}-${{ matrix.arch }}
path: DeepGEMM/dist/*.whl
- name: Upload PyPI artifacts
uses: actions/upload-artifact@v4
with:
name: deepgemm-pypi-cuda${{ matrix.cuda-version }}-${{ matrix.arch }}
path: DeepGEMM/dist-pypi/*.whl
test-cu130:
needs: build-cu130-matrix
strategy:
fail-fast: false
matrix:
include:
- arch_label: sm90
runner: 8-gpu-h200
wheel_arch: x86_64
- arch_label: sm100
runner: 8-gpu-b200
wheel_arch: x86_64
- arch_label: sm100-aarch64
runner: 4-gpu-gb300
wheel_arch: aarch64
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Checkout DeepGEMM
uses: actions/checkout@v4
with:
repository: sgl-project/DeepGEMM
ref: ${{ inputs.branch || 'dev' }}
path: DeepGEMM
submodules: recursive
- name: Download wheel
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: deepgemm-wheel-cuda13.0-${{ matrix.wheel_arch }}
- name: Install wheel and deps
env:
CU_TAG: cu130
run: |
python3 -m pip install --upgrade pip
# deep_gemm imports torch before loading _C.so, so torch must preload the
# libcudart the cu130 wheel links. Pin the CUDA-matched torch (see sgl-deep-gemm.Dockerfile).
python3 -m pip install "torch==${TORCH_VER}" --index-url "https://download.pytorch.org/whl/${CU_TAG}" --force-reinstall
python3 -m pip install numpy
python3 -m pip install dist/*.whl
python3 -c "import deep_gemm; print('deep_gemm:', deep_gemm.__file__)"
- name: Run DeepGEMM test suite
run: |
chmod +x "${{ github.workspace }}/DeepGEMM/sgl_deep_gemm/run_tests.sh"
"${{ github.workspace }}/DeepGEMM/sgl_deep_gemm/run_tests.sh"
release-cu130:
needs: [build-cu130-matrix, test-cu130]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: deepgemm-wheel-cuda13.0-*
- name: Download PyPI artifacts
uses: actions/download-artifact@v4
with:
path: dist-pypi/
merge-multiple: true
pattern: deepgemm-pypi-cuda13.0-*
- name: Upload to PyPI
run: |
pip install --upgrade twine "packaging>=24.2"
python3 -m twine upload --skip-existing dist-pypi/* -u __token__ -p ${{ secrets.SGL_DEEP_GEMM_PYPI_TOKEN }}
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ inputs.version }}
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
files: |
dist/*
- name: Clone wheel index
run: git clone https://oauth2:${WHL_TOKEN}@github.com/sgl-project/whl.git sgl-whl
env:
WHL_TOKEN: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
- name: Update wheel index
run: python3 scripts/update_deepgemm_whl_index.py --cuda 130
- name: Push wheel index
run: |
cd sgl-whl
git config --local user.name "sglang-bot"
git config --local user.email "sglangbot@gmail.com"
git add -A
git commit -m "update sgl-deep-gemm whl index for v${{ inputs.version }}"
git push

Some files were not shown because too many files have changed in this diff Show More