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
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:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -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');
|
||||
@@ -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()
|
||||
@@ -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*'
|
||||
@@ -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/?$",
|
||||
]
|
||||
@@ -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(/|$)",
|
||||
]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 "========================================="
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -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 }}"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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}"
|
||||
@@ -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
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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.`
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 .
|
||||
@@ -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/
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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}"
|
||||
@@ -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}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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)"
|
||||
@@ -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
Reference in New Issue
Block a user