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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,331 @@
name: Build and Publish Multi-Arch Docker Images
# Reusable workflow: builds CUDA 12 + CUDA 13 images for amd64 and arm64,
# then creates multi-arch manifests with caller-specified tags.
on:
workflow_call:
inputs:
docker_target:
description: "Dockerfile target stage (framework or runtime)"
required: true
type: string
sgl_version:
description: "Version string passed as SGL_VERSION build arg (empty to skip)"
required: false
type: string
default: ""
extra_build_args:
description: "Additional --build-arg flags appended to docker buildx build"
required: false
type: string
default: ""
checkout_ref:
description: "Git ref to checkout (empty for default)"
required: false
type: string
default: ""
tag_config:
description: 'JSON array of {"cuda":"cu129|cu130","tags":["tag1","tag2"]}. Tags support {version} substitution.'
required: true
type: string
use_environment:
description: "GitHub environment name (e.g. prod) or empty for none"
required: false
type: string
default: ""
image_repo:
description: "Docker Hub repo to push to (e.g. lmsysorg/sglang-staging for testing)"
required: false
type: string
default: "lmsysorg/sglang"
jobs:
build-x86:
if: github.repository == 'sgl-project/sglang'
environment: ${{ inputs.use_environment || null }}
runs-on: x64-docker-build-node
env:
TAG_CONFIG: ${{ inputs.tag_config }}
SGL_VERSION: ${{ inputs.sgl_version }}
IMAGE_REPO: ${{ inputs.image_repo }}
outputs:
digest-cu129: ${{ steps.build-cu129.outputs.digest }}
digest-cu130: ${{ steps.build-cu130.outputs.digest }}
steps:
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache
- name: Cleanup workspace (remove root-owned files from prior runs)
run: sudo rm -rf "$GITHUB_WORKSPACE"/* || true
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Compute Docker build metadata args
run: |
set -euo pipefail
BUILD_COMMIT="$(git rev-parse HEAD)"
BUILD_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
for CUDA_VARIANT in cu129 cu130; do
python3 scripts/ci/utils/docker_build_metadata_args.py \
--cuda "${CUDA_VARIANT}" \
--tag-config "${TAG_CONFIG}" \
--image-repo "${IMAGE_REPO}" \
--sgl-version "${SGL_VERSION}" \
--build-commit "${BUILD_COMMIT}" \
--build-url "${BUILD_URL}" \
> "/tmp/docker-metadata-${CUDA_VARIANT}.args"
done
- name: Free disk space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
docker-images: true
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: Prune Docker to reclaim disk space
run: |
docker buildx prune --filter "until=72h" -f
docker system prune -af --filter "until=72h"
docker volume prune -af
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push AMD64 image (CUDA 12)
id: build-cu129
run: |
VERSION_ARG=""
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu129.args
docker buildx build \
--target ${{ inputs.docker_target }} \
--platform linux/amd64 \
--output type=image,name=${{ inputs.image_repo }},push-by-digest=true,name-canonical=true,push=true \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=12.9.1 \
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=0 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu129.json \
--no-cache \
.
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata-cu129.json'))['containerimage.digest'])")
echo "Pushed digest: ${DIGEST}"
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
- name: Build and push AMD64 image (CUDA 13)
id: build-cu130
run: |
VERSION_ARG=""
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu130.args
docker buildx build \
--target ${{ inputs.docker_target }} \
--platform linux/amd64 \
--output type=image,name=${{ inputs.image_repo }},push-by-digest=true,name-canonical=true,push=true \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=13.0.1 \
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=0 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu130.json \
--no-cache \
.
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata-cu130.json'))['containerimage.digest'])")
echo "Pushed digest: ${DIGEST}"
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
build-arm64:
if: github.repository == 'sgl-project/sglang'
environment: ${{ inputs.use_environment || null }}
runs-on: arm-docker-build-node
env:
TAG_CONFIG: ${{ inputs.tag_config }}
SGL_VERSION: ${{ inputs.sgl_version }}
IMAGE_REPO: ${{ inputs.image_repo }}
outputs:
digest-cu129: ${{ steps.build-cu129.outputs.digest }}
digest-cu130: ${{ steps.build-cu130.outputs.digest }}
steps:
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache
- name: Cleanup workspace (remove root-owned files from prior runs)
run: sudo rm -rf "$GITHUB_WORKSPACE"/* || true
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Compute Docker build metadata args
run: |
set -euo pipefail
BUILD_COMMIT="$(git rev-parse HEAD)"
BUILD_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
for CUDA_VARIANT in cu129 cu130; do
python3 scripts/ci/utils/docker_build_metadata_args.py \
--cuda "${CUDA_VARIANT}" \
--tag-config "${TAG_CONFIG}" \
--image-repo "${IMAGE_REPO}" \
--sgl-version "${SGL_VERSION}" \
--build-commit "${BUILD_COMMIT}" \
--build-url "${BUILD_URL}" \
> "/tmp/docker-metadata-${CUDA_VARIANT}.args"
done
- name: Prune Docker to reclaim disk space
run: |
docker buildx prune --filter "until=72h" -f
docker system prune -af --filter "until=72h"
docker volume prune -af
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push ARM64 image (CUDA 12)
id: build-cu129
run: |
VERSION_ARG=""
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu129.args
docker buildx build \
--target ${{ inputs.docker_target }} \
--platform linux/arm64 \
--output type=image,name=${{ inputs.image_repo }},push-by-digest=true,name-canonical=true,push=true \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=12.9.1 \
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=1 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu129.json \
--no-cache \
.
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata-cu129.json'))['containerimage.digest'])")
echo "Pushed digest: ${DIGEST}"
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
- name: Build and push ARM64 image (CUDA 13)
id: build-cu130
run: |
VERSION_ARG=""
if [ -n "${SGL_VERSION}" ]; then
VERSION_ARG="--build-arg SGL_VERSION=${SGL_VERSION}"
fi
mapfile -t METADATA_ARGS < /tmp/docker-metadata-cu130.args
docker buildx build \
--target ${{ inputs.docker_target }} \
--platform linux/arm64 \
--output type=image,name=${{ inputs.image_repo }},push-by-digest=true,name-canonical=true,push=true \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=13.0.1 \
--build-arg BUILD_TYPE=all \
--build-arg GRACE_BLACKWELL=1 \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
"${METADATA_ARGS[@]}" \
${VERSION_ARG} \
${{ inputs.extra_build_args }} \
--metadata-file /tmp/metadata-cu130.json \
--no-cache \
.
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata-cu130.json'))['containerimage.digest'])")
echo "Pushed digest: ${DIGEST}"
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
create-manifests:
runs-on: ubuntu-latest
needs: [build-x86, build-arm64]
if: github.repository == 'sgl-project/sglang'
environment: ${{ inputs.use_environment || null }}
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create multi-arch manifests
env:
TAG_CONFIG: ${{ inputs.tag_config }}
SGL_VERSION: ${{ inputs.sgl_version }}
IMAGE_REPO: ${{ inputs.image_repo }}
X86_CU129: ${{ needs.build-x86.outputs.digest-cu129 }}
X86_CU130: ${{ needs.build-x86.outputs.digest-cu130 }}
ARM64_CU129: ${{ needs.build-arm64.outputs.digest-cu129 }}
ARM64_CU130: ${{ needs.build-arm64.outputs.digest-cu130 }}
SHORT_SHA: ${{ github.sha }}
run: |
echo "${TAG_CONFIG}" | jq -c '.[]' | while read -r entry; do
CUDA=$(echo "${entry}" | jq -r '.cuda')
if [ "${CUDA}" = "cu129" ]; then
X86_DIGEST="${X86_CU129}"
ARM64_DIGEST="${ARM64_CU129}"
else
X86_DIGEST="${X86_CU130}"
ARM64_DIGEST="${ARM64_CU130}"
fi
TAG_ARGS=""
for tag in $(echo "${entry}" | jq -r '.tags[]'); do
# Substitute template variables
tag=$(echo "${tag}" | sed "s/{version}/${SGL_VERSION}/g")
tag=$(echo "${tag}" | sed "s/{date}/$(date +%Y%m%d)/g")
tag=$(echo "${tag}" | sed "s/{short_sha}/${SHORT_SHA:0:8}/g")
TAG_ARGS="${TAG_ARGS} -t ${IMAGE_REPO}:${tag}"
done
docker buildx imagetools create \
${TAG_ARGS} \
${IMAGE_REPO}@${X86_DIGEST} \
${IMAGE_REPO}@${ARM64_DIGEST}
echo "Published:${TAG_ARGS}"
done
@@ -0,0 +1,78 @@
name: Cleanup Old Nightly Docker Tags
# Reusable workflow: deletes old nightly Docker Hub tags, keeping the most recent ones.
# Can also be triggered manually to clean up tags on demand.
on:
workflow_call:
inputs:
tag_prefixes:
description: 'JSON array of tag prefixes to clean up (e.g. ["nightly-dev", "nightly-dev-cu13"])'
required: true
type: string
keep_count:
description: "Number of most recent tags to keep per prefix"
required: false
type: number
default: 14
image_repo:
description: "Docker Hub repo to clean up (e.g. lmsysorg/sglang-staging for testing)"
required: false
type: string
default: "lmsysorg/sglang"
workflow_dispatch:
inputs:
tag_prefixes:
description: 'JSON array of tag prefixes to clean up (e.g. ["nightly-dev", "nightly-dev-cu13"])'
required: true
type: string
keep_count:
description: "Number of most recent tags to keep per prefix"
required: false
type: number
default: 14
image_repo:
description: "Docker Hub repo to clean up (e.g. lmsysorg/sglang-staging for testing)"
required: false
type: string
default: "lmsysorg/sglang"
jobs:
cleanup:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
steps:
- name: Cleanup old nightly tags
env:
TAG_PREFIXES: ${{ inputs.tag_prefixes }}
KEEP_COUNT: ${{ inputs.keep_count }}
IMAGE_REPO: ${{ inputs.image_repo }}
run: |
TOKEN=$(curl -s -H "Content-Type: application/json" \
-X POST -d '{"username": "${{ secrets.DOCKERHUB_USERNAME }}", "password": "${{ secrets.DOCKERHUB_TOKEN }}"}' \
https://hub.docker.com/v2/users/login/ | jq -r .token)
TAGS_RESPONSE=$(curl -s -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/${IMAGE_REPO}/tags/?page_size=100")
echo "${TAG_PREFIXES}" | jq -r '.[]' | while read -r PREFIX; do
echo "--- Checking prefix: ${PREFIX} ---"
TAGS=$(echo "$TAGS_RESPONSE" | jq -r \
--arg prefix "${PREFIX}" \
'.results[] | select(.name | test("^\($prefix)-[0-9]")) | "\(.last_updated)|\(.name)"' \
| sort -r | cut -d'|' -f2)
TAG_COUNT=$(echo "$TAGS" | grep -c . || true)
if [ "$TAG_COUNT" -gt "$KEEP_COUNT" ]; then
echo "Found $TAG_COUNT tags, keeping only the $KEEP_COUNT most recent"
TAGS_TO_DELETE=$(echo "$TAGS" | tail -n +$((KEEP_COUNT + 1)))
for tag in $TAGS_TO_DELETE; do
echo "Deleting tag: $tag"
curl -X DELETE -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/${IMAGE_REPO}/tags/$tag/"
done
else
echo "Only $TAG_COUNT tags found, no cleanup needed"
fi
done
@@ -0,0 +1,223 @@
name: Check Changes
on:
workflow_call:
inputs:
git_ref:
type: string
default: ''
run_all_tests:
type: boolean
default: false
force_continue_on_error:
type: boolean
default: false
pr_test_yml:
description: 'Workflow YAML whose stage `run_timeout_minutes` drives partition sizing.'
type: string
default: '.github/workflows/pr-test.yml'
outputs:
main_package:
value: ${{ jobs.run.outputs.main_package }}
sgl_kernel:
value: ${{ jobs.run.outputs.sgl_kernel }}
jit_kernel:
value: ${{ jobs.run.outputs.jit_kernel }}
multimodal_gen:
value: ${{ jobs.run.outputs.multimodal_gen }}
partitions:
value: ${{ jobs.run.outputs.partitions }}
partition_model_sha:
value: ${{ jobs.run.outputs.partition_model_sha }}
runs_on_map:
value: ${{ jobs.run.outputs.runs_on_map }}
enable_retry:
value: ${{ jobs.run.outputs.enable_retry }}
continue_on_error:
value: ${{ jobs.run.outputs.continue_on_error }}
jobs:
run:
name: check-changes
runs-on: ubuntu-latest
outputs:
main_package: ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests }}
sgl_kernel: ${{ steps.filter.outputs.sgl_kernel }}
jit_kernel: ${{ steps.filter.outputs.jit_kernel || steps.run-mode.outputs.run_all_tests }}
multimodal_gen: ${{ steps.filter.outputs.multimodal_gen || steps.run-mode.outputs.run_all_tests }}
partitions: ${{ steps.partitions.outputs.partitions }}
partition_model_sha: ${{ steps.partition-model-sha.outputs.sha }}
runs_on_map: ${{ steps.runner-map.outputs.runs_on_map }}
enable_retry: ${{ steps.set-retry.outputs.enable_retry }}
continue_on_error: ${{ steps.set-continue-on-error.outputs.continue_on_error }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-maintenance
- name: Determine run mode
id: run-mode
run: |
# Run all tests for scheduled runs and workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.git_ref
if [[ "${{ github.event_name }}" == "schedule" || "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (schedule=${{ github.event_name == 'schedule' }}, run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- ".github/workflows/pr-test.yml"
- ".github/workflows/pr-gate.yml"
- ".github/actions/**"
- "python/pyproject.toml"
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "scripts/ci/cuda/*"
- "scripts/ci/utils/*"
- "test/**/!(*.md)"
multimodal_gen:
- ".github/workflows/pr-test.yml"
- ".github/workflows/pr-test-multimodal-gen.yml"
- "python/pyproject.toml"
- "python/sglang/multimodal_gen/**/!(*.md|*.ipynb)"
- "python/sglang/srt/model_executor/runner_backend_utils/breakable_cuda_graph/**"
- "python/sglang/srt/observability/**"
- "python/sglang/jit_kernel/**"
- "test/registered/jit/diffusion/**"
- "test/registered/jit/benchmark/diffusion/**"
- "python/sglang/cli/**"
jit_kernel:
- ".github/workflows/pr-test.yml"
- ".github/workflows/pr-test-jit-kernel.yml"
- "python/pyproject.toml"
- "python/sglang/jit_kernel/**"
- "test/registered/jit/**"
sgl_kernel:
# Intentionally excludes ".github/workflows/pr-test-sgl-kernel.yml" —
# see API-side detector below for rationale.
- "sgl-kernel/**/!(*.md|THIRDPARTYNOTICES.txt|LICENSE)"
- name: Determine full-parallel mode
id: parallel-mode
run: |
# `full=true` lifts the matrix-fanout throttle so each suite's
# max_parallel = size. Conditions:
# 1. Scheduled cron run.
# 2. run_all_tests run (manual full dispatch / release) -- a full
# run should mirror the cron's parallelism, not just its test set.
# 3. pull_request event with the `high priority` label.
FULL=false
if [[ "${{ github.event_name }}" == "schedule" ]]; then
FULL=true
echo "Scheduled run -> full parallelism"
elif [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
FULL=true
echo "run_all_tests -> full parallelism"
elif [[ "${{ github.event_name }}" == "pull_request" && "${{ contains(github.event.pull_request.labels.*.name, 'high priority') }}" == "true" ]]; then
FULL=true
echo "high priority PR -> full parallelism"
fi
echo "full=$FULL" >> "$GITHUB_OUTPUT"
- name: Resolve sglang-ci-stats SHA
id: partition-model-sha
env:
GH_TOKEN: ${{ github.token }}
run: |
# Pin all shards to one immutable commit so dispatch and every
# runtime LPT use the same model snapshot. Soft fail -> static.
SHA=$(gh api repos/sgl-project/sglang-ci-stats/commits/main --jq '.sha' 2>/dev/null || true)
if [[ -n "$SHA" ]]; then
echo "Pinned sglang-ci-stats@$SHA"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
else
echo "::warning::Could not resolve sglang-ci-stats SHA; using in-source est_time"
echo "sha=" >> "$GITHUB_OUTPUT"
fi
- name: Fetch live partition model
if: steps.partition-model-sha.outputs.sha != ''
run: |
# SHA resolved -> require fetch (curl --retry). Soft fallback
# would risk cross-shard LPT divergence on transient curl flake.
rm -f /tmp/partition-model.json
URL="https://raw.githubusercontent.com/sgl-project/sglang-ci-stats/${{ steps.partition-model-sha.outputs.sha }}/model.json"
curl --fail --silent --show-error --max-time 15 --retry 3 --retry-delay 2 \
"$URL" -o /tmp/partition-model.json
echo "Fetched partition-model.json ($(wc -c < /tmp/partition-model.json) bytes)"
- name: Compute partitions
id: partitions
run: |
# Emit a single JSON output `partitions` keyed by suite name with
# {size, arr, max_parallel} fields per suite. Replaces the prior
# full/low max-parallel presets; `--full-parallel` keeps the
# `high priority` PR / scheduled cron escape hatch.
# See scripts/ci/utils/compute_partitions.py.
python3 scripts/ci/utils/compute_partitions.py \
--full-parallel ${{ steps.parallel-mode.outputs.full }} \
--partition-model-file /tmp/partition-model.json \
--pr-test-yml ${{ inputs.pr_test_yml }} \
>> "$GITHUB_OUTPUT"
- name: Set B200 runner tag
id: set-runner
run: |
# Use kernel-build runner only when sgl_kernel changes are detected.
sgl_kernel="${{ steps.filter.outputs.sgl_kernel }}"
if [[ "$sgl_kernel" == "true" ]]; then
echo "b200_runner=4-gpu-b200-kernel" >> $GITHUB_OUTPUT
else
echo "b200_runner=4-gpu-b200" >> $GITHUB_OUTPUT
fi
- name: Build runs_on_map (resolves $b200_runner sentinel)
id: runner-map
run: |
python3 scripts/ci/runner_configs.py --map \
'${{ steps.set-runner.outputs.b200_runner }}' \
>> "$GITHUB_OUTPUT"
- name: Enable retry for CI
id: set-retry
run: |
echo "enable_retry=true" >> $GITHUB_OUTPUT
echo "Retry logic enabled for CI"
- name: Set continue-on-error for full test runs
id: set-continue-on-error
run: |
if [[ "${{ steps.run-mode.outputs.run_all_tests }}" == "true" || "${{ inputs.force_continue_on_error }}" == "true" ]]; then
echo "continue_on_error=true" >> $GITHUB_OUTPUT
echo "Full test run or force flag detected, enabling continue-on-error to run all tests"
else
echo "continue_on_error=false" >> $GITHUB_OUTPUT
echo "Filtered run, continue-on-error disabled"
fi
- name: Show filter results in summary (table)
run: |
{
echo "## Change Detection"
echo ""
echo "| Component | Changed |"
echo "|-------------------|---------|"
echo "| main_package | ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests }} |"
echo "| sgl_kernel | ${{ steps.filter.outputs.sgl_kernel }} |"
echo "| jit_kernel | ${{ steps.filter.outputs.jit_kernel || steps.run-mode.outputs.run_all_tests }} |"
echo "| multimodal_gen | ${{ steps.filter.outputs.multimodal_gen || steps.run-mode.outputs.run_all_tests }} |"
echo "| b200_runner | ${{ steps.set-runner.outputs.b200_runner }} |"
echo "| enable_retry | ${{ steps.set-retry.outputs.enable_retry }} |"
echo "| continue_on_error | ${{ steps.set-continue-on-error.outputs.continue_on_error }} |"
} >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,118 @@
name: PR Test - Build sgl-kernel Wheels
# Reusable workflow that builds the sgl-kernel CUDA wheels for one
# architecture / runner pool. Called twice from pr-test.yml: once for the
# x86_64 runner pool, once for arm64. All architecture-specific values
# (runner label, display name, artifact name suffix) are passed in via
# inputs.
on:
workflow_call:
inputs:
runs_on:
description: 'Runner label (e.g. x64-kernel-build-node, arm-kernel-build-node)'
type: string
required: true
job_display_name:
description: 'Display name shown in the Actions UI (e.g. "Build Wheel", "Build Wheel Arm")'
type: string
default: 'Build Wheel'
arch_suffix:
description: "Suffix appended to the upload-artifact name. '' for x86, '-aarch64' for arm."
type: string
default: ''
git_ref:
type: string
default: ''
skip_pr_test_health_check:
description: 'Forwarded from the caller workflow input of the same name. Not consumed inside this reusable workflow today, but mirrored so the resulting env context matches the inline-job baseline.'
type: boolean
default: false
# Mirror the caller workflow's top-level env block. GitHub Actions does NOT
# automatically inherit workflow-level env vars across the workflow_call
# boundary, so anything pr-test.yml defines must be redeclared here for the
# reusable jobs to see the same context. Most of these are not read by the
# wheel-build path, but PR_TEST_BYPASS_MAINTENANCE_ON_MAIN is consumed by the
# check-maintenance composite action and would otherwise be unset.
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
SKIP_PR_TEST_HEALTH_CHECK: ${{ inputs.skip_pr_test_health_check && 'true' || 'false' }}
FORCE_REBUILD_DEEPEP: '1'
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
USE_VENV: false
jobs:
build:
runs-on: ${{ inputs.runs_on }}
timeout-minutes: 240
strategy:
matrix:
include:
- python-version: "3.10"
cuda-version: "13.0"
name: ${{ inputs.job_display_name }}
steps:
- name: Cleanup
run: |
if [ -d "$GITHUB_WORKSPACE" ]; then
sudo rm -rf "$GITHUB_WORKSPACE"/* || true
else
echo "$GITHUB_WORKSPACE does not exist, nothing to clean"
fi
- uses: actions/checkout@v4
with:
submodules: "recursive"
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-maintenance
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Free Docker disk space
run: |
set -x
# `-f` only (no `-a`): drops dangling <none>:<none> layers from
# prior runs but keeps the tagged sgl-kernel-deps:* base images.
docker image prune -f
# Reap orphan buildx builders from interrupted prior runs. Must
# `buildx rm` (not `volume rm`) — the volume is held open by the
# buildkit helper container; removing the builder frees both.
# `sgl-kernel-builder` and `default` are skipped on purpose.
for b in $(docker buildx ls --format '{{.Name}}' | grep -E '^builder-' || true); do
echo "Removing orphan buildx builder: $b"
docker buildx rm "$b" || true
done
# Trim the persistent local buildx cache (--cache-to mode=max grows
# unbounded). Cache-hit imports rewrite the blob, so mtime tracks
# "last carried forward by a build" — 14d is well past the freshest
# CI cycle.
if [ -d "$HOME/.cache/sgl-kernel/buildx/blobs" ]; then
find "$HOME/.cache/sgl-kernel/buildx/blobs" -type f -mtime +14 -delete
fi
df -h /
- name: Build wheel for Python ${{ matrix.python-version }} and CUDA ${{ matrix.cuda-version }}
run: |
cd sgl-kernel
./build.sh "${{ matrix.python-version }}" "${{ matrix.cuda-version }}"
env:
USE_CCACHE: 1
- name: Verify wheel artifacts
run: |
ls -alh sgl-kernel/dist
ls -alh sgl-kernel/dist/*.whl
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: wheel-python${{ matrix.python-version }}-cuda${{ matrix.cuda-version }}${{ inputs.arch_suffix }}
path: sgl-kernel/dist/*
if-no-files-found: error
+184
View File
@@ -0,0 +1,184 @@
name: PR Test Stage
# Reusable workflow for one CUDA test stage. Caller pr-test.yml forwards
# `check_changes` = toJson(needs.check-changes.outputs) and
# `caller_inputs` = toJson(inputs) as bundles. `partitions` is forwarded
# separately to keep matrix expressions single-fromJson — its value is itself
# a JSON string, so reading it via `check_changes` would need double fromJson,
# and matrix can't use step-resolved values. base-a-test-cpu stays inline in
# pr-test.yml (bespoke uv pip / protoc / rust-cache install).
on:
workflow_call:
inputs:
self_name:
description: 'Caller job key; used for partitions[suite] lookup.'
type: string
required: true
runner_config:
description: 'Key in scripts/ci/runner_configs.yml. Resolves install script, artifact version, install timeout, runs-on label, and rdma_devices.'
type: string
required: true
check_changes:
description: 'toJson(needs.check-changes.outputs). Read via fromJson(...).main_package / sgl_kernel / continue_on_error / runs_on_map etc.'
type: string
required: true
caller_inputs:
description: 'toJson(inputs) from pr-test.yml. Read via fromJson(...).git_ref / skip_pr_test_health_check / test_parallel_dispatch.'
type: string
required: true
partitions:
description: 'check-changes.outputs.partitions raw — kept separate to avoid double-fromJson in matrix expressions.'
type: string
required: true
run_timeout_minutes:
description: 'timeout-minutes for the Run test step. Required so compute_partitions.py can read it from pr-test.yml without a duplicated default constant.'
type: string
required: true
timeout_per_file:
description: 'run_suite.py --timeout-per-file value (empty = unset).'
type: string
default: ''
warmup_deep_gemm_models:
description: 'Space-separated `model:gpus` list; empty skips DeepGEMM warmup.'
type: string
default: ''
warmup_server_models:
description: 'Space-separated `model:gpus` list; empty skips server-CUDA-graph warmup.'
type: string
default: ''
warmup_timeout_minutes:
description: 'timeout-minutes for each warmup step. Bump for stages with large cold-cache model lists.'
type: string
default: '25'
extra_pytest_path:
description: 'pytest path to run after the suite (b200 FA4 jit_kernel tests). Empty skips.'
type: string
default: ''
# Mirror pr-test.yml top-level env. Reusable workflows do NOT inherit caller's
# workflow-level env across the workflow_call boundary, so anything pr-test.yml
# defines must be redeclared here for the called job to see the same context.
env:
SGLANG_IS_IN_CI: true
# base-a runs the fwd-occupancy sanity kit; async-assert probes add GPU work
# and skew the occupancy measurement, so keep them off in that stage.
SGLANG_ENABLE_ASYNC_ASSERT: ${{ startsWith(inputs.self_name, 'base-a') && 'false' || 'true' }}
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
SKIP_PR_TEST_HEALTH_CHECK: ${{ (fromJson(inputs.caller_inputs).skip_pr_test_health_check || fromJson(inputs.caller_inputs).test_parallel_dispatch || fromJson(inputs.caller_inputs).run_all_tests) && 'true' || 'false' }}
FORCE_REBUILD_DEEPEP: '1'
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
USE_VENV: false
jobs:
run:
name: ${{ inputs.self_name }} (${{ matrix.partition }})
# Runs on schedule / parallel-dispatch / non-failed PR with main_package or sgl_kernel changes.
if: |
always() &&
((github.event_name == 'schedule' || fromJson(inputs.caller_inputs).test_parallel_dispatch == true) || (!failure() && !cancelled())) &&
(fromJson(inputs.check_changes).main_package == 'true' || fromJson(inputs.check_changes).sgl_kernel == 'true')
# runs-on resolved from runs_on_map; check-changes already substituted
# $b200_runner (see runner_configs.py --map). rdma_devices is exported
# below in a setup step via $GITHUB_ENV.
runs-on: ${{ fromJson(fromJson(inputs.check_changes).runs_on_map)[inputs.runner_config] }}
timeout-minutes: 240
strategy:
fail-fast: false
max-parallel: ${{ fromJson(inputs.partitions)[inputs.self_name].max_parallel }}
matrix:
partition: ${{ fromJson(inputs.partitions)[inputs.self_name].arr }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ fromJson(inputs.caller_inputs).git_ref || github.sha }}
- name: Resolve runner_config
id: rc
run: python3 scripts/ci/runner_configs.py '${{ inputs.runner_config }}' >> "$GITHUB_OUTPUT"
- name: Export rdma_devices to job env
run: echo "SGLANG_CI_RDMA_ALL_DEVICES=${{ steps.rc.outputs.rdma_devices || '' }}" >> "$GITHUB_ENV"
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts (v4)
if: ${{ fromJson(inputs.check_changes).sgl_kernel == 'true' && steps.rc.outputs.artifact_version == 'v4' }}
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Download artifacts (v6)
if: ${{ fromJson(inputs.check_changes).sgl_kernel == 'true' && steps.rc.outputs.artifact_version == 'v6' }}
uses: actions/download-artifact@v6
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: ${{ fromJson(steps.rc.outputs.install_timeout) }}
env:
GRACE_BLACKWELL: ${{ steps.rc.outputs.grace_blackwell || '0' }}
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ fromJson(inputs.check_changes).sgl_kernel }} bash ${{ steps.rc.outputs.install }}
- name: Warmup DeepGEMM JIT Compilation
if: inputs.warmup_deep_gemm_models != ''
timeout-minutes: ${{ fromJson(inputs.warmup_timeout_minutes) }}
run: |
# Activate venv if available (GITHUB_ENV may have failed to propagate)
[ -f "${SGLANG_CI_VENV_PATH:-/dev/null}/bin/activate" ] && source "${SGLANG_CI_VENV_PATH}/bin/activate"
[ -f "${SGLANG_CI_VENV_PATH:-/dev/null}/env.sh" ] && source "${SGLANG_CI_VENV_PATH}/env.sh"
python3 scripts/ci/cuda/warmup_deep_gemm.py ${{ inputs.warmup_deep_gemm_models }}
- name: Warmup Server CUDA Graphs
if: inputs.warmup_server_models != ''
timeout-minutes: ${{ fromJson(inputs.warmup_timeout_minutes) }}
run: |
[ -f "${SGLANG_CI_VENV_PATH:-/dev/null}/bin/activate" ] && source "${SGLANG_CI_VENV_PATH}/bin/activate"
[ -f "${SGLANG_CI_VENV_PATH:-/dev/null}/env.sh" ] && source "${SGLANG_CI_VENV_PATH}/env.sh"
python3 scripts/ci/cuda/warmup_server.py ${{ inputs.warmup_server_models }}
- name: Fetch live partition model
if: fromJson(inputs.check_changes).partition_model_sha != ''
run: |
# SHA resolved by check-changes -> require fetch (curl --retry)
# so all shards stay on the same snapshot.
rm -f /tmp/partition-model.json
URL="https://raw.githubusercontent.com/sgl-project/sglang-ci-stats/${{ fromJson(inputs.check_changes).partition_model_sha }}/model.json"
curl --fail --silent --show-error --max-time 15 --retry 3 --retry-delay 2 \
"$URL" -o /tmp/partition-model.json
- name: Run test
timeout-minutes: ${{ fromJson(inputs.run_timeout_minutes) }}
env:
CONTINUE_ON_ERROR_FLAG: ${{ fromJson(inputs.check_changes).continue_on_error == 'true' && '--continue-on-error' || '' }}
run: |
cd test
python3 run_suite.py --hw cuda --suite ${{ inputs.self_name }} \
--auto-partition-id ${{ matrix.partition }} \
--auto-partition-size ${{ fromJson(inputs.partitions)[inputs.self_name].size }} \
--partition-model-file /tmp/partition-model.json \
${{ inputs.timeout_per_file && format('--timeout-per-file {0}', inputs.timeout_per_file) || '' }} \
$CONTINUE_ON_ERROR_FLAG
- name: Run extra pytest
if: inputs.extra_pytest_path != ''
timeout-minutes: 10
run: python3 -m pytest -q ${{ inputs.extra_pytest_path }}
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.partition }}
tracker-issue: "26340"
bot-token: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
- name: Cleanup venv
if: always()
run: bash scripts/ci/cuda/ci_cleanup_venv.sh
+170
View File
@@ -0,0 +1,170 @@
name: AMD AITER Scout
on:
schedule:
- cron: '0 20 * * 1' # Monday 20:00 UTC
# - cron: '0 20 * * 4' # Thursday 20:00 UTC (temporarily disabled)
workflow_dispatch:
inputs:
aiter_ref:
description: 'AITER git ref (branch, tag, or SHA). Default: main (latest commit)'
required: false
type: string
default: 'main'
job_filter:
description: 'Comma-separated workflows to run: nightly-amd, nightly-amd-rocm720, pr-test-amd, pr-test-amd-rocm720. Default: all'
required: false
type: string
default: 'all'
continue_on_error:
description: 'Continue running other workflows even if one fails'
required: false
type: boolean
default: true
permissions:
actions: write
contents: read
issues: read
pull-requests: read
concurrency:
group: amd-aiter-scout-${{ github.run_id }}
cancel-in-progress: true
jobs:
resolve-aiter:
runs-on: ubuntu-latest
outputs:
aiter_sha: ${{ steps.resolve.outputs.sha }}
run_nightly_amd: ${{ steps.parse.outputs.run_nightly_amd }}
run_nightly_amd_rocm720: ${{ steps.parse.outputs.run_nightly_amd_rocm720 }}
run_pr_test_amd: ${{ steps.parse.outputs.run_pr_test_amd }}
run_pr_test_amd_rocm720: ${{ steps.parse.outputs.run_pr_test_amd_rocm720 }}
steps:
- name: Resolve AITER commit
id: resolve
run: |
REF="${{ inputs.aiter_ref || 'main' }}"
echo "Resolving AITER ref: ${REF}"
SHA=$(git ls-remote https://github.com/ROCm/aiter.git "refs/heads/${REF}" | head -1 | cut -f1)
if [ -z "$SHA" ]; then
SHA=$(git ls-remote https://github.com/ROCm/aiter.git "refs/tags/${REF}" | head -1 | cut -f1)
fi
if [ -z "$SHA" ]; then
SHA=$(git ls-remote https://github.com/ROCm/aiter.git "${REF}" | head -1 | cut -f1)
fi
if [ -z "$SHA" ]; then
SHA="${REF}"
fi
echo "sha=${SHA}" >> $GITHUB_OUTPUT
echo "### AITER Ref Resolution" >> $GITHUB_STEP_SUMMARY
echo "- **Requested ref:** \`${REF}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Resolved SHA:** \`${SHA}\`" >> $GITHUB_STEP_SUMMARY
echo "- **AITER commit:** https://github.com/ROCm/aiter/commit/${SHA}" >> $GITHUB_STEP_SUMMARY
- name: Parse job filter
id: parse
run: |
FILTER="${{ inputs.job_filter || 'all' }}"
echo "Job filter: ${FILTER}"
if [[ "$FILTER" == "all" ]]; then
echo "run_nightly_amd=true" >> $GITHUB_OUTPUT
echo "run_nightly_amd_rocm720=true" >> $GITHUB_OUTPUT
echo "run_pr_test_amd=true" >> $GITHUB_OUTPUT
echo "run_pr_test_amd_rocm720=true" >> $GITHUB_OUTPUT
else
# Wrap with commas for exact substring matching (avoids "nightly-amd" matching "nightly-amd-rocm720")
PADDED=",${FILTER// /},"
echo "run_nightly_amd=$(echo "$PADDED" | grep -q ',nightly-amd,' && echo true || echo false)" >> $GITHUB_OUTPUT
echo "run_nightly_amd_rocm720=$(echo "$PADDED" | grep -q ',nightly-amd-rocm720,' && echo true || echo false)" >> $GITHUB_OUTPUT
echo "run_pr_test_amd=$(echo "$PADDED" | grep -q ',pr-test-amd,' && echo true || echo false)" >> $GITHUB_OUTPUT
echo "run_pr_test_amd_rocm720=$(echo "$PADDED" | grep -q ',pr-test-amd-rocm720,' && echo true || echo false)" >> $GITHUB_OUTPUT
fi
echo "### Job Filter" >> $GITHUB_STEP_SUMMARY
echo "- **Filter:** \`${FILTER}\`" >> $GITHUB_STEP_SUMMARY
call-nightly-amd:
if: needs.resolve-aiter.outputs.run_nightly_amd == 'true'
needs: resolve-aiter
uses: ./.github/workflows/nightly-test-amd.yml
secrets: inherit
with:
ref: amd/aiter-ci
aiter_ref: ${{ needs.resolve-aiter.outputs.aiter_sha }}
job_filter: 'all'
continue_on_error: ${{ inputs.continue_on_error == '' && true || inputs.continue_on_error }}
call-nightly-amd-rocm720:
if: needs.resolve-aiter.outputs.run_nightly_amd_rocm720 == 'true'
needs: resolve-aiter
uses: ./.github/workflows/nightly-test-amd-rocm720.yml
secrets: inherit
with:
ref: amd/aiter-ci
aiter_ref: ${{ needs.resolve-aiter.outputs.aiter_sha }}
job_filter: 'all'
continue_on_error: ${{ inputs.continue_on_error == '' && true || inputs.continue_on_error }}
call-pr-test-amd:
if: needs.resolve-aiter.outputs.run_pr_test_amd == 'true'
needs: resolve-aiter
uses: ./.github/workflows/pr-test-amd.yml
secrets: inherit
with:
ref: amd/aiter-ci
run_all_tests: true
aiter_ref: ${{ needs.resolve-aiter.outputs.aiter_sha }}
continue_on_error: ${{ inputs.continue_on_error == '' && true || inputs.continue_on_error }}
call-pr-test-amd-rocm720:
if: needs.resolve-aiter.outputs.run_pr_test_amd_rocm720 == 'true'
needs: resolve-aiter
uses: ./.github/workflows/pr-test-amd-rocm720.yml
secrets: inherit
with:
ref: amd/aiter-ci
run_all_tests: true
aiter_ref: ${{ needs.resolve-aiter.outputs.aiter_sha }}
continue_on_error: ${{ inputs.continue_on_error == '' && true || inputs.continue_on_error }}
check-all-jobs:
if: always()
needs:
- resolve-aiter
- call-nightly-amd
- call-nightly-amd-rocm720
- call-pr-test-amd
- call-pr-test-amd-rocm720
runs-on: ubuntu-latest
steps:
- name: Summary
run: |
echo "## AMD AITER Scout Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **SGLang ref:** \`amd/aiter-ci\`" >> $GITHUB_STEP_SUMMARY
echo "- **AITER SHA:** \`${{ needs.resolve-aiter.outputs.aiter_sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **AITER commit:** https://github.com/ROCm/aiter/commit/${{ needs.resolve-aiter.outputs.aiter_sha }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Workflow | Result |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Nightly AMD (AITER Latest) | \`${{ needs.call-nightly-amd.result }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Nightly AMD ROCm 7.2 | \`${{ needs.call-nightly-amd-rocm720.result }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| PR Test AMD (AITER Latest) | \`${{ needs.call-pr-test-amd.result }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| PR Test AMD ROCm 7.2 | \`${{ needs.call-pr-test-amd-rocm720.result }}\` |" >> $GITHUB_STEP_SUMMARY
- name: Check if any job failed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more workflows failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more workflows were cancelled"
exit 1
fi
echo "All workflows passed"
+352
View File
@@ -0,0 +1,352 @@
name: AMD CI Job Monitor
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight UTC
pull_request:
paths:
- '.github/workflows/amd-ci-job-monitor.yml'
- 'scripts/ci/utils/query_job_status.py'
workflow_dispatch:
inputs:
hours:
description: 'Time window in hours'
required: false
default: '24'
type: string
job_filter:
description: 'Job name filter (leave empty for all AMD jobs)'
required: false
type: string
# Bound the API cost when the same ref pushes repeatedly. See note in
# runner-utilization.yml for the original incident this guards against.
concurrency:
group: amd-ci-job-monitor-${{ github.ref }}
cancel-in-progress: true
jobs:
fetch-actions-data:
name: Fetch Actions Snapshot
# Skip fork PRs entirely, and require the `run-ci` label on same-repo PRs.
# schedule and workflow_dispatch always run.
if: >-
github.event_name != 'pull_request' ||
(github.event.pull_request.head.repo.full_name == github.repository &&
contains(github.event.pull_request.labels.*.name, 'run-ci'))
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Select workflows for snapshot
id: select-workflows
run: |
if [[ -n "${{ inputs.job_filter }}" ]]; then
echo "workflows=pr-test-amd.yml" >> "$GITHUB_OUTPUT"
else
echo "workflows=pr-test-amd.yml,nightly-test-amd.yml,pr-test-amd-rocm720.yml,nightly-test-amd-rocm720.yml" >> "$GITHUB_OUTPUT"
fi
- name: Fetch Actions data snapshot
timeout-minutes: 30
run: |
# PR-trigger is just a script smoke check; scan a 20-minute window
# (0.34h) to bound API cost. schedule / dispatch run the full report.
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--workflow "${{ steps.select-workflows.outputs.workflows }}" \
--hours ${{ (github.event_name == 'pull_request' && '0.34') || inputs.hours || '24' }} \
--dump-data-file actions-job-snapshot.json
- name: Upload Actions data snapshot
uses: actions/upload-artifact@v4
with:
name: actions-job-snapshot
path: actions-job-snapshot.json
if-no-files-found: error
# Single job filter mode
custom-report:
name: Custom Job Report
if: ${{ inputs.job_filter }}
needs: fetch-actions-data
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Custom Job Report
timeout-minutes: 30
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ inputs.job_filter }}" \
--workflow "pr-test-amd.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# Parse workflow files to get job names dynamically
parse-workflows:
name: Parse Workflow Jobs
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
outputs:
pr_jobs: ${{ steps.parse.outputs.pr_jobs }}
nightly_jobs: ${{ steps.parse.outputs.nightly_jobs }}
pr_rocm720_jobs: ${{ steps.parse.outputs.pr_rocm720_jobs }}
nightly_rocm720_jobs: ${{ steps.parse.outputs.nightly_rocm720_jobs }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Parse workflow files
id: parse
run: |
# Parse pr-test-amd.yml and extract job names (exclude utility jobs)
# Excluded: call-gate, check-changes, pr-test-amd-finish, cancel, check-all-jobs
pr_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/pr-test-amd.yml | \
grep -v -E '^(call-gate|check-changes|pr-test-amd-finish|cancel|check-all-jobs)$' | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "pr_jobs=$pr_jobs" >> $GITHUB_OUTPUT
echo "PR jobs: $pr_jobs"
# Parse nightly-test-amd.yml and extract job names (exclude utility jobs)
# Excluded: check-all-jobs
nightly_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/nightly-test-amd.yml | \
grep -v -E '^(check-all-jobs)$' | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "nightly_jobs=$nightly_jobs" >> $GITHUB_OUTPUT
echo "Nightly jobs: $nightly_jobs"
# Parse pr-test-amd-rocm720.yml (exclude utility jobs)
# Excluded: call-gate, check-changes, pr-test-amd-finish, cancel, check-all-jobs
pr_rocm720_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/pr-test-amd-rocm720.yml | \
grep -v -E '^(call-gate|check-changes|pr-test-amd-finish|cancel|check-all-jobs)$' | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "pr_rocm720_jobs=$pr_rocm720_jobs" >> $GITHUB_OUTPUT
echo "PR ROCm 7.2 jobs: $pr_rocm720_jobs"
# Parse nightly-test-amd-rocm720.yml (exclude utility jobs)
# Excluded: check-all-jobs
nightly_rocm720_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/nightly-test-amd-rocm720.yml | \
grep -v -E '^(check-all-jobs)$' | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "nightly_rocm720_jobs=$nightly_rocm720_jobs" >> $GITHUB_OUTPUT
echo "Nightly ROCm 7.2 jobs: $nightly_rocm720_jobs"
# PR CI reports using dynamic matrix
pr-ci-reports:
name: PR - ${{ matrix.job_name }}
needs: [parse-workflows, fetch-actions-data]
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job_name: ${{ fromJson(needs.parse-workflows.outputs.pr_jobs) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Report
timeout-minutes: 15
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "pr-test-amd.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# Nightly AMD test reports using dynamic matrix
nightly-reports:
name: Nightly - ${{ matrix.job_name }}
needs: [parse-workflows, fetch-actions-data]
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job_name: ${{ fromJson(needs.parse-workflows.outputs.nightly_jobs) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Nightly Report
timeout-minutes: 15
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "nightly-test-amd.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# PR ROCm 7.2 CI reports using dynamic matrix
pr-rocm720-ci-reports:
name: PR ROCm720 - ${{ matrix.job_name }}
needs: [parse-workflows, fetch-actions-data]
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job_name: ${{ fromJson(needs.parse-workflows.outputs.pr_rocm720_jobs) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate PR ROCm 7.2 Report
timeout-minutes: 15
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "pr-test-amd-rocm720.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# Nightly ROCm 7.2 reports using dynamic matrix
nightly-rocm720-reports:
name: Nightly ROCm720 - ${{ matrix.job_name }}
needs: [parse-workflows, fetch-actions-data]
if: ${{ !inputs.job_filter }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job_name: ${{ fromJson(needs.parse-workflows.outputs.nightly_rocm720_jobs) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Nightly ROCm 7.2 Report
timeout-minutes: 15
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "nightly-test-amd-rocm720.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# Runner fleet report - cross-workflow runner analytics in a single pass
runner-fleet-report:
name: Runner Fleet Report
if: ${{ !inputs.job_filter }}
needs: fetch-actions-data
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: pip install tabulate
- name: Download Actions data snapshot
uses: actions/download-artifact@v4
with:
name: actions-job-snapshot
path: ci-data
- name: Generate Runner Fleet Report
timeout-minutes: 30
run: |
python scripts/ci/utils/query_job_status.py \
--repo ${{ github.repository }} \
--runner-report \
--workflow "pr-test-amd.yml,nightly-test-amd.yml,pr-test-amd-rocm720.yml,nightly-test-amd-rocm720.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
+10
View File
@@ -0,0 +1,10 @@
name: Auto tune
on:
workflow_dispatch:
jobs:
auto-tune-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -0,0 +1,88 @@
name: Bot Bump Docs Version
#
# Bumps the "install from source" release-branch version pinned in the docs
# (the `git clone -b v<version> ...sglang.git` line in
# docs_new/docs/get-started/install.mdx "Method 2: From source" and
# docs_new/docs/hardware-platforms/amd_gpu.mdx) whenever a release tag is
# pushed, and opens a PR with the change.
#
# Triggers mirror release-docker.yml: a pushed `v*` tag (the release), or a
# manual run with an explicit version.
on:
push:
tags:
- "v[0-9]+.*"
workflow_dispatch:
inputs:
version:
description: "Version to set (without v prefix, e.g., 0.5.13)"
required: true
permissions:
contents: write
pull-requests: write
jobs:
resolve-version:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#v}"
fi
if [ -z "$VERSION" ] || ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
bump-docs-version:
needs: resolve-version
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# Branch off main so the PR targets the docs on the default branch,
# not the (detached) tag commit that triggered this run.
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Configure Git and branch
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
# github.run_id is unique per run repo-wide; run_attempt disambiguates re-runs.
BRANCH_NAME="bot/bump-docs-version-${{ needs.resolve-version.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
- name: Run docs version bump script
run: |
python scripts/release/bump_docs_install_version.py "${{ needs.resolve-version.outputs.version }}"
- name: Commit and create PR
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
if git diff --quiet; then
echo "Docs already pin this version; no PR needed."
exit 0
fi
bash scripts/release/commit_and_pr.sh "docs install" "${{ needs.resolve-version.outputs.version }}" "$BRANCH_NAME"
@@ -0,0 +1,50 @@
name: Bot Bump Flashinfer Version
on:
workflow_dispatch:
inputs:
new_version:
description: 'New flashinfer version (e.g., 0.6.4)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
bump-flashinfer-version:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Configure Git and branch
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
RANDOM_SUFFIX=$(echo $RANDOM | md5sum | head -c 4)
BRANCH_NAME="bot/bump-flashinfer-version-${{ github.event.inputs.new_version }}-${RANDOM_SUFFIX}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
- name: Run flashinfer version bump script
run: |
python scripts/release/bump_flashinfer_version.py "${{ github.event.inputs.new_version }}"
- name: Commit and create PR
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
bash scripts/release/commit_and_pr.sh "flashinfer" "${{ github.event.inputs.new_version }}" "$BRANCH_NAME"
@@ -0,0 +1,60 @@
name: Bot Bump Kernel Version to SGLang
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
bump-kernel-version-to-sglang:
runs-on: ubuntu-latest
outputs:
branch_name: ${{ steps.set_output.outputs.branch_name }}
needs_sync: ${{ steps.check_sync.outputs.needs_sync }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Check if sync is needed
id: check_sync
run: |
python scripts/release/check_kernel_version_to_sglang.py
- name: Configure Git and branch
if: steps.check_sync.outputs.needs_sync == 'true'
id: set_output
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
RANDOM_SUFFIX=$(echo $RANDOM | md5sum | head -c 4)
KERNEL_VERSION="${{ steps.check_sync.outputs.kernel_version }}"
BRANCH_NAME="bot/bump-kernel-version-to-sglang-${KERNEL_VERSION}-${RANDOM_SUFFIX}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
echo "KERNEL_VERSION=$KERNEL_VERSION" >> $GITHUB_ENV
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Run kernel version bump script
if: steps.check_sync.outputs.needs_sync == 'true'
run: |
python scripts/release/bump_kernel_version_to_sglang.py
- name: Commit and create PR
if: steps.check_sync.outputs.needs_sync == 'true'
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
bash scripts/release/commit_and_pr_kernel_to_sglang.sh "$KERNEL_VERSION" "$BRANCH_NAME"
@@ -0,0 +1,50 @@
name: Bot Bump Kernel Version
on:
workflow_dispatch:
inputs:
new_version:
description: 'New sgl-kernel version (e.g., 0.3.12)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
bump-kernel-version:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Configure Git and branch
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
RANDOM_SUFFIX=$(echo $RANDOM | md5sum | head -c 4)
BRANCH_NAME="bot/bump-kernel-version-${{ github.event.inputs.new_version }}-${RANDOM_SUFFIX}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
- name: Run kernel version bump script
run: |
python scripts/release/bump_kernel_version.py "${{ github.event.inputs.new_version }}"
- name: Commit and create PR
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
bash scripts/release/commit_and_pr.sh "sgl-kernel" "${{ github.event.inputs.new_version }}" "$BRANCH_NAME"
@@ -0,0 +1,89 @@
name: Bot Bump SGLang Version
on:
workflow_dispatch:
inputs:
new_version:
description: 'New SGLang version (e.g., 0.5.3 or 0.5.3rc0)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
bump-sglang-version:
runs-on: ubuntu-latest
outputs:
branch_name: ${{ steps.set_output.outputs.branch_name }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Python dependencies
run: |
pip install tomli
- name: Configure Git and branch
id: set_output
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
RANDOM_SUFFIX=$(echo $RANDOM | md5sum | head -c 4)
BRANCH_NAME="bot/bump-sglang-version-${{ github.event.inputs.new_version }}-${RANDOM_SUFFIX}"
git checkout -b "$BRANCH_NAME"
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Run SGLang version bump script
run: |
python scripts/release/bump_sglang_version.py "${{ github.event.inputs.new_version }}"
- name: Commit and create PR
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
bash scripts/release/commit_and_pr.sh "SGLang" "${{ github.event.inputs.new_version }}" "$BRANCH_NAME"
run-nightly-tests-nvidia:
needs: bump-sglang-version
uses: ./.github/workflows/nightly-test-nvidia.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
run-nightly-tests-amd:
needs: bump-sglang-version
uses: ./.github/workflows/nightly-test-amd.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
run-nightly-tests-npu:
needs: bump-sglang-version
uses: ./.github/workflows/nightly-test-npu.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
run-pr-tests-xeon:
needs: bump-sglang-version
uses: ./.github/workflows/pr-test-xeon.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
run-pr-tests-xpu:
needs: bump-sglang-version
uses: ./.github/workflows/pr-test-xpu.yml
with:
ref: ${{ needs.bump-sglang-version.outputs.branch_name }}
secrets: inherit
+273
View File
@@ -0,0 +1,273 @@
name: Bot Cherry Pick to Release Branch
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to cherry-pick (preferred). Provide either pr_number OR commit_sha, not both.'
required: false
type: string
commit_sha:
description: 'Commit SHA to cherry-pick (alternative to pr_number; full or short hash).'
required: false
type: string
target_branch:
description: 'Target release branch (e.g., release/v0.5.7)'
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
cherry-pick:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
steps:
- name: Validate inputs
env:
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
COMMIT_SHA_INPUT: ${{ github.event.inputs.commit_sha }}
run: |
if [[ ! "$TARGET_BRANCH" =~ ^release/v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
echo "::error::Target branch must match pattern 'release/vX.Y' or 'release/vX.Y.Z' (e.g., release/v0.5.7)"
exit 1
fi
if [[ -z "$PR_NUMBER" && -z "$COMMIT_SHA_INPUT" ]]; then
echo "::error::Provide either 'pr_number' or 'commit_sha'."
exit 1
fi
if [[ -n "$PR_NUMBER" && -n "$COMMIT_SHA_INPUT" ]]; then
echo "::error::Provide exactly one of 'pr_number' or 'commit_sha', not both."
exit 1
fi
if [[ -n "$PR_NUMBER" && ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::pr_number must be a positive integer."
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure Git
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
- name: Validate target branch exists
env:
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
run: |
git fetch origin
if ! git ls-remote --exit-code --heads origin "$TARGET_BRANCH" > /dev/null 2>&1; then
echo "::error::Target branch '$TARGET_BRANCH' does not exist on remote"
exit 1
fi
- name: Resolve PR / commit
id: resolve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.inputs.pr_number }}
COMMIT_SHA_INPUT: ${{ github.event.inputs.commit_sha }}
run: |
set -eo pipefail
shopt -s inherit_errexit
SOURCE_PR=""
PR_URL=""
if [[ -n "$PR_NUMBER" ]]; then
if ! PR_JSON=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state,mergeCommit,title,number,url); then
echo "::error::Failed to fetch PR #$PR_NUMBER from $GITHUB_REPOSITORY (see gh output above)."
exit 1
fi
STATE=$(jq -r '.state' <<<"$PR_JSON")
if [[ "$STATE" != "MERGED" ]]; then
echo "::error::PR #$PR_NUMBER is not merged (state=$STATE). Only merged PRs can be cherry-picked."
exit 1
fi
FULL_SHA=$(jq -r '.mergeCommit.oid // empty' <<<"$PR_JSON")
if [[ -z "$FULL_SHA" ]]; then
echo "::error::PR #$PR_NUMBER is marked merged but has no associated merge commit."
exit 1
fi
PR_TITLE=$(jq -r '.title // empty' <<<"$PR_JSON")
PR_URL=$(jq -r '.url // empty' <<<"$PR_JSON")
SOURCE_PR="#$PR_NUMBER"
else
if ! git cat-file -t "$COMMIT_SHA_INPUT" > /dev/null 2>&1; then
echo "::error::Commit SHA '$COMMIT_SHA_INPUT' does not exist in the fetched history"
exit 1
fi
FULL_SHA=$(git rev-parse --verify "${COMMIT_SHA_INPUT}^{commit}")
PR_TITLE=$(git log -1 --format="%s" "$FULL_SHA")
# Look up the merged PR associated with this commit. Distinguish "API call
# failed" (abort — could mask an unmerged commit) from "API returned no
# merged PR" (fall through to ancestry check).
if ! PR_LOOKUP_RAW=$(gh api -X GET "repos/${GITHUB_REPOSITORY}/commits/${FULL_SHA}/pulls"); then
echo "::error::Failed to query PRs associated with commit ${FULL_SHA}. Aborting to avoid cherry-picking unverified commits."
exit 1
fi
PR_LOOKUP=$(jq '[.[] | select(.merged_at != null)][0] // empty' <<<"$PR_LOOKUP_RAW")
if [[ -n "$PR_LOOKUP" ]]; then
ASSOC_PR_NUM=$(jq -r '.number' <<<"$PR_LOOKUP")
PR_URL=$(jq -r '.html_url // empty' <<<"$PR_LOOKUP")
PR_TITLE=$(jq -r '.title // empty' <<<"$PR_LOOKUP")
SOURCE_PR="#$ASSOC_PR_NUM"
else
# No merged PR found for this commit. Require it to at least be reachable
# from the default branch so we don't cherry-pick unmerged work.
if ! DEFAULT_BRANCH=$(gh repo view "$GITHUB_REPOSITORY" --json defaultBranchRef --jq '.defaultBranchRef.name'); then
echo "::error::Could not determine default branch for ${GITHUB_REPOSITORY}."
exit 1
fi
if [[ -z "$DEFAULT_BRANCH" ]]; then
echo "::error::GitHub API returned an empty default branch for ${GITHUB_REPOSITORY}."
exit 1
fi
if ! git rev-parse --verify "origin/${DEFAULT_BRANCH}" >/dev/null 2>&1; then
echo "::error::origin/${DEFAULT_BRANCH} is not fetched locally; cannot verify whether ${FULL_SHA} is merged."
exit 1
fi
if ! git merge-base --is-ancestor "$FULL_SHA" "origin/${DEFAULT_BRANCH}"; then
echo "::error::Commit '$FULL_SHA' has no associated merged PR and is not reachable from origin/${DEFAULT_BRANCH}. Refusing to cherry-pick unmerged commits."
exit 1
fi
fi
fi
SHORT_SHA=$(git rev-parse --short "$FULL_SHA")
# Detect merge commit by counting parents. `git show -s --format=%P` prints
# parent SHAs separated by spaces; >1 means it's a merge commit.
PARENTS_LINE=$(git show -s --format=%P "$FULL_SHA")
PARENT_COUNT=$(wc -w <<<"$PARENTS_LINE" | tr -d ' ')
IS_MERGE_COMMIT="false"
if [[ "$PARENT_COUNT" -gt 1 ]]; then
IS_MERGE_COMMIT="true"
fi
echo "full_sha=$FULL_SHA" >> $GITHUB_OUTPUT
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "is_merge_commit=$IS_MERGE_COMMIT" >> $GITHUB_OUTPUT
echo "source_pr=$SOURCE_PR" >> $GITHUB_OUTPUT
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
{
echo "pr_title<<EOF"
echo "$PR_TITLE"
echo "EOF"
} >> $GITHUB_OUTPUT
echo "Cherry-picking ${SHORT_SHA} (${SOURCE_PR:-no associated PR}) - ${PR_TITLE}"
- name: Cherry-pick commit
id: cherry_pick
env:
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
FULL_SHA: ${{ steps.resolve.outputs.full_sha }}
SHORT_SHA: ${{ steps.resolve.outputs.short_sha }}
IS_MERGE_COMMIT: ${{ steps.resolve.outputs.is_merge_commit }}
run: |
RANDOM_SUFFIX=$(head -c 4 /dev/urandom | xxd -p)
NEW_BRANCH="cherry-pick/${SHORT_SHA}-to-${TARGET_BRANCH#release/}-${RANDOM_SUFFIX}"
git checkout -b "$NEW_BRANCH" "origin/$TARGET_BRANCH"
echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT
CP_ARGS=()
if [[ "$IS_MERGE_COMMIT" == "true" ]]; then
CP_ARGS+=(-m 1)
fi
if git cherry-pick "${CP_ARGS[@]}" "$FULL_SHA"; then
echo "cherry_pick_success=true" >> $GITHUB_OUTPUT
else
echo "::error::Cherry-pick of ${FULL_SHA} onto ${TARGET_BRANCH} failed due to conflicts. Resolve locally: 'git checkout ${TARGET_BRANCH} && git cherry-pick ${FULL_SHA}', fix conflicts, push a branch, and open the PR by hand."
git cherry-pick --abort || echo "::warning::git cherry-pick --abort returned non-zero; ignoring since the job is exiting."
echo "cherry_pick_success=false" >> $GITHUB_OUTPUT
exit 1
fi
- name: Push branch
if: steps.cherry_pick.outputs.cherry_pick_success == 'true'
env:
NEW_BRANCH: ${{ steps.cherry_pick.outputs.new_branch }}
run: |
git push origin "$NEW_BRANCH"
- name: Create Pull Request
id: create_pr
if: steps.cherry_pick.outputs.cherry_pick_success == 'true'
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_CHERRY_PICK }}
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
SHORT_SHA: ${{ steps.resolve.outputs.short_sha }}
FULL_SHA: ${{ steps.resolve.outputs.full_sha }}
PR_TITLE: ${{ steps.resolve.outputs.pr_title }}
SOURCE_PR: ${{ steps.resolve.outputs.source_pr }}
PR_URL: ${{ steps.resolve.outputs.pr_url }}
NEW_BRANCH: ${{ steps.cherry_pick.outputs.new_branch }}
run: |
if [[ -n "$SOURCE_PR" ]]; then
CHERRY_PICK_TITLE="[Cherry-pick to ${TARGET_BRANCH}] ${PR_TITLE} (${SOURCE_PR})"
else
CHERRY_PICK_TITLE="[Cherry-pick to ${TARGET_BRANCH}] ${PR_TITLE} (${SHORT_SHA})"
fi
BODY="Cherry-pick of commit \`${FULL_SHA}\` to \`${TARGET_BRANCH}\`."$'\n\n'
if [[ -n "$SOURCE_PR" ]]; then
BODY+="**Source PR:** ${SOURCE_PR} (${PR_URL})"$'\n'
fi
BODY+="**Source commit:** \`${FULL_SHA}\`"$'\n'
BODY+="**Original title:** ${PR_TITLE}"$'\n\n'
BODY+="---"$'\n'
BODY+="*This PR was automatically created by the cherry-pick workflow.*"
CREATED_PR_URL=$(printf '%s' "$BODY" | gh pr create \
--title "$CHERRY_PICK_TITLE" \
--base "$TARGET_BRANCH" \
--head "$NEW_BRANCH" \
--label "cherry-pick" \
--body-file -)
CREATED_PR_NUMBER="${CREATED_PR_URL##*/}"
echo "created_pr_url=$CREATED_PR_URL" >> $GITHUB_OUTPUT
echo "created_pr_number=$CREATED_PR_NUMBER" >> $GITHUB_OUTPUT
echo "Created cherry-pick PR #${CREATED_PR_NUMBER}: ${CREATED_PR_URL}"
- name: Summary
if: always()
env:
FULL_SHA: ${{ steps.resolve.outputs.full_sha }}
PR_TITLE: ${{ steps.resolve.outputs.pr_title }}
SOURCE_PR: ${{ steps.resolve.outputs.source_pr }}
TARGET_BRANCH: ${{ github.event.inputs.target_branch }}
CHERRY_PICK_SUCCESS: ${{ steps.cherry_pick.outputs.cherry_pick_success }}
NEW_BRANCH: ${{ steps.cherry_pick.outputs.new_branch }}
CREATED_PR_NUMBER: ${{ steps.create_pr.outputs.created_pr_number }}
CREATED_PR_URL: ${{ steps.create_pr.outputs.created_pr_url }}
ACTOR: ${{ github.actor }}
run: |
echo "## Cherry-Pick Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Triggered by:** @${ACTOR}" >> $GITHUB_STEP_SUMMARY
if [[ -n "$SOURCE_PR" ]]; then
echo "- **Source PR:** ${SOURCE_PR}" >> $GITHUB_STEP_SUMMARY
fi
echo "- **Source commit:** ${FULL_SHA}" >> $GITHUB_STEP_SUMMARY
echo "- **Title:** ${PR_TITLE}" >> $GITHUB_STEP_SUMMARY
echo "- **Target Branch:** ${TARGET_BRANCH}" >> $GITHUB_STEP_SUMMARY
if [[ "$CHERRY_PICK_SUCCESS" == "true" ]]; then
echo "- **Status:** Success" >> $GITHUB_STEP_SUMMARY
echo "- **PR Branch:** ${NEW_BRANCH}" >> $GITHUB_STEP_SUMMARY
if [[ -n "$CREATED_PR_NUMBER" ]]; then
echo "- **Cherry-pick PR:** [#${CREATED_PR_NUMBER}](${CREATED_PR_URL})" >> $GITHUB_STEP_SUMMARY
fi
else
echo "- **Status:** Failed" >> $GITHUB_STEP_SUMMARY
fi
@@ -0,0 +1,104 @@
name: Cancel PR Workflows on Close
# Cancels every unfinished run on a PR's head branch when the PR closes
# (merged or not). Runs are listed repo-wide by branch, not per workflow:
# the previous styfle/cancel-workflow-action `workflow_id: all` fetched
# only the first page (30) of repo workflows, so newer workflows (e.g.
# pr-test-extra.yml) were never cancelled and kept occupying runners.
on:
pull_request_target:
types:
- closed
permissions:
actions: write
jobs:
cancel:
runs-on: ubuntu-latest
steps:
- name: Cancel unfinished runs of this PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
HEAD_REPO_ID: ${{ github.event.pull_request.head.repo.id }}
shell: bash
run: |
set -euo pipefail
if [ -z "$HEAD_REPO_ID" ]; then
echo "Head repo no longer exists (fork deleted), nothing to match"
exit 0
fi
FAILURES=0
# `branch=` matches run.head_branch, shared with same-named
# branches of other forks -- filter by head repo id. Exclude this
# run itself: pull_request_target runs also carry the head branch.
# The status filter takes one value per request, so sweep every
# non-terminal state (pending/waiting/action_required runs would
# otherwise start after the PR is closed). Pass 2 catches runs
# still materializing during pass 1.
for pass in 1 2; do
run_ids=""
for status in queued in_progress waiting pending requested action_required; do
ids=""
for attempt in 1 2 3; do
if ids=$(gh api -X GET "repos/$REPO/actions/runs" \
--paginate \
-f branch="$HEAD_BRANCH" \
-f status="$status" \
-F per_page=100 \
--jq ".workflow_runs[]
| select(.head_repository.id == $HEAD_REPO_ID and .id != $GITHUB_RUN_ID)
| .id"); then
break
fi
ids=""
if [ "$attempt" -eq 3 ]; then
echo "::error::Listing $status runs failed after 3 attempts"
FAILURES=1
else
sleep 5
fi
done
run_ids="$run_ids"$'\n'"$ids"
done
run_ids=$(echo "$run_ids" | sed '/^$/d' | sort -u)
if [ -z "$run_ids" ]; then
echo "Pass $pass: no unfinished runs found"
break
fi
echo "Pass $pass: cancelling $(echo "$run_ids" | wc -l | tr -d ' ') run(s)"
for run_id in $run_ids; do
echo "Cancelling https://github.com/$REPO/actions/runs/$run_id"
if gh run cancel "$run_id" --repo "$REPO" 2>/dev/null; then
continue
fi
# Plain cancel fails for runs stuck behind approval /
# deployment protection rules; force-cancel handles those.
if gh api -X POST "repos/$REPO/actions/runs/$run_id/force-cancel" >/dev/null 2>&1; then
echo " force-cancelled"
continue
fi
# Finished between listing and cancelling is fine.
state=$(gh api "repos/$REPO/actions/runs/$run_id" --jq '.status' 2>/dev/null || echo "unknown")
if [ "$state" = "completed" ]; then
echo " already finished, nothing to cancel"
else
echo "::error::Failed to cancel run $run_id (status: $state)"
FAILURES=1
fi
done
if [ "$pass" -eq 1 ]; then
sleep 20
fi
done
exit "$FAILURES"
@@ -0,0 +1,234 @@
name: Cancel Unfinished PR Runs
on:
workflow_dispatch:
inputs:
workflows:
description: 'Space-separated list of workflow filenames to cancel'
required: true
type: string
default: 'pr-test.yml pr-test-extra.yml'
include_high_priority:
description: 'Also cancel runs from high-priority PRs'
required: false
type: boolean
default: false
include_rerun_test:
description: 'Also cancel /rerun-test dispatched runs (rerun-test.yml)'
required: false
type: boolean
default: false
permissions:
actions: write # Needed to cancel runs
contents: read # Needed to read repo info
pull-requests: read # needed for gh pr view (labels)
jobs:
cancel-unfinished-pr-runs:
runs-on: ubuntu-latest
steps:
- name: Install GitHub CLI
run: sudo apt-get install -y gh jq
- name: Cancel unfinished PR-associated runs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
WORKFLOWS: ${{ github.event.inputs.workflows }}
INCLUDE_HIGH_PRIORITY: ${{ github.event.inputs.include_high_priority }}
INCLUDE_RERUN_TEST: ${{ github.event.inputs.include_rerun_test }}
shell: bash
run: |
set -euo pipefail
# Read the space-separated string from the input into a bash array
read -r -a WORKFLOW_FILES <<< "${WORKFLOWS}"
if [ "$INCLUDE_RERUN_TEST" = "true" ]; then
WORKFLOW_FILES+=("rerun-test.yml")
fi
echo "Targeting ${#WORKFLOW_FILES[@]} workflow(s): ${WORKFLOW_FILES[*]}"
echo "include_high_priority=$INCLUDE_HIGH_PRIORITY, include_rerun_test=$INCLUDE_RERUN_TEST"
echo ""
# Decide whether to cancel run_id given a PR-lookup endpoint.
# $1 = run_id
# $2 = gh api path returning a list of PR objects (head=... or commits/<sha>/pulls)
# $3 = short label for log messages
maybe_cancel_for_pr() {
local run_id="$1"
local pr_query="$2"
local pr_label="$3"
local pr_info
pr_info=$(gh api -H "Accept: application/vnd.github+json" "$pr_query" \
--jq '.[0] | {number, state, merged_at}' 2>/dev/null || true)
if [ -z "$pr_info" ] || [ "$pr_info" = "null" ]; then
echo " ⚠️ No PR found ($pr_label), skipping"
return
fi
local pr_number pr_state
pr_number=$(echo "$pr_info" | jq -r '.number // empty')
pr_state=$(echo "$pr_info" | jq -r '.state // empty')
if [ -z "$pr_number" ]; then
echo " ⚠️ PR lookup returned empty number, skipping"
return
fi
local pr_url="https://github.com/$REPO/pull/$pr_number"
echo " PR: $pr_url ($pr_state)"
# Closed PR (merged or not): always cancel, skip label checks.
if [ "$pr_state" = "closed" ]; then
echo " 🚫 Cancelling (PR closed)..."
gh run cancel "$run_id" --repo "$REPO" || echo " ⚠️ Cancellation failed"
return
fi
# Open PR: apply label-based skip rules.
local labels
labels=$(gh pr view "$pr_number" --repo "$REPO" --json labels \
| jq -r '.labels[].name' 2>/dev/null || true)
if echo "$labels" | grep -Fxq "bypass-maintenance"; then
echo " 🛑 Skipping (bypass-maintenance label, never cancelled)"
return
fi
if echo "$labels" | grep -Fxq "high priority"; then
if [ "$INCLUDE_HIGH_PRIORITY" != "true" ]; then
echo " 🛑 Skipping (high priority label)"
return
fi
echo " ⚠️ High priority PR, but include_high_priority is enabled"
fi
echo " 🚫 Cancelling..."
gh run cancel "$run_id" --repo "$REPO" || echo " ⚠️ Cancellation failed"
}
export -f maybe_cancel_for_pr
for workflow_file in "${WORKFLOW_FILES[@]}"; do
echo "========================================="
echo "Workflow: $workflow_file"
echo "========================================="
# Get all unfinished runs.
# Use server-side --status filter: without it, `gh run list --limit 1000`
# only sees the most recent 1000 runs by createdAt, which on busy workflows
# like pr-test.yml is < 3 days. Old stuck runs would be missed.
all_runs=""
for status in queued in_progress waiting; do
batch=$(gh run list \
--repo "$REPO" \
--workflow "$workflow_file" \
--status "$status" \
--json databaseId,status,event,url,createdAt,displayTitle \
--limit 1000 \
| jq -c '.[]')
if [ -n "$batch" ]; then
if [ -n "$all_runs" ]; then
all_runs="$all_runs"$'\n'"$batch"
else
all_runs="$batch"
fi
fi
done
if [ -z "$all_runs" ]; then
echo "✅ No unfinished runs found"
echo ""
continue
fi
# Count runs by event type
total_runs=$(echo "$all_runs" | wc -l)
pr_runs=$(echo "$all_runs" | jq -s '[.[] | select(.event=="pull_request")] | length')
dispatch_runs=$(echo "$all_runs" | jq -s '[.[] | select(.event=="workflow_dispatch")] | length')
other_runs=$(echo "$all_runs" | jq -s '[.[] | select(.event!="pull_request" and .event!="workflow_dispatch")] | length')
echo "📊 Summary: $total_runs unfinished ($pr_runs pull_request, $dispatch_runs workflow_dispatch, $other_runs other)"
echo ""
# Other runs: list only, do not cancel.
if [ "$other_runs" -gt 0 ]; then
echo "--- Other Runs (listed only, not cancelled) ---"
echo "$all_runs" | jq -c 'select(.event!="pull_request" and .event!="workflow_dispatch")' | while read -r run; do
run_url=$(echo "$run" | jq -r '.url')
run_event=$(echo "$run" | jq -r '.event')
run_status=$(echo "$run" | jq -r '.status')
echo " • $run_event ($run_status): $run_url"
done
echo ""
fi
# PR runs: resolve PR via head=owner:branch.
if [ "$pr_runs" -gt 0 ]; then
echo "--- PR Runs (resolving via head=owner:branch) ---"
echo "$all_runs" | jq -c 'select(.event=="pull_request")' | while read -r run; do
run_id=$(echo "$run" | jq -r '.databaseId')
run_url=$(echo "$run" | jq -r '.url')
run_status=$(echo "$run" | jq -r '.status')
echo ""
echo "Run ($run_status): $run_url"
run_details=$(gh api -H "Accept: application/vnd.github+json" \
"repos/$REPO/actions/runs/$run_id" 2>/dev/null || true)
if [ -z "$run_details" ]; then
echo " ⚠️ Could not fetch run details, skipping"
continue
fi
head_owner=$(echo "$run_details" | jq -r '.head_repository.owner.login // empty')
head_branch=$(echo "$run_details" | jq -r '.head_branch // empty')
if [ -z "$head_owner" ] || [ -z "$head_branch" ]; then
echo " ⚠️ Missing head info, skipping"
continue
fi
echo " Branch: ${head_owner}:${head_branch}"
maybe_cancel_for_pr "$run_id" \
"repos/$REPO/pulls?state=all&head=${head_owner}:${head_branch}" \
"head=${head_owner}:${head_branch}"
done
echo ""
fi
# workflow_dispatch runs (e.g. /rerun-test): resolve PR via pr_head_sha
# parsed from run-name (`[rerun-test] <cmd> <sha>`).
if [ "$dispatch_runs" -gt 0 ]; then
echo "--- Dispatch Runs (resolving via pr_head_sha in display_title) ---"
echo "$all_runs" | jq -c 'select(.event=="workflow_dispatch")' | while read -r run; do
run_id=$(echo "$run" | jq -r '.databaseId')
run_url=$(echo "$run" | jq -r '.url')
run_status=$(echo "$run" | jq -r '.status')
display_title=$(echo "$run" | jq -r '.displayTitle // empty')
echo ""
echo "Run ($run_status): $run_url"
echo " Title: $display_title"
# Last whitespace-delimited token if it is a 40-hex SHA.
last_token=$(echo "$display_title" | awk '{print $NF}')
if ! [[ "$last_token" =~ ^[0-9a-f]{40}$ ]]; then
echo " ⚠️ No pr_head_sha in title, skipping"
continue
fi
echo " pr_head_sha: $last_token"
maybe_cancel_for_pr "$run_id" \
"repos/$REPO/commits/$last_token/pulls" \
"sha=$last_token"
done
echo ""
fi
done
echo "========================================="
echo "✅ Processing complete"
echo "========================================="
+66
View File
@@ -0,0 +1,66 @@
name: CI Auto Bisect
on:
workflow_run:
workflows: ["PR Test Base"]
types: [completed]
branches: [main]
workflow_dispatch: {}
concurrency:
group: ci-auto-bisect
cancel-in-progress: true
permissions:
contents: read
actions: read
jobs:
auto-bisect:
# Only run for scheduled pr-test completions (not PR-triggered), or manual dispatch
if: >
github.repository == 'sgl-project/sglang' && (
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.event == 'schedule' &&
github.event.workflow_run.conclusion != 'cancelled')
)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for git log between SHAs
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.14'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests anthropic
- name: Run Auto Bisect
id: bisect
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PYTHONUNBUFFERED: 1
PYTHONIOENCODING: utf-8
run: |
cd scripts/ci_monitor
python ci_auto_bisect.py \
--github-token $GITHUB_TOKEN \
--anthropic-api-key $ANTHROPIC_API_KEY \
--output bisect_results.json \
--max-failures 10
- name: Upload Bisect Results
if: always() && hashFiles('scripts/ci_monitor/bisect_results.json') != ''
uses: actions/upload-artifact@v4
with:
name: ci-auto-bisect-${{ github.run_number }}
path: scripts/ci_monitor/bisect_results.json
retention-days: 14
+164
View File
@@ -0,0 +1,164 @@
name: CI Coverage Overview
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
workflow_dispatch:
inputs:
output_format:
description: 'Output format'
required: false
default: 'markdown'
type: choice
options:
- markdown
- json
jobs:
summary:
name: Summary
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Generate Summary Report
run: |
python scripts/ci/utils/ci_coverage_report.py --section summary
by-folder:
name: Tests by Folder
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Generate Tests by Folder Report
run: |
python scripts/ci/utils/ci_coverage_report.py --section by-folder
by-suite:
name: Tests by Suite
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Generate Tests by Suite Report
run: |
python scripts/ci/utils/ci_coverage_report.py --section by-suite
unit-test-coverage:
name: Unit Test Code Coverage
runs-on: 1-gpu-h100
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain + protoc
# Editable install of sglang triggers `build_rust` for the bundled
# native gRPC extension (rust/sglang-grpc). The 1-gpu-h100 runner
# image has no Rust toolchain by default, which is why this job has
# been failing on every scheduled run. Use the same helper that
# ci_install_dependency.sh uses so the install path matches the
# rest of CI.
run: |
bash scripts/ci/utils/install_rust_protoc.sh
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
- name: Install dependencies
timeout-minutes: 10
run: |
pip install -e "python/[test]"
- name: Run unit tests with coverage
timeout-minutes: 10
run: |
pytest test/registered/unit/ \
--cov --cov-config=.coveragerc \
--cov-report=term-missing:skip-covered \
--continue-on-collection-errors \
-v | tee coverage_output.txt
- name: Write coverage to summary
if: always()
run: |
echo "## Unit Test Code Coverage" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${GITHUB_SHA::8}\` | **Branch:** \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Test result line (e.g., "== 42 passed, 1 failed in 23.5s ==")
echo '```' >> $GITHUB_STEP_SUMMARY
grep -E '^=+.*passed' coverage_output.txt >> $GITHUB_STEP_SUMMARY || true
echo "" >> $GITHUB_STEP_SUMMARY
# Coverage total, reformatted
awk '/^TOTAL / { for(i=1;i<=NF;i++) if($i~/^[0-9]+$/ || $i~/^[0-9]+%$/) a[++n]=$i; if(n>=3) printf "TOTAL Stmts: %s Miss: %s Cover: %s\n", a[1], a[2], a[3] }' coverage_output.txt >> $GITHUB_STEP_SUMMARY || true
echo '```' >> $GITHUB_STEP_SUMMARY
# Core modules with coverage < 50%, sorted by uncovered lines (descending)
LOW_COV=$(awk '/^python\/.*%/ {
for (i=1; i<=NF; i++) {
if ($i ~ /^[0-9]+%$/) {
pct = $i + 0
if (pct >= 1 && pct < 50) {
stmts = $(i-2) + 0
miss = $(i-1) + 0
printf "%d|%s|%d|%d|%d%%\n", miss, $1, stmts, miss, pct
}
break
}
}
}' coverage_output.txt \
| grep -E '/(mem_cache|managers|sampling|parser|observability|function_call|entrypoints|speculative|multimodal|utils)/' \
| sort -t'|' -k1 -nr | cut -d'|' -f2- | head -40 || true)
if [ -n "$LOW_COV" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details><summary>Top uncovered core modules (coverage < 50%, sorted by uncovered lines)</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| File | Stmts | Miss | Cover |" >> $GITHUB_STEP_SUMMARY
echo "|------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY
echo "$LOW_COV" | while IFS='|' read -r file stmts miss pct; do
echo "| \`$file\` | $stmts | $miss | $pct |" >> $GITHUB_STEP_SUMMARY
done
echo "</details>" >> $GITHUB_STEP_SUMMARY
fi
json-export:
name: JSON Export
runs-on: ubuntu-latest
if: inputs.output_format == 'json'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Generate JSON Report
run: |
python scripts/ci/utils/ci_coverage_report.py --output-format json > ci_coverage.json
- name: Upload JSON artifact
uses: actions/upload-artifact@v4
with:
name: ci-coverage-report
path: ci_coverage.json
+53
View File
@@ -0,0 +1,53 @@
name: CI Failure Monitor
on:
schedule:
- cron: '0 */12 * * *' # Every 12 hour
workflow_dispatch:
concurrency:
group: ci-failure-monitor-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
actions: read
jobs:
failure-analysis:
if: github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.14'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Run Failure Analysis
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GH_PAT_FOR_RUNNER_ADMIN: ${{ secrets.GH_PAT_FOR_RUNNER_ADMIN }}
PYTHONUNBUFFERED: 1
PYTHONIOENCODING: utf-8
run: |
cd scripts/ci_monitor
python ci_failures_analysis.py \
--token $GITHUB_TOKEN \
--limit 100 \
--output ci_failure_analysis_$(date +%Y%m%d_%H%M%S).json
- name: Upload Analysis Results
uses: actions/upload-artifact@v4
with:
name: ci-failure-analysis-${{ github.run_number }}
path: |
scripts/ci_monitor/ci_failure_analysis_*.json
retention-days: 7
+68
View File
@@ -0,0 +1,68 @@
name: CI Model Inventory
# Builds models-per-stage.json: each NVIDIA (CUDA) CI suite -> the HuggingFace
# model ids its tests reference, for runner cache pre-warming. Pure static
# analysis (no GPU, no sglang install), so it stays fresh per push to main.
# Grab the result with:
# gh run download -R sgl-project/sglang --name models-per-stage
# or read the rendered table in the run's job summary.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- "test/registered/**"
- "test/run_suite.py"
- "python/sglang/test/test_utils.py"
- "python/sglang/test/lora_utils.py"
- "python/sglang/test/ci/**"
- "scripts/ci/list_stage_models.py"
- "scripts/ci/stage_models_overrides.json"
- "scripts/ci/runner_configs.yml"
- ".github/workflows/ci-model-inventory.yml"
pull_request:
# Validate the tool itself end-to-end when it changes. Deliberately NOT
# filtered on test/registered/** -- that would run on most PRs; the
# published inventory is refreshed by the push-to-main trigger instead.
paths:
- "scripts/ci/list_stage_models.py"
- "scripts/ci/test_list_stage_models.py"
- "scripts/ci/stage_models_overrides.json"
- ".github/workflows/ci-model-inventory.yml"
schedule:
# Nightly safety net (09:00 UTC) so the artifact never goes stale even if a
# change lands outside the path filter above.
- cron: "0 9 * * *"
jobs:
build-inventory:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Run extractor unit tests
run: python3 -m unittest discover -s scripts/ci -p 'test_list_stage_models.py'
- name: Generate model inventory
run: |
python3 scripts/ci/list_stage_models.py \
--backend cuda \
--commit "${{ github.sha }}" \
--output models-per-stage.json \
--markdown inventory.md
- name: Write job summary
run: cat inventory.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload inventory artifact
uses: actions/upload-artifact@v4
with:
name: models-per-stage
path: models-per-stage.json
retention-days: 90
@@ -0,0 +1,96 @@
name: Close Inactive Issues
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
contents: read
jobs:
close-inactive-issues:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
steps:
- name: Check and close inactive issues
uses: actions/github-script@v8
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const sixtyDaysAgo = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000);
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
console.log(`Owner: ${owner}, Repo: ${repo}`);
async function fetchIssues(page = 1) {
console.log(`Fetching issues for ${owner}/${repo}, page ${page}`);
return await github.rest.issues.listForRepo({
owner,
repo,
state: 'open',
sort: 'updated',
direction: 'asc',
per_page: 100,
page: page
});
}
async function processIssues() {
console.log('Starting to process issues');
console.log(`Repository: ${owner}/${repo}`);
let page = 1;
let hasMoreIssues = true;
while (hasMoreIssues) {
try {
const issues = await fetchIssues(page);
console.log(`Fetched ${issues.data.length} issues on page ${page}`);
if (issues.data.length === 0) {
hasMoreIssues = false;
break;
}
for (const issue of issues.data) {
// Skip if the issue has 'good first issue' label
if (issue.labels.some(label => label.name === 'good first issue')) {
console.log(`Skipping issue #${issue.number} as it's marked as 'good first issue'`);
continue;
}
if (new Date(issue.updated_at) < sixtyDaysAgo) {
try {
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: 'closed',
labels: [...issue.labels.map(l => l.name), 'inactive']
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: 'This issue has been automatically closed due to inactivity. Please feel free to reopen it if needed.'
});
console.log(`Closed issue #${issue.number} due to inactivity.`);
} catch (error) {
console.error(`Failed to close issue #${issue.number}: ${error.message}`);
}
} else {
console.log(`Issue #${issue.number} is still active. Stopping processing.`);
hasMoreIssues = false;
break;
}
}
page += 1;
} catch (error) {
console.error(`Error fetching issues on page ${page}: ${error.message}`);
hasMoreIssues = false;
}
}
console.log('Finished processing issues');
}
await processIssues();
@@ -0,0 +1,214 @@
name: Diffusion CI Ground Truth Generation (NPU)
on:
workflow_dispatch:
inputs:
ref:
description: 'Git ref to checkout'
required: false
default: ''
type: string
case_ids:
description: 'Specific case IDs to run (space-separated, optional)'
required: false
default: ''
type: string
output_name:
description: 'Custom local output/artifact folder name. Leave empty to use defaults.'
required: false
default: ''
type: string
publish_target_dir:
description: 'Remote target directory in sgl-project/ci-data. Leave empty to use diffusion-ci/consistency_gt/sglang_generated/ascend.'
required: false
default: ''
type: string
concurrency:
group: diffusion-ci-gt-gen-${{ github.ref }}-${{ inputs.output_name || inputs.case_ids || 'default' }}
cancel-in-progress: true
permissions:
contents: write
actions: read
env:
SGLANG_IS_IN_CI: true
OUTPUT_NAME: ${{ inputs.output_name || 'diffusion-ci-outputs' }}
PUBLISH_TARGET_DIR: ${{ inputs.publish_target_dir || 'diffusion-ci/consistency_gt/sglang_generated/ascend' }}
jobs:
compute-diffusion-partitions:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
matrix-1npu: ${{ steps.compute.outputs.matrix-1npu }}
matrix-2npu: ${{ steps.compute.outputs.matrix-2npu }}
partition-count-1npu: ${{ steps.compute.outputs['partition-count-1npu'] }}
partition-count-2npu: ${{ steps.compute.outputs['partition-count-2npu'] }}
plan-1npu: ${{ steps.compute.outputs.plan-1npu }}
plan-2npu: ${{ steps.compute.outputs.plan-2npu }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Compute partitions
env:
USE_NPU_CONFIGS: true
id: compute
run: |
python scripts/ci/utils/diffusion/compute_diffusion_partitions.py \
--min-time 1200 \
--target-time 1800 \
--max-time 2400 \
--max-partitions 10 \
--parametrized-only
multimodal-diffusion-gen-1npu:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-1npu != '{"include":[]}'
runs-on: linux-aarch64-a3-2
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-1npu) }}
timeout-minutes: 150
container:
image: swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-a3-ubuntu22.04-py3.11
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3 diffusion
- name: Generate outputs
env:
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-1npu }}
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 1-npu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-1npu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-1npu-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
multimodal-diffusion-gen-2npu:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-2npu != '{"include":[]}'
runs-on: linux-aarch64-a3-16
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-2npu) }}
timeout-minutes: 150
container:
image: swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-a3-ubuntu22.04-py3.11
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3 diffusion
- name: Generate outputs
env:
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-2npu }}
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 2-npu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-2npu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-2npu-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
+571
View File
@@ -0,0 +1,571 @@
name: Diffusion CI Ground Truth Generation
on:
workflow_dispatch:
inputs:
ref:
description: 'Git ref to checkout'
required: false
default: ''
type: string
case_ids:
description: 'Specific case IDs to run (space-separated, optional)'
required: false
default: ''
type: string
output_name:
description: 'Custom local output/artifact folder name. Leave empty to use defaults.'
required: false
default: ''
type: string
publish_target_dir:
description: 'Remote target directory in sgl-project/ci-data. Leave empty to use sglang_generated, or official_generated when run_official_cases is true.'
required: false
default: ''
type: string
kernel_artifact_run_id:
description: "Optional sgl-kernel wheel source: GitHub Actions run ID of a pr-test.yml run on the SAME commit as 'ref' that uploaded wheel-python3.10-cuda13.0. Use only when this branch's torch/ABI change makes the PyPI sglang-kernel wheel incompatible. Find it under Actions > pr-test; artifacts expire after 90 days."
required: false
default: ''
type: string
run_official_cases:
description: 'Run official comparable GT cases instead of native SGLang GT cases.'
required: false
default: false
type: boolean
official_case_ids:
description: 'Specific official case IDs to run (space-separated). Leave empty to run all official comparable cases.'
required: false
default: ''
type: string
official_source_group:
description: 'Official GT source group filter: all, diffusers, wan21, ltx, or ltx23. Used only when run_official_cases is true.'
required: false
default: ''
type: string
ci_data_ref:
description: 'ci-data ref to use for repro scripts when running official GT cases.'
required: false
default: 'main'
type: string
concurrency:
group: diffusion-ci-gt-gen-${{ github.ref }}-${{ inputs.output_name || inputs.case_ids || inputs.official_case_ids || inputs.official_source_group || inputs.run_official_cases || 'default' }}
cancel-in-progress: true
permissions:
contents: write
actions: read
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
OUTPUT_NAME: ${{ inputs.output_name || 'diffusion-ci-outputs' }}
PUBLISH_TARGET_DIR: ${{ inputs.publish_target_dir || (inputs.run_official_cases && 'diffusion-ci/consistency_gt/official_generated' || 'diffusion-ci/consistency_gt/sglang_generated') }}
jobs:
compute-official-gt-matrix:
if: github.repository == 'sgl-project/sglang' && inputs.run_official_cases
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.compute.outputs.matrix }}
case-count: ${{ steps.compute.outputs.case-count }}
steps:
- name: Compute official case matrix
id: compute
env:
OFFICIAL_CASE_IDS: ${{ inputs.official_case_ids }}
OFFICIAL_SOURCE_GROUP: ${{ inputs.official_source_group || 'all' }}
run: |
python3 - <<'PY'
import json
import os
groups = {
"diffusers": [
"flux_2_image_t2i",
"flux_2_klein_image_t2i",
"flux_2_klein_base_image_t2i",
"flux_2_ti2i",
"flux_image_t2i",
"qwen_image_edit_2509_ti2i",
"qwen_image_edit_2511_ti2i",
"qwen_image_edit_ti2i",
"qwen_image_layered_i2i",
"qwen_image_t2i",
"zimage_image_t2i",
],
"wan21": ["wan2_1_t2v_1.3b"],
"ltx": [
"ltx_2_two_stage_t2v",
"ltx_2.3_two_stage_t2v_2gpus",
"ltx_2_3_two_stage_ti2v_2gpus",
"ltx_2.3_one_stage_ti2v",
"ltx_2_3_hq_pipeline",
],
"ltx23": [
"ltx_2.3_two_stage_t2v_2gpus",
"ltx_2.3_one_stage_ti2v",
],
}
source_group = os.environ["OFFICIAL_SOURCE_GROUP"].strip() or "all"
if source_group == "all":
selected_groups = ["diffusers", "wan21", "ltx"]
elif source_group in groups:
selected_groups = [source_group]
else:
raise SystemExit(f"unknown official_source_group: {source_group}")
requested = os.environ["OFFICIAL_CASE_IDS"].split()
requested_set = set(requested)
h200_cases = {
"flux_2_image_t2i",
"flux_2_klein_image_t2i",
"flux_2_klein_base_image_t2i",
"flux_2_ti2i",
}
include = []
for group in selected_groups:
for case_id in groups[group]:
if requested_set and case_id not in requested_set:
continue
include.append(
{
"source_group": group,
"case_id": case_id,
"runner": "8-gpu-h200" if case_id in h200_cases else "1-gpu-h100",
}
)
known_cases = {case for cases in groups.values() for case in cases}
unknown = sorted(requested_set - known_cases)
if unknown:
raise SystemExit(f"unknown official case id(s): {' '.join(unknown)}")
if not include:
raise SystemExit("official case matrix is empty")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
f.write(f"matrix={json.dumps({'include': include}, separators=(',', ':'))}\n")
f.write(f"case-count={len(include)}\n")
PY
official-gt-hopper:
needs: compute-official-gt-matrix
if: needs.compute-official-gt-matrix.result == 'success'
runs-on: ${{ matrix.runner }}
timeout-minutes: 240
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-official-gt-matrix.outputs.matrix) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Checkout ci-data repro scripts
uses: actions/checkout@v4
with:
repository: sgl-project/ci-data
ref: ${{ inputs.ci_data_ref || 'main' }}
path: ci-data
token: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
sparse-checkout: |
diffusion-ci/repro_scripts
diffusion-ci/consistency_gt/official_generated/case_map.json
sparse-checkout-cone-mode: false
- name: Prepare sgl-kernel/dist for prebuilt wheel
if: inputs.kernel_artifact_run_id != ''
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download prebuilt sgl-kernel wheel
if: inputs.kernel_artifact_run_id != ''
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
name: wheel-python3.10-cuda13.0
run-id: ${{ inputs.kernel_artifact_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
CUSTOM_BUILD_SGL_KERNEL="${{ inputs.kernel_artifact_run_id != '' && 'true' || 'false' }}" \
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Install official LTX repro dependencies
if: matrix.source_group == 'ltx' || matrix.source_group == 'ltx23'
run: |
UV_SYSTEM_PYTHON=1 uv pip install \
"transformers==4.52.4" \
openimageio \
--index-strategy unsafe-best-match \
--prerelease allow
UV_SYSTEM_PYTHON=1 uv pip uninstall kernels
- name: Checkout official Wan2.1 repo
if: matrix.source_group == 'wan21'
run: |
mkdir -p /tmp/mmgen-official-code
if [ ! -d /tmp/mmgen-official-code/Wan2.1/.git ]; then
git clone https://github.com/Wan-Video/Wan2.1.git /tmp/mmgen-official-code/Wan2.1
fi
git -C /tmp/mmgen-official-code/Wan2.1 fetch --depth 1 origin 9737cba9c1c3c4d04b33fcad41c111989865d315
git -C /tmp/mmgen-official-code/Wan2.1 checkout 9737cba9c1c3c4d04b33fcad41c111989865d315
git -C /tmp/mmgen-official-code/Wan2.1 rev-parse HEAD
- name: Checkout official LTX-2 repo
if: matrix.source_group == 'ltx' || matrix.source_group == 'ltx23'
run: |
mkdir -p /tmp/mmgen-official-code
if [ ! -d /tmp/mmgen-official-code/LTX-2/.git ]; then
git clone https://github.com/Lightricks/LTX-2.git /tmp/mmgen-official-code/LTX-2
fi
git -C /tmp/mmgen-official-code/LTX-2 fetch --depth 1 origin 41d924371612b692c0fd1e4d9d94c3dfb3c02cb3
git -C /tmp/mmgen-official-code/LTX-2 checkout 41d924371612b692c0fd1e4d9d94c3dfb3c02cb3
git -C /tmp/mmgen-official-code/LTX-2 rev-parse HEAD
- name: Generate official output
env:
HF_TOKEN: ${{ secrets.SGLANG_DIFFUSION_CI_HF_TOKEN || secrets.HF_TOKEN }}
HUGGING_FACE_HUB_TOKEN: ${{ secrets.SGLANG_DIFFUSION_CI_HF_TOKEN || secrets.HF_TOKEN }}
RUNAI_STREAMER_MEMORY_LIMIT: 0
PYTORCH_CUDA_ALLOC_CONF: expandable_segments:True
CUDA_VISIBLE_DEVICES: 0
CASE_ID: ${{ matrix.case_id }}
SOURCE_GROUP: ${{ matrix.source_group }}
run: |
git -C ci-data rev-parse HEAD
mkdir -p "python/${{ env.OUTPUT_NAME }}"
if [ "$SOURCE_GROUP" = "diffusers" ]; then
cd python
python ../ci-data/diffusion-ci/repro_scripts/gen_official_diffusion_gt.py \
--suite 1-gpu \
--out-dir ./${{ env.OUTPUT_NAME }} \
--case-ids "$CASE_ID" \
--dtype bf16 \
--device-map none \
--generator-device cuda
elif [ "$SOURCE_GROUP" = "wan21" ]; then
cd python
python ../ci-data/diffusion-ci/repro_scripts/gen_official_diffusion_gt.py \
--suite 1-gpu \
--out-dir ./${{ env.OUTPUT_NAME }} \
--case-ids "$CASE_ID" \
--wan-official-repo-dir /tmp/mmgen-official-code/Wan2.1 \
--dtype bf16 \
--device-map none \
--generator-device cuda
elif [ "$SOURCE_GROUP" = "ltx" ] || [ "$SOURCE_GROUP" = "ltx23" ]; then
cd python
extra_ltx_args=()
if [ "$CASE_ID" = "ltx_2_3_hq_pipeline" ]; then
extra_ltx_args+=(--num-frames 24)
fi
set +e
PYTHONPATH=/tmp/mmgen-official-code/LTX-2/packages/ltx-core/src:/tmp/mmgen-official-code/LTX-2/packages/ltx-pipelines/src:$PWD:$PYTHONPATH \
python ../ci-data/diffusion-ci/repro_scripts/gen_official_ltx23.py \
--out-dir ./${{ env.OUTPUT_NAME }} \
--case-ids "$CASE_ID" \
"${extra_ltx_args[@]}"
status=$?
set -e
if [ "$status" -ne 0 ]; then
find ./${{ env.OUTPUT_NAME }} \( -name 'official_ltx_manifest.json' -o -name 'official_ltx23_manifest.json' \) -print -exec cat {} \;
exit "$status"
fi
else
echo "Unknown SOURCE_GROUP=$SOURCE_GROUP"
exit 1
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-${{ matrix.source_group }}-${{ matrix.case_id }}
path: |
python/${{ env.OUTPUT_NAME }}/*.jpg
python/${{ env.OUTPUT_NAME }}/*.png
python/${{ env.OUTPUT_NAME }}/official_gt_manifest_*.json
python/${{ env.OUTPUT_NAME }}/official_ltx_manifest.json
python/${{ env.OUTPUT_NAME }}/official_ltx23_manifest.json
retention-days: 7
- name: Validate generated GT images
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}" \
--check-only
- name: Publish official GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
compute-diffusion-partitions:
if: github.repository == 'sgl-project/sglang' && !inputs.run_official_cases
runs-on: ubuntu-latest
outputs:
matrix-1gpu: ${{ steps.compute.outputs.matrix-1gpu }}
matrix-2gpu: ${{ steps.compute.outputs.matrix-2gpu }}
matrix-b200: ${{ steps.compute.outputs.matrix-b200 }}
partition-count-1gpu: ${{ steps.compute.outputs['partition-count-1gpu'] }}
partition-count-2gpu: ${{ steps.compute.outputs['partition-count-2gpu'] }}
partition-count-b200: ${{ steps.compute.outputs['partition-count-b200'] }}
plan-1gpu: ${{ steps.compute.outputs.plan-1gpu }}
plan-2gpu: ${{ steps.compute.outputs.plan-2gpu }}
plan-b200: ${{ steps.compute.outputs.plan-b200 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Compute partitions
id: compute
run: |
python scripts/ci/utils/diffusion/compute_diffusion_partitions.py \
--min-time 1200 \
--target-time 1800 \
--max-time 2400 \
--max-partitions 10 \
--parametrized-only
multimodal-diffusion-gen-1gpu:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-1gpu != '{"include":[]}'
runs-on: 1-gpu-h100
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-1gpu) }}
timeout-minutes: 150
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Prepare sgl-kernel/dist for prebuilt wheel
if: inputs.kernel_artifact_run_id != ''
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download prebuilt sgl-kernel wheel
if: inputs.kernel_artifact_run_id != ''
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
name: wheel-python3.10-cuda13.0
run-id: ${{ inputs.kernel_artifact_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
CUSTOM_BUILD_SGL_KERNEL="${{ inputs.kernel_artifact_run_id != '' && 'true' || 'false' }}" \
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Generate outputs
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-1gpu }}
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 1-gpu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-1gpu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-1gpu-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Validate generated GT images
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}" \
--check-only
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
multimodal-diffusion-gen-2gpu:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-2gpu != '{"include":[]}'
runs-on: 2-gpu-h100
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-2gpu) }}
timeout-minutes: 150
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Prepare sgl-kernel/dist for prebuilt wheel
if: inputs.kernel_artifact_run_id != ''
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download prebuilt sgl-kernel wheel
if: inputs.kernel_artifact_run_id != ''
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
name: wheel-python3.10-cuda13.0
run-id: ${{ inputs.kernel_artifact_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
CUSTOM_BUILD_SGL_KERNEL="${{ inputs.kernel_artifact_run_id != '' && 'true' || 'false' }}" \
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Generate outputs
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-2gpu }}
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 2-gpu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-2gpu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-2gpu-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Validate generated GT images
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}" \
--check-only
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
multimodal-diffusion-gen-b200:
needs: compute-diffusion-partitions
if: |
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-b200 != '{"include":[]}'
runs-on: 4-gpu-b200
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-b200) }}
timeout-minutes: 240
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Prepare sgl-kernel/dist for prebuilt wheel
if: inputs.kernel_artifact_run_id != ''
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download prebuilt sgl-kernel wheel
if: inputs.kernel_artifact_run_id != ''
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
name: wheel-python3.10-cuda13.0
run-id: ${{ inputs.kernel_artifact_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
run: |
CUSTOM_BUILD_SGL_KERNEL="${{ inputs.kernel_artifact_run_id != '' && 'true' || 'false' }}" \
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Generate outputs
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-b200 }}
run: |
cd python
python -m sglang.multimodal_gen.test.scripts.gen_diffusion_ci_outputs \
--suite 1-gpu-b200 \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-b200'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
--out-dir ./${{ env.OUTPUT_NAME }} \
${{ inputs.case_ids != '' && format('--case-ids {0}', inputs.case_ids) || '' }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_NAME }}-b200-part${{ matrix.part }}
path: python/${{ env.OUTPUT_NAME }}
retention-days: 7
- name: Validate generated GT images
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}" \
--check-only
- name: Publish GT images to sgl-project/ci-data
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python scripts/ci/utils/diffusion/publish_diffusion_gt.py \
--source-dir python/${{ env.OUTPUT_NAME }} \
--target-dir "${{ env.PUBLISH_TARGET_DIR }}"
+359
View File
@@ -0,0 +1,359 @@
name: Full Test (NPU)
on:
# pull_request:
# branches:
# - main
# paths:
# - ".github/workflows/full-test-npu.yml"
workflow_dispatch:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
image_a3:
description: 'The a3 running docker image of the test task.'
required: false
type: string
default: 'swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-a3-ubuntu22.04-py3.11'
skip_install_flag:
description: 'Indicates whether to skip the installation of sglang, defaulting to false.'
required: false
type: string
default: 'false'
concurrency:
group: full-test-npu-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
set-image-config:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.set-vars.outputs.ref }}
job_filter: ${{ steps.set-vars.outputs.job_filter }}
image_a3: ${{ steps.set-vars.outputs.image_a3 }}
skip_install_flag: ${{ steps.set-vars.outputs.skip_install_flag }}
steps:
# When triggered by PR, no inputs parameters are used. The latest community code is tested by default.
- name: Set image config
id: set-vars
run: |
if [ -z "${{ inputs.ref }}" ]; then
echo "ref=" >> $GITHUB_OUTPUT
else
echo "ref=${{ inputs.ref }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.job_filter }}" ]; then
echo "job_filter=all" >> $GITHUB_OUTPUT
else
echo "job_filter=${{ inputs.job_filter }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.image_a3 }}" ]; then
echo "image_a3=swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:8.5.0-a3-ubuntu22.04-py3.11" >> $GITHUB_OUTPUT
else
echo "image_a3=${{ inputs.image_a3 }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.skip_install_flag }}" ]; then
echo "skip_install_flag=false" >> $GITHUB_OUTPUT
else
echo "skip_install_flag=${{ inputs.skip_install_flag }}" >> $GITHUB_OUTPUT
fi
nighly-test-npu:
needs: [set-image-config]
name: nightly-test-npu
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
uses: ./.github/workflows/nightly-test-npu.yml
with:
ref: ${{ needs.set-image-config.outputs.ref }}
job_filter: ${{ needs.set-image-config.outputs.job_filter }}
image_a3: ${{ needs.set-image-config.outputs.image_a3 }}
skip_install_flag: ${{ needs.set-image-config.outputs.skip_install_flag }}
secrets: inherit
full-1-npu-a3:
needs: [set-image-config]
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
runs-on: linux-aarch64-a3-2
container:
image: ${{ needs.set-image-config.outputs.image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.set-image-config.outputs.ref || github.ref }}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
if [ ${{ needs.set-image-config.outputs.skip_install_flag }} != "true" ];then
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
fi
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Print Log Information
run: |
bash scripts/ci/npu/npu_log_print.sh
- name: Run test
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
pip install sglang_router
hf download lmms-lab/MMMU --repo-type dataset
pip install sentence_transformers torchaudio==2.8.0
pip install protobuf==6.31.1 zss pre-commit wandb>=0.16.0 tenacity==8.3.0 loguru openpyxl latex2sympy2 zstandard transformers-stream-generator tqdm-multiprocess pycocoevalcap
pip install yt-dlp sentencepiece==0.1.99 nltk av ftfy sqlitedict==2.1.0 sacrebleu>=1.5.0 pytablewriter black==24.1.0 isort==5.13.2 peft>=0.2.0 accelerate>=0.29.1
pip install jsonlines httpx==0.25.0 evaluate>=0.4.0 datasets==2.16.1 numexpr xgrammar==0.2.1 numpy==1.26.4 dotenv
git clone --branch v0.3.3 --depth 1 https://github.com/EvolvingLMMs-Lab/lmms-eval.git
cd ./lmms-eval
nohup pip install . > lmmslog.txt 2>&1 &
sleep 120
export PYTHONPATH=$PYTHONPATH:$(pwd)
cd ../
cd test
python3 run_suite.py --hw npu --suite full-1-npu-a3 --nightly --continue-on-error --timeout-per-file 3600
full-2-npu-a3:
needs: [set-image-config]
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
runs-on: linux-aarch64-a3-2
container:
image: ${{ needs.set-image-config.outputs.image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.set-image-config.outputs.ref || github.ref }}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
if [ ${{ needs.set-image-config.outputs.skip_install_flag }} != "true" ];then
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
fi
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Print Log Information
run: |
bash scripts/ci/npu/npu_log_print.sh
- name: Run test
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
pip install sglang_router
hf download lmms-lab/MMMU --repo-type dataset
pip install sentence_transformers torchaudio==2.8.0
pip install protobuf==6.31.1 zss pre-commit wandb>=0.16.0 tenacity==8.3.0 loguru openpyxl latex2sympy2 zstandard transformers-stream-generator tqdm-multiprocess pycocoevalcap
pip install yt-dlp sentencepiece==0.1.99 nltk av ftfy sqlitedict==2.1.0 sacrebleu>=1.5.0 pytablewriter black==24.1.0 isort==5.13.2 peft>=0.2.0 accelerate>=0.29.1
pip install jsonlines httpx==0.25.0 evaluate>=0.4.0 datasets==2.16.1 numexpr xgrammar==0.2.1 numpy==1.26.4 dotenv
git clone --branch v0.3.3 --depth 1 https://github.com/EvolvingLMMs-Lab/lmms-eval.git
cd ./lmms-eval
nohup pip install . > lmmslog.txt 2>&1 &
sleep 120
export PYTHONPATH=$PYTHONPATH:$(pwd)
cd ../
cd test
python3 run_suite.py --hw npu --suite full-2-npu-a3 --nightly --continue-on-error --timeout-per-file 3600
full-4-npu-a3:
needs: [set-image-config]
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
runs-on: linux-aarch64-a3-4
container:
image: ${{ needs.set-image-config.outputs.image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.set-image-config.outputs.ref || github.ref }}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
if [ ${{ needs.set-image-config.outputs.skip_install_flag }} != "true" ];then
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
fi
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Print Log Information
run: |
bash scripts/ci/npu/npu_log_print.sh
- name: Run test
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
pip install sglang_router
hf download lmms-lab/MMMU --repo-type dataset
pip install sentence_transformers torchaudio==2.8.0
pip install protobuf==6.31.1 zss pre-commit wandb>=0.16.0 tenacity==8.3.0 loguru openpyxl latex2sympy2 zstandard transformers-stream-generator tqdm-multiprocess pycocoevalcap
pip install yt-dlp sentencepiece==0.1.99 nltk av ftfy sqlitedict==2.1.0 sacrebleu>=1.5.0 pytablewriter black==24.1.0 isort==5.13.2 peft>=0.2.0 accelerate>=0.29.1
pip install jsonlines httpx==0.25.0 evaluate>=0.4.0 datasets==2.16.1 numexpr xgrammar==0.2.1 numpy==1.26.4 dotenv
git clone --branch v0.3.3 --depth 1 https://github.com/EvolvingLMMs-Lab/lmms-eval.git
cd ./lmms-eval
nohup pip install . > lmmslog.txt 2>&1 &
sleep 120
export PYTHONPATH=$PYTHONPATH:$(pwd)
cd ../
cd test
python3 run_suite.py --hw npu --suite full-4-npu-a3 --nightly --continue-on-error --timeout-per-file 3600
full-16-npu-a3:
needs: [set-image-config]
if: ${{ (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request') }}
runs-on: linux-aarch64-a3-16
container:
image: ${{ needs.set-image-config.outputs.image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.set-image-config.outputs.ref || github.ref }}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
if [ ${{ needs.set-image-config.outputs.skip_install_flag }} != "true" ];then
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
fi
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Print Log Information
run: |
bash scripts/ci/npu/npu_log_print.sh
- name: Run test
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
pip install sglang_router
hf download lmms-lab/MMMU --repo-type dataset
pip install sentence_transformers torchaudio==2.8.0
pip install protobuf==6.31.1 zss pre-commit wandb>=0.16.0 tenacity==8.3.0 loguru openpyxl latex2sympy2 zstandard transformers-stream-generator tqdm-multiprocess pycocoevalcap
pip install yt-dlp sentencepiece==0.1.99 nltk av ftfy sqlitedict==2.1.0 sacrebleu>=1.5.0 pytablewriter black==24.1.0 isort==5.13.2 peft>=0.2.0 accelerate>=0.29.1
pip install jsonlines httpx==0.25.0 evaluate>=0.4.0 datasets==2.16.1 numexpr xgrammar==0.2.1 numpy==1.26.4 dotenv
git clone --branch v0.3.3 --depth 1 https://github.com/EvolvingLMMs-Lab/lmms-eval.git
cd ./lmms-eval
nohup pip install . > lmmslog.txt 2>&1 &
sleep 120
export PYTHONPATH=$PYTHONPATH:$(pwd)
cd ../
cd test
python3 run_suite.py --hw npu --suite full-16-npu-a3 --nightly --continue-on-error --timeout-per-file 3600
check-all-jobs:
if: github.repository == 'sgl-project/sglang' && always()
needs:
- nighly-test-npu
- full-1-npu-a3
- full-2-npu-a3
- full-4-npu-a3
- full-16-npu-a3
runs-on: ubuntu-latest
container:
image: docker.m.daocloud.io/ubuntu:22.04
steps:
- name: Check if any job failed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more nightly test jobs failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more nightly test jobs were cancelled"
exit 1
fi
echo "All nightly test jobs passed"
+20
View File
@@ -0,0 +1,20 @@
name: Auto Label PRs
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Auto-label by file changes
uses: actions/labeler@v5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/labeler.yml
sync-labels: false
+63
View File
@@ -0,0 +1,63 @@
name: Lint
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Reject changes under legacy docs/
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
BASE_REF="${{ github.base_ref }}"
# Refresh origin/<base_ref> with full history; --depth=1 would
# shallow the ref and break the merge-base used by `...`.
git fetch --no-tags origin "$BASE_REF"
# First, verify the diff itself succeeds and check whether any
# files changed. A silent failure here would let docs/ changes
# through, so the explicit `if !` guard is important.
if ! CHANGED=$(git diff --name-only --diff-filter=ACMRDTUXB "origin/${BASE_REF}...HEAD"); then
echo "git diff origin/${BASE_REF}...HEAD failed; aborting." >&2
exit 2
fi
if [ -z "$CHANGED" ]; then
echo "No changed files vs origin/${BASE_REF}; skipping."
exit 0
fi
# Re-emit with -z so xargs can safely handle whitespace in paths.
git diff -z --name-only --diff-filter=ACMRDTUXB "origin/${BASE_REF}...HEAD" \
| xargs -0 python3 scripts/ci/check_no_docs_changes.py
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install pre-commit hook
run: |
python -m pip install pre-commit
pre-commit install
- name: Run pre-commit checks
run: SKIP=no-commit-to-branch pre-commit run --all-files --show-diff-on-failure
- name: Run lychee docs checks (offline references)
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
with:
args: --config .github/linters/lychee.toml README.md "docs/**/*.md" "docs/**/*.rst" "docs/**/*.ipynb"
- name: Run sgl-kernel clang-format checks
uses: DoozyX/clang-format-lint-action@v0.20
with:
source: sgl-kernel
extensions: h,c,cpp,hpp,cu,cuh,cc
clangFormatVersion: 20
style: file
+317
View File
@@ -0,0 +1,317 @@
name: List Active Runs
on:
workflow_dispatch:
inputs:
workflows:
description: 'Space-separated list of workflow filenames to check'
required: false
type: string
default: 'pr-test.yml'
permissions:
actions: read
contents: read
pull-requests: read
jobs:
list-active-runs:
runs-on: ubuntu-latest
steps:
- name: Install GitHub CLI
run: sudo apt-get install -y gh jq
- name: List active runs grouped by PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
WORKFLOWS: ${{ github.event.inputs.workflows || 'pr-test.yml' }}
shell: bash
run: |
set -euo pipefail
echo "========================================="
echo "🔍 Active Workflow Runs Report"
echo "========================================="
echo ""
# Get all workflows or specific ones
read -r -a workflow_files <<< "${WORKFLOWS}"
echo "📋 Checking specified workflows: ${WORKFLOWS}"
echo ""
# Create a temporary file to store PR data
pr_data_file=$(mktemp)
# Process each workflow
for workflow_file in ${workflow_files[@]}; do
echo "Scanning workflow: $workflow_file"
# Get all active runs (queued, waiting, in_progress)
active_runs=$(gh run list \
--repo "$REPO" \
--workflow "$workflow_file" \
--json databaseId,status,event,headBranch,createdAt,updatedAt,headSha,number,attempt \
--limit 500 \
| jq -c '.[] | select(.status=="queued" or .status=="waiting" or .status=="in_progress")')
if [ -z "$active_runs" ]; then
continue
fi
# Process each run
echo "$active_runs" | while read -r run; do
run_id=$(echo "$run" | jq -r '.databaseId')
run_status=$(echo "$run" | jq -r '.status')
run_event=$(echo "$run" | jq -r '.event')
created_at=$(echo "$run" | jq -r '.createdAt')
head_sha=$(echo "$run" | jq -r '.headSha')
run_number=$(echo "$run" | jq -r '.number')
run_attempt=$(echo "$run" | jq -r '.attempt // 1')
# Get detailed run information including jobs
run_details=$(gh api "repos/$REPO/actions/runs/$run_id" 2>/dev/null || true)
if [ -z "$run_details" ]; then
continue
fi
head_owner=$(echo "$run_details" | jq -r '.head_repository.owner.login // empty')
head_branch=$(echo "$run_details" | jq -r '.head_branch // empty')
if [ -z "$head_owner" ] || [ -z "$head_branch" ]; then
continue
fi
# Find PR number (may be empty for non-PR runs)
pr_number=$(gh api "repos/$REPO/pulls?state=open&head=${head_owner}:${head_branch}" \
--jq '.[0].number // empty' 2>/dev/null || true)
if [ -z "$pr_number" ]; then
pr_number="NO_PR"
fi
# Get jobs for this run (with pagination to avoid missing jobs)
jobs=$(gh api "repos/$REPO/actions/runs/$run_id/jobs" --paginate --jq '.jobs[]' | jq -s '.')
running_jobs=$(echo "$jobs" | jq '[.[] | select(.status=="in_progress")] | length')
queued_jobs=$(echo "$jobs" | jq '[.[] | select(.status=="queued" or .status=="waiting")] | length')
# Get runner info for running jobs
runners=$(echo "$jobs" | jq -r '.[] | select(.status=="in_progress") | .runner_name // "N/A"' | paste -sd "," -)
# Calculate queue time
current_time=$(date -u +%s)
created_time=$(date -u -d "$created_at" +%s 2>/dev/null || echo "$current_time")
queue_time=$((current_time - created_time))
queue_minutes=$((queue_time / 60))
# Store data in temporary file (unified format with event and branch)
echo "$pr_number|$workflow_file|$run_id|$run_status|$running_jobs|$queued_jobs|$runners|$queue_minutes|$created_at|$head_sha|$run_attempt|$run_event|$head_branch" >> "$pr_data_file"
done
done
echo ""
echo "========================================="
echo "📊 Active Runs Summary"
echo "========================================="
echo ""
if [ ! -s "$pr_data_file" ]; then
echo "✅ No active runs found"
rm -f "$pr_data_file"
exit 0
fi
# Get unique PR numbers (exclude NO_PR entries)
pr_numbers=$(cut -d'|' -f1 < "$pr_data_file" | grep -v '^NO_PR$' | sort -u || true)
# Separate high priority and normal PRs
high_priority_prs=()
normal_prs=()
for pr_num in $pr_numbers; do
labels=$(gh pr view "$pr_num" --repo "$REPO" --json labels \
| jq -r '.labels[].name' 2>/dev/null || true)
if echo "$labels" | grep -Fxq "high priority"; then
high_priority_prs+=($pr_num)
else
normal_prs+=($pr_num)
fi
done
# Combine: high priority first, then normal
sorted_pr_numbers=("${high_priority_prs[@]}" "${normal_prs[@]}")
pr_count=0
total_running=0
total_queued=0
for pr_num in "${sorted_pr_numbers[@]}"; do
pr_count=$((pr_count + 1))
# Get PR details
pr_info=$(gh pr view "$pr_num" --repo "$REPO" --json title,author,labels,url 2>/dev/null || true)
if [ -z "$pr_info" ]; then
continue
fi
pr_title=$(echo "$pr_info" | jq -r '.title')
pr_author=$(echo "$pr_info" | jq -r '.author.login')
pr_url=$(echo "$pr_info" | jq -r '.url')
pr_labels=$(echo "$pr_info" | jq -r '.labels[].name' | paste -sd ", " -)
if [ -z "$pr_labels" ]; then
pr_labels="(no labels)"
fi
# Add priority indicator
priority_indicator=""
if echo "$pr_labels" | grep -q "high priority"; then
priority_indicator="🔴 [HIGH PRIORITY] "
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔗 ${priority_indicator}PR #$pr_num: $pr_title"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "👤 Author: $pr_author"
echo "🏷️ Labels: $pr_labels"
echo "🔗 URL: $pr_url"
echo ""
# Get all runs for this PR
pr_runs=$(grep "^$pr_num|" "$pr_data_file")
pr_running_total=0
pr_queued_total=0
echo "$pr_runs" | while read -r line; do
workflow=$(echo "$line" | cut -d'|' -f2)
run_id=$(echo "$line" | cut -d'|' -f3)
status=$(echo "$line" | cut -d'|' -f4)
running=$(echo "$line" | cut -d'|' -f5)
queued=$(echo "$line" | cut -d'|' -f6)
runners=$(echo "$line" | cut -d'|' -f7)
queue_min=$(echo "$line" | cut -d'|' -f8)
created=$(echo "$line" | cut -d'|' -f9)
attempt=$(echo "$line" | cut -d'|' -f11)
pr_running_total=$((pr_running_total + running))
pr_queued_total=$((pr_queued_total + queued))
run_url="https://github.com/$REPO/actions/runs/$run_id"
# Calculate retry count for this specific run
retry_count=$((attempt - 1))
# Show retry indicator
retry_indicator=""
if [ "$retry_count" -gt 0 ]; then
retry_indicator=" 🔄 Retry #$retry_count"
fi
echo " 📦 Workflow: $workflow (Run #$run_id)$retry_indicator"
echo " Status: $status"
echo " 🟢 Running jobs: $running"
echo " 🟡 Queued jobs: $queued"
if [ "$running" -gt 0 ] && [ "$runners" != "" ]; then
echo " 🖥️ Runners: $runners"
fi
if [ "$queue_min" -gt 0 ]; then
echo " ⏱️ Queue time: ${queue_min} minutes"
fi
echo " 🔗 Run URL: $run_url"
echo ""
done
# Summary for this PR
pr_running_total=$(grep "^$pr_num|" "$pr_data_file" | cut -d'|' -f5 | awk '{sum+=$1} END {print sum+0}')
pr_queued_total=$(grep "^$pr_num|" "$pr_data_file" | cut -d'|' -f6 | awk '{sum+=$1} END {print sum+0}')
total_running=$((total_running + pr_running_total))
total_queued=$((total_queued + pr_queued_total))
echo " 📊 PR Total: $pr_running_total running, $pr_queued_total queued"
echo ""
done
# --- Non-PR Runs Section ---
non_pr_runs=$(grep '^NO_PR|' "$pr_data_file" 2>/dev/null || true)
non_pr_running=0
non_pr_queued=0
if [ -n "$non_pr_runs" ]; then
echo "========================================="
echo "📦 Non-PR Runs (manual / scheduled / other)"
echo "========================================="
echo ""
echo "$non_pr_runs" | while read -r line; do
workflow=$(echo "$line" | cut -d'|' -f2)
run_id=$(echo "$line" | cut -d'|' -f3)
status=$(echo "$line" | cut -d'|' -f4)
running=$(echo "$line" | cut -d'|' -f5)
queued=$(echo "$line" | cut -d'|' -f6)
runners=$(echo "$line" | cut -d'|' -f7)
queue_min=$(echo "$line" | cut -d'|' -f8)
created=$(echo "$line" | cut -d'|' -f9)
attempt=$(echo "$line" | cut -d'|' -f11)
event=$(echo "$line" | cut -d'|' -f12)
branch=$(echo "$line" | cut -d'|' -f13)
run_url="https://github.com/$REPO/actions/runs/$run_id"
retry_count=$((attempt - 1))
retry_indicator=""
if [ "$retry_count" -gt 0 ]; then
retry_indicator=" 🔄 Retry #$retry_count"
fi
echo " 📦 Workflow: $workflow (Run #$run_id)$retry_indicator"
echo " Event: $event"
echo " Branch: $branch"
echo " Status: $status"
echo " 🟢 Running jobs: $running"
echo " 🟡 Queued jobs: $queued"
if [ "$running" -gt 0 ] && [ "$runners" != "" ]; then
echo " 🖥️ Runners: $runners"
fi
if [ "$queue_min" -gt 0 ]; then
echo " ⏱️ Queue time: ${queue_min} minutes"
fi
echo " 🔗 Run URL: $run_url"
echo ""
done
non_pr_running=$(echo "$non_pr_runs" | cut -d'|' -f5 | awk '{sum+=$1} END {print sum+0}')
non_pr_queued=$(echo "$non_pr_runs" | cut -d'|' -f6 | awk '{sum+=$1} END {print sum+0}')
non_pr_count=$(echo "$non_pr_runs" | wc -l | tr -d ' ')
total_running=$((total_running + non_pr_running))
total_queued=$((total_queued + non_pr_queued))
echo " 📊 Non-PR Total: $non_pr_running running, $non_pr_queued queued"
echo ""
fi
# Overall summary
echo "========================================="
echo "📈 Overall Summary"
echo "========================================="
echo "Total PRs with active runs: $pr_count"
echo "Total non-PR active runs: ${non_pr_count:-0}"
echo "Total running jobs: $total_running"
echo "Total queued jobs: $total_queued"
echo "========================================="
# Cleanup
rm -f "$pr_data_file"
+466
View File
@@ -0,0 +1,466 @@
name: Nightly Test (GB200 72GPU)
# NOTE: Nightly (schedule) runs require no approval.
# Manual (workflow_dispatch) runs are gated by the gb200-ci environment
# to prevent individuals from queuing arbitrary jobs on the shared GB200 cluster.
on:
schedule:
- cron: '0 2 * * *' # 2 AM UTC daily (offset from other nightly runs)
workflow_dispatch: # allow manual trigger; gated by gb200-ci environment
inputs:
image:
description: 'Optional. SGLang Docker image to benchmark. Leave empty for the default nightly image. Mutually exclusive with pr_number and sglang_branch.'
required: false
default: ''
pr_number:
description: 'Optional. PR number to build from (works for PRs from forks too, via refs/pull/<N>/head). Preferred over sglang_branch when a PR exists. Mutually exclusive with image and sglang_branch.'
required: false
default: ''
sglang_branch:
description: 'Optional. Branch name on sgl-project/sglang to build from (use when no PR is open yet). For fork branches, open a PR and use pr_number instead. Mutually exclusive with image and pr_number.'
required: false
default: ''
configs:
description: 'Optional. Comma-separated names to run only a subset. Format: {model-prefix}-{precision}-{isl}{osl}-{recipe}. E.g. "dsr1-fp8-1k1k-max-tpt" or "dsr1-fp8-1k1k-max-tpt,dsr1-fp4-1k1k-mid-curve". Leave empty to run all. Available names are listed in the setup job log.'
required: false
default: ''
concurrency:
group: nightly-test-gb200
cancel-in-progress: false
env:
SGLANG_IS_IN_CI: true
SGLANG_ENABLE_ASYNC_ASSERT: true
SRT_SLURM_BRANCH: sglang-nightly-regression
SLURM_PARTITION: batch
SLURM_ACCOUNT: sglang
# Docker Hub repo for ephemeral branch/PR build images (kept separate from
# the released `lmsysorg/sglang` repo). Cleaned up by `cleanup-image`.
CI_IMAGE_REPO: lmsysorg/sglang-staging
# How many most recent staging tags to retain after each run.
CI_IMAGE_KEEP_TAGS: 60
jobs:
# ---------------------------------------------------------------------------
# Reject conflicting inputs early. At most one of `image`, `pr_number`,
# `sglang_branch` may be set — they select different image sources. Only runs
# on manual dispatch; all downstream jobs chain through this so invalid
# inputs halt the pipeline before cluster resources are reserved.
# ---------------------------------------------------------------------------
validate-inputs:
if: github.repository == 'sgl-project/sglang' && github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Reject conflicting inputs
run: |
IMAGE="${{ inputs.image }}"
PR="${{ inputs.pr_number }}"
BRANCH="${{ inputs.sglang_branch }}"
sources=0
[ -n "$IMAGE" ] && sources=$((sources + 1))
[ -n "$PR" ] && sources=$((sources + 1))
[ -n "$BRANCH" ] && sources=$((sources + 1))
if [ "$sources" -gt 1 ]; then
echo "::error::Specify at most one of 'image' ('$IMAGE'), 'pr_number' ('$PR'), or 'sglang_branch' ('$BRANCH')."
exit 1
fi
if [ -n "$PR" ] && ! echo "$PR" | grep -Eq '^[0-9]+$'; then
echo "::error::pr_number must be a positive integer, got '$PR'."
exit 1
fi
# ---------------------------------------------------------------------------
# Reads scripts/ci/slurm/nightly-configs.yaml and generates one matrix entry
# per recipe YAML. Each job runs the full concurrency sweep defined in the
# recipe as a single Slurm job.
# To add/remove configs, edit nightly-configs.yaml only.
# ---------------------------------------------------------------------------
setup:
needs: validate-inputs
# Run if validate-inputs succeeded (dispatch) or was skipped (cron).
if: |
always() && github.repository == 'sgl-project/sglang'
&& (needs.validate-inputs.result == 'success' || needs.validate-inputs.result == 'skipped')
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate benchmark matrix
id: generate
env:
CONFIGS_FILTER: ${{ inputs.configs }}
run: |
pip install pyyaml -q
# List all available config names first so they're visible in logs
# even when a filter rejects an unknown name.
ALL_MATRIX=$(python3 scripts/ci/slurm/generate_matrix.py \
scripts/ci/slurm/nightly-configs.yaml --runner gb200)
echo "Available config names for runner gb200:"
echo "$ALL_MATRIX" | python3 -c "import json,sys; [print(f' - {e[\"name\"]}') for e in json.load(sys.stdin)]"
FILTER_ARG=()
if [ -n "$CONFIGS_FILTER" ]; then
echo ""
echo "Filtering to: $CONFIGS_FILTER"
FILTER_ARG=(--filter "$CONFIGS_FILTER")
fi
MATRIX=$(python3 scripts/ci/slurm/generate_matrix.py \
scripts/ci/slurm/nightly-configs.yaml --runner gb200 "${FILTER_ARG[@]}")
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
# ---------------------------------------------------------------------------
# When pr_number or sglang_branch is provided, build an ARM64 (GB200) image
# from that ref and push it to Docker Hub under lmsysorg/sglang-staging.
# Uses refs/pull/<N>/head for PRs so fork PRs work without cross-repo auth.
# Old staging tags are pruned by `cleanup-image` at the end of the run.
# Skipped on nightly (cron) runs and manual runs with neither pr_number nor
# sglang_branch.
# ---------------------------------------------------------------------------
build-image:
needs: [validate-inputs, setup]
if: |
github.repository == 'sgl-project/sglang' && github.event_name == 'workflow_dispatch'
&& (inputs.pr_number != '' || inputs.sglang_branch != '')
runs-on: arm-docker-build-node
outputs:
image_ref: ${{ steps.build.outputs.image_ref }}
image_tag: ${{ steps.build.outputs.image_tag }}
steps:
# Self-hosted runners retain the workspace across jobs. Prior `docker buildx`
# runs on this node leave root-owned build artifacts (e.g. sgl-kernel/build/)
# that actions/checkout cannot remove, causing EACCES on rmdir. Wipe them
# via a throwaway root container before checkout recreates the workspace.
- name: Clean workspace (remove root-owned files from prior runs)
run: |
docker run --rm -v "${{ github.workspace }}:/workspace" alpine:3 \
sh -c 'rm -rf /workspace/..?* /workspace/.[!.]* /workspace/*' || true
- name: Checkout code
uses: actions/checkout@v4
with:
# PRs (including fork PRs) resolve via refs/pull/<N>/head on upstream.
# Otherwise fall back to the branch name on sgl-project/sglang.
ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || inputs.sglang_branch }}
- name: Verify checkout
env:
PR_NUMBER: ${{ inputs.pr_number }}
BRANCH: ${{ inputs.sglang_branch }}
run: |
SHA=$(git rev-parse HEAD)
echo "Commit SHA: $SHA"
echo "Author: $(git log -1 --format='%an <%ae>')"
echo "Date: $(git log -1 --format='%aI')"
echo "Subject: $(git log -1 --format='%s')"
echo ""
if [ -n "$PR_NUMBER" ]; then
echo "Cross-check: https://github.com/sgl-project/sglang/pull/${PR_NUMBER}/commits"
else
echo "Cross-check: https://github.com/sgl-project/sglang/commits/${BRANCH}"
fi
echo "Commit URL: https://github.com/sgl-project/sglang/commit/${SHA}"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push ARM64 image
id: build
run: |
if [ -n "${{ inputs.pr_number }}" ]; then
TAG_STUB="pr-${{ inputs.pr_number }}"
SOURCE_DESC="PR #${{ inputs.pr_number }}"
else
TAG_STUB=$(echo "${{ inputs.sglang_branch }}" | tr '/' '-' | tr -cd '[:alnum:]._-')
SOURCE_DESC="branch ${{ inputs.sglang_branch }}"
fi
# run_attempt disambiguates "Re-run jobs" so the squash filename
# (derived from the image URL) doesn't collide with a stale one.
TAG="${TAG_STUB}-${{ github.run_id }}-${{ github.run_attempt }}"
IMAGE_REF="${CI_IMAGE_REPO}:${TAG}"
echo "Building ${IMAGE_REF} from ${SOURCE_DESC}"
docker buildx build \
--platform linux/arm64 \
--output type=image,name=${IMAGE_REF},push=true \
--target framework_final \
-f docker/Dockerfile \
--build-arg CUDA_VERSION=13.0.1 \
--build-arg BUILD_TYPE=all \
--build-arg CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) \
--build-arg GRACE_BLACKWELL=1 \
--build-arg BRANCH_TYPE=local \
--build-arg INSTALL_FLASHINFER_JIT_CACHE=1 \
--no-cache \
.
echo "image_ref=${IMAGE_REF}" >> $GITHUB_OUTPUT
echo "image_tag=${TAG}" >> $GITHUB_OUTPUT
# ---------------------------------------------------------------------------
# Import Docker images to Lustre squash files once before all benchmark jobs.
# This avoids parallel jobs racing to enroot import the same image.
# When build-image ran, we import the freshly built Docker Hub staging image
# (lmsysorg/sglang-staging is public → no auth needed for enroot pull).
# Otherwise we use the `image` input (or its default public nightly image).
# ---------------------------------------------------------------------------
prepare-image:
needs: [setup, build-image]
if: |
always() && github.repository == 'sgl-project/sglang'
&& needs.setup.result == 'success'
&& (needs.build-image.result == 'success' || needs.build-image.result == 'skipped')
environment: ${{ github.event_name == 'workflow_dispatch' && 'gb200-ci' || '' }}
runs-on: 72-gpu-gb200
outputs:
squash_file: ${{ steps.import.outputs.squash_file }}
nginx_squash_file: ${{ steps.import.outputs.nginx_squash_file }}
image: ${{ steps.resolve.outputs.image }}
env:
NGINX_IMAGE: nginx:1.27.4
steps:
- name: Resolve image to import
id: resolve
run: |
BUILT_IMAGE="${{ needs.build-image.outputs.image_ref }}"
if [ -n "$BUILT_IMAGE" ]; then
echo "Using freshly built image: $BUILT_IMAGE"
echo "image=$BUILT_IMAGE" >> $GITHUB_OUTPUT
else
IMAGE="${{ inputs.image || 'lmsysorg/sglang:dev-cu13' }}"
echo "Using pre-existing image: $IMAGE"
echo "image=$IMAGE" >> $GITHUB_OUTPUT
fi
- name: Import Docker images to Lustre
id: import
env:
IMAGE: ${{ steps.resolve.outputs.image }}
run: |
SQUASH_FILE="/mnt/lustre01/users-public/sglang-ci/$(echo "$IMAGE" | sed 's/[\/:@#]/_/g')_$(date +%Y%m%d).sqsh"
NGINX_SQUASH_FILE="/mnt/lustre01/users-public/sglang-ci/$(echo "$NGINX_IMAGE" | sed 's/[\/:@#]/_/g').sqsh"
if [ -f "$SQUASH_FILE" ]; then
echo "Squash file already exists, skipping import: $SQUASH_FILE"
else
enroot import -o "$SQUASH_FILE" "docker://$IMAGE"
fi
if [ -f "$NGINX_SQUASH_FILE" ]; then
echo "Nginx squash file already exists, skipping import: $NGINX_SQUASH_FILE"
else
enroot import -o "$NGINX_SQUASH_FILE" "docker://$NGINX_IMAGE"
fi
echo "squash_file=$SQUASH_FILE" >> $GITHUB_OUTPUT
echo "nginx_squash_file=$NGINX_SQUASH_FILE" >> $GITHUB_OUTPUT
nightly-gb200-benchmark:
needs: [setup, prepare-image]
# Use always() + explicit success checks so a skipped transitive upstream
# (e.g. build-image when neither pr_number nor sglang_branch is set) does
# not propagate a skip to this job. Direct deps must still have succeeded.
if: |
always() && github.repository == 'sgl-project/sglang'
&& needs.setup.result == 'success'
&& needs.prepare-image.result == 'success'
runs-on: 72-gpu-gb200
strategy:
fail-fast: false
matrix:
config: ${{ fromJson(needs.setup.outputs.matrix) }}
env:
FRAMEWORK: dynamo-sglang
MODEL: ${{ matrix.config.model }}
MODEL_PREFIX: ${{ matrix.config.model_prefix }}
PRECISION: ${{ matrix.config.precision }}
ISL: ${{ matrix.config.isl }}
OSL: ${{ matrix.config.osl }}
CONFIG_FILE: ${{ matrix.config.config_file }}
RESULT_FILENAME: gb200-${{ matrix.config.name }}
MATRIX_CONFIG_NAME: ${{ matrix.config.name }}
SQUASH_FILE: ${{ needs.prepare-image.outputs.squash_file }}
NGINX_SQUASH_FILE: ${{ needs.prepare-image.outputs.nginx_squash_file }}
# S3 log-upload credentials — consumed by srt-slurm's postprocess stage
# to upload /logs after each Slurm job; prefix derived in launch_gb200.sh.
AWS_ACCESS_KEY_ID: ${{ secrets.NV_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.NV_S3_SECRET_ACCESS_KEY }}
S3_BUCKET: ${{ secrets.NV_S3_BUCKET }}
S3_ENDPOINT_URL: ${{ secrets.NV_S3_ENDPOINT_URL }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Clean up prior Slurm jobs from this runner
continue-on-error: true
env:
RUNNER_NAME: ${{ runner.name }}
run: |
STALE_JOBS=$(squeue --noheader --format="%i %j" | grep "${RUNNER_NAME}" | awk '{print $1}')
if [ -n "$STALE_JOBS" ]; then
echo "Cancelling stale jobs: $STALE_JOBS"
scancel $STALE_JOBS
fi
- name: Launch GB200 benchmark via srt-slurm
timeout-minutes: 360
env:
RUNNER_NAME: ${{ runner.name }}
run: bash scripts/ci/slurm/launch_gb200.sh
- name: Process results
if: always()
env:
RUNNER_NAME: ${{ runner.name }}
run: |
pip install tabulate pyyaml -q
SRT_REPO_DIR="/mnt/lustre01/users-public/sglang-ci/workspace/${RUNNER_NAME}/srt-slurm"
for result_file in ${{ github.workspace }}/${RESULT_FILENAME}_*.json; do
[ -f "$result_file" ] || continue
basename_file=$(basename "$result_file")
ctx=$(echo "$basename_file" | sed -n 's/.*_ctx_\([0-9]*\)_gen.*/\1/p')
gen=$(echo "$basename_file" | sed -n 's/.*_gen_\([0-9]*\)\.json/\1/p')
[ -n "$ctx" ] && [ -n "$gen" ] || continue
RESULT_FILENAME="${result_file%.json}" PREFILL_GPUS="$ctx" DECODE_GPUS="$gen" \
RECIPE_FILE="$SRT_REPO_DIR/$CONFIG_FILE" \
python3 scripts/ci/slurm/process_result.py
done
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: gb200-${{ matrix.config.name }}-${{ github.run_id }}
path: |
${{ github.workspace }}/*.json
${{ github.workspace }}/multinode_server_logs.tar.gz
retention-days: 30
if-no-files-found: warn
- name: Analyze logs with AI on failure
if: failure()
continue-on-error: true
env:
MODAL_TOKEN_ID: ${{ secrets.NV_MODAL_TOKEN_ID }}
MODAL_TOKEN_SECRET: ${{ secrets.NV_MODAL_TOKEN_SECRET }}
run: |
TARBALL="${{ github.workspace }}/multinode_server_logs.tar.gz"
if [ -f "$TARBALL" ]; then
uv run --with modal python scripts/ci/slurm/analyze_logs_with_modal.py \
--tarball "$TARBALL" \
--job-id "${{ matrix.config.name }}-${{ github.run_id }}" \
--output "${{ github.workspace }}/ai_analysis.md"
if [ -f "${{ github.workspace }}/ai_analysis.md" ]; then
echo "## AI Log Analysis" >> $GITHUB_STEP_SUMMARY
cat "${{ github.workspace }}/ai_analysis.md" >> $GITHUB_STEP_SUMMARY
fi
else
echo "No log tarball found, skipping analysis"
fi
- name: Upload AI analysis to S3
if: failure()
continue-on-error: true
env:
ISL: ${{ matrix.config.isl }}
OSL: ${{ matrix.config.osl }}
run: |
ANALYSIS="${{ github.workspace }}/ai_analysis.md"
[ -f "$ANALYSIS" ] || { echo "no ai_analysis.md, skipping"; exit 0; }
case "${{ github.event_name }}" in
schedule) TRIGGER=cron ;;
workflow_dispatch) TRIGGER=manual ;;
*) TRIGGER="${{ github.event_name }}" ;;
esac
fmt() { if [ $(( $1 % 1024 )) -eq 0 ]; then echo "$(( $1 / 1024 ))k"; else echo "$1"; fi; }
SEQ_LEN="$(fmt "$ISL")$(fmt "$OSL")"
KEY="${TRIGGER}/${{ github.run_id }}-${{ github.run_attempt }}/${SEQ_LEN}/${{ matrix.config.name }}/ai_analysis.md"
aws --endpoint-url "$S3_ENDPOINT_URL" s3 cp "$ANALYSIS" "s3://${S3_BUCKET}/${KEY}"
echo "uploaded to s3://${S3_BUCKET}/${KEY}"
- name: Clean up Slurm jobs on failure/cancel
if: failure() || cancelled()
continue-on-error: true
env:
RUNNER_NAME: ${{ runner.name }}
run: |
ACTIVE_JOBS=$(squeue --noheader --format="%i %j" | grep "${RUNNER_NAME}" | awk '{print $1}')
if [ -n "$ACTIVE_JOBS" ]; then
echo "Cancelling jobs: $ACTIVE_JOBS"
scancel $ACTIVE_JOBS
fi
collect-results:
needs: nightly-gb200-benchmark
if: github.repository == 'sgl-project/sglang' && always()
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: results/
pattern: gb200-*
- name: Print summary
run: |
pip install tabulate -q
python3 scripts/ci/slurm/summarize.py results/ >> $GITHUB_STEP_SUMMARY
# ---------------------------------------------------------------------------
# Prune old tags in the staging repo, keeping only the most recent N. Mirrors
# the pattern used by release-docker-dev.yml. Runs after benchmarks so the
# freshly built image (whose sqsh is already on Lustre) becomes a regular
# aged-out tag over time. No-op when the repo has ≤ CI_IMAGE_KEEP_TAGS tags.
# ---------------------------------------------------------------------------
cleanup-image:
needs: [build-image, nightly-gb200-benchmark]
if: always() && needs.build-image.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Prune old staging tags on Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
TOKEN=$(curl -s -H "Content-Type: application/json" \
-X POST -d "{\"username\": \"${DOCKERHUB_USERNAME}\", \"password\": \"${DOCKERHUB_TOKEN}\"}" \
https://hub.docker.com/v2/users/login/ | jq -r .token)
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "::error::Docker Hub login failed"
exit 1
fi
TAGS_RESPONSE=$(curl -s -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/${CI_IMAGE_REPO}/tags/?page_size=100")
# Sort tags by last_updated (newest first), keep names only.
TAGS=$(echo "$TAGS_RESPONSE" | jq -r \
'.results[] | "\(.last_updated)|\(.name)"' \
| sort -r | cut -d'|' -f2)
TAG_COUNT=$(echo "$TAGS" | grep -c . || true)
if [ "$TAG_COUNT" -gt "$CI_IMAGE_KEEP_TAGS" ]; then
echo "Found $TAG_COUNT tags in ${CI_IMAGE_REPO}, keeping $CI_IMAGE_KEEP_TAGS most recent"
TAGS_TO_DELETE=$(echo "$TAGS" | tail -n +$((CI_IMAGE_KEEP_TAGS + 1)))
for tag in $TAGS_TO_DELETE; do
echo "Deleting ${CI_IMAGE_REPO}:${tag}"
curl -s -X DELETE -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/${CI_IMAGE_REPO}/tags/${tag}/"
done
else
echo "Only $TAG_COUNT tags in ${CI_IMAGE_REPO}, no cleanup needed"
fi
@@ -0,0 +1,250 @@
name: Nightly Test (AMD MI355X 2N 1P1D Disagg)
# A self-hosted runner on the amd-sglang login node salloc's two MI355X nodes
# and runs the Docker harness (prefill + decode over MORI, a standalone load
# balancer, then an sglang.bench_serving concurrency sweep) via
# scripts/ci/slurm/launch_mi355x.sh.
on:
schedule:
- cron: '17 3 * * *' # 03:17 UTC
workflow_dispatch:
inputs:
image:
description: 'Optional. ROCm SGLang Docker image to benchmark. Leave empty for the recipe default.'
required: false
default: ''
configs:
description: 'Optional. Comma-separated names to run only a subset. Leave empty to run all. Available names are listed in the setup job log.'
required: false
default: ''
concurrency:
group: nightly-test-mi355x
cancel-in-progress: false
env:
SGLANG_IS_IN_CI: true
jobs:
# ---------------------------------------------------------------------------
# Reads scripts/ci/slurm/nightly-configs.yaml and generates one matrix entry
# per recipe YAML for runner=mi355x. To add/remove configs, edit
# nightly-configs.yaml only.
# ---------------------------------------------------------------------------
setup:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
image: ${{ steps.image.outputs.image }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Resolve latest MI35x nightly image
id: image
# Track the newest published image so the nightly actually catches
# regressions instead of re-testing one pinned build. We resolve on the
# GitHub-hosted runner (clean Docker Hub access, no rate limits) and pass
# the tag to the self-hosted runner. Scope is deliberately tight: only
# the clean main MI35x ROCm 7.2 line
# `lmsysorg/sglang-rocm:v*-rocm720-mi35x-YYYYMMDD` (excludes -test-,
# miles-, mori-, vllm-, rocm7_14 variants). On any failure we emit an
# empty string and the launcher falls back to the recipe's pinned
# `runtime.image`.
run: |
set -o pipefail
RESOLVED=$(curl -fsSL "https://hub.docker.com/v2/repositories/lmsysorg/sglang-rocm/tags?page_size=100&ordering=last_updated" \
| python3 -c "import json,re,sys;p=re.compile(r'^v[0-9][0-9A-Za-z.]*-rocm720-mi35x-([0-9]{8})\$');c=sorted((m.group(1),t['name']) for t in json.load(sys.stdin).get('results',[]) for m in [p.match(t.get('name',''))] if m);print(c[-1][1] if c else '')" || true)
if [ -n "$RESOLVED" ]; then
echo "Resolved latest MI35x image: lmsysorg/sglang-rocm:$RESOLVED"
echo "image=lmsysorg/sglang-rocm:$RESOLVED" >> $GITHUB_OUTPUT
else
echo "Could not resolve a latest image; launcher will use recipe default."
echo "image=" >> $GITHUB_OUTPUT
fi
- name: Generate benchmark matrix
id: generate
env:
CONFIGS_FILTER: ${{ inputs.configs }}
run: |
pip install pyyaml -q
ALL_MATRIX=$(python3 scripts/ci/slurm/generate_matrix.py \
scripts/ci/slurm/nightly-configs.yaml --runner mi355x)
echo "Available config names for runner mi355x:"
echo "$ALL_MATRIX" | python3 -c "import json,sys; [print(f' - {e[\"name\"]}') for e in json.load(sys.stdin)]"
FILTER_ARG=()
if [ -n "$CONFIGS_FILTER" ]; then
echo ""
echo "Filtering to: $CONFIGS_FILTER"
FILTER_ARG=(--filter "$CONFIGS_FILTER")
fi
MATRIX=$(python3 scripts/ci/slurm/generate_matrix.py \
scripts/ci/slurm/nightly-configs.yaml --runner mi355x "${FILTER_ARG[@]}")
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
nightly-mi355x-benchmark:
needs: setup
if: |
always() && github.repository == 'sgl-project/sglang'
&& needs.setup.result == 'success'
runs-on: linux-sglang-mi35x-di
strategy:
fail-fast: false
matrix:
config: ${{ fromJson(needs.setup.outputs.matrix) }}
env:
FRAMEWORK: sglang
HW: mi355x
MODEL: ${{ matrix.config.model }}
MODEL_PREFIX: ${{ matrix.config.model_prefix }}
PRECISION: ${{ matrix.config.precision }}
ISL: ${{ matrix.config.isl }}
OSL: ${{ matrix.config.osl }}
CONFIG_FILE: ${{ matrix.config.config_file }}
RESULT_FILENAME: mi355x-${{ matrix.config.name }}
MATRIX_CONFIG_NAME: ${{ matrix.config.name }}
# NOTE: RUNNER_NAME is a built-in default env var on every runner, so the
# launch + cleanup steps read it directly. Do NOT set it here from
# ${{ runner.name }} -- the `runner` context is not available in job-level
# env and makes the whole workflow file invalid.
# Shared-NFS HuggingFace cache dir; the launcher resolves the live snapshot
# via refs/main. Per-model value comes from nightly-configs.yaml model_path.
MODEL_PATH: ${{ matrix.config.model_path }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Clean up prior Slurm jobs from this runner
continue-on-error: true
run: |
# launch_mi355x.sh names the allocation
# mi355x-ci-<RUNNER_NAME>-<GITHUB_RUN_ID>-<config>
# Here we clear THIS runner's leftovers from a crashed previous run, so
# we match the RUNNER_NAME prefix (older runs have a different run id).
# Never a blanket `squeue --me`, which would kill a concurrent leg.
# %200j: squeue truncates the job name (%j) by default -- widen it so
# the grep sees the full name.
if [ -z "${RUNNER_NAME:-}" ]; then echo "RUNNER_NAME unset; skipping"; exit 0; fi
STALE_JOBS=$(squeue --me --noheader --format="%i %200j" | grep -F "mi355x-ci-${RUNNER_NAME}-" | awk '{print $1}' || true)
if [ -n "$STALE_JOBS" ]; then
echo "Cancelling stale jobs for ${RUNNER_NAME}: $STALE_JOBS"
scancel $STALE_JOBS
fi
- name: Launch MI355X 2N 1P1D benchmark
timeout-minutes: 180
env:
# Manual dispatch input wins; otherwise use the latest image resolved
# by the setup job; otherwise the launcher falls back to the recipe default.
IMAGE_OVERRIDE: ${{ inputs.image != '' && inputs.image || needs.setup.outputs.image }}
# Keep the scheduler off mia1-p01-g20: its ionic RDMA driver ABI
# mismatches the container, so MORI reports "no active RDMA device"
# and the disagg server dies on init. Remove once the node is fixed.
SLURM_EXCLUDE: mia1-p01-g20
run: bash scripts/ci/slurm/launch_mi355x.sh
- name: Pack logs
if: always()
run: |
LOG_DIR="$HOME/.mi355x_ci/${MATRIX_CONFIG_NAME}"
LOG_BUNDLE="${GITHUB_WORKSPACE}/${RESULT_FILENAME}_logs.tar.gz"
if [ ! -d "$LOG_DIR" ]; then
echo "WARN: log directory not found: $LOG_DIR"
exit 0
fi
mapfile -d '' LOG_FILES < <(
find "$LOG_DIR" -maxdepth 1 -type f \
\( -name '*.log' -o -name 'server_exit_*' -o -name 'bench_exit' \) \
-printf '%P\0' | sort -z
)
if [ "${#LOG_FILES[@]}" -eq 0 ]; then
echo "WARN: no log files found in $LOG_DIR"
exit 0
fi
STAGING_DIR="$(mktemp -d)"
trap 'rm -rf "$STAGING_DIR"' EXIT
for log_file in "${LOG_FILES[@]}"; do
cp -a "$LOG_DIR/$log_file" "$STAGING_DIR/$log_file" || true
done
tar czf "$LOG_BUNDLE" -C "$STAGING_DIR" .
echo "Packed ${#LOG_FILES[@]} log file(s) -> $LOG_BUNDLE"
- name: Process results
if: always()
run: |
pip install tabulate pyyaml -q
for result_file in ${{ github.workspace }}/${RESULT_FILENAME}_*.json; do
[ -f "$result_file" ] || continue
basename_file=$(basename "$result_file")
ctx=$(echo "$basename_file" | sed -n 's/.*_ctx_\([0-9]*\)_gen.*/\1/p')
gen=$(echo "$basename_file" | sed -n 's/.*_gen_\([0-9]*\)\.json/\1/p')
[ -n "$ctx" ] && [ -n "$gen" ] || continue
RESULT_FILENAME="${result_file%.json}" PREFILL_GPUS="$ctx" DECODE_GPUS="$gen" \
RECIPE_FILE="${{ github.workspace }}/$CONFIG_FILE" \
python3 scripts/ci/slurm/process_result.py
done
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: mi355x-${{ matrix.config.name }}-${{ github.run_id }}
path: |
${{ github.workspace }}/*.json
${{ github.workspace }}/*_logs.tar.gz
retention-days: 30
if-no-files-found: warn
- name: Clean up Slurm jobs on failure/cancel
if: failure() || cancelled()
continue-on-error: true
run: |
# Cancel only THIS leg's allocation -- match the full job name
# mi355x-ci-<RUNNER_NAME>-<GITHUB_RUN_ID>-<config>. GITHUB_RUN_ID +
# config are unique per matrix leg, so a concurrent leg is never hit.
if [ -z "${RUNNER_NAME:-}" ]; then echo "RUNNER_NAME unset; skipping"; exit 0; fi
JOB_TAG="mi355x-ci-${RUNNER_NAME}-${GITHUB_RUN_ID}-${MATRIX_CONFIG_NAME}"
ACTIVE_JOBS=$(squeue --me --noheader --format="%i %200j" | grep -F "$JOB_TAG" | awk '{print $1}' || true)
if [ -n "$ACTIVE_JOBS" ]; then
echo "Cancelling jobs for ${JOB_TAG}: $ACTIVE_JOBS"
scancel $ACTIVE_JOBS
fi
- name: Clean up MI355X scratch
if: always()
continue-on-error: true
run: |
LOG_DIR="$HOME/.mi355x_ci/${MATRIX_CONFIG_NAME}"
if [ -d "$LOG_DIR" ]; then
echo "Removing MI355X scratch directory: $LOG_DIR"
rm -rf "$LOG_DIR"
fi
collect-results:
needs: nightly-mi355x-benchmark
if: github.repository == 'sgl-project/sglang' && always()
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: results/
pattern: mi355x-*
- name: Print summary
run: |
pip install tabulate -q
python3 scripts/ci/slurm/summarize.py results/ >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,95 @@
name: Nightly Experimental sgl-router Docker Image
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
env:
IMAGE_REPO: lmsysorg/sglang-staging
IMAGE_TAG: experimental-router-dev
jobs:
# Build each arch natively (no QEMU emulation) and push the image by digest.
# The manifest job then stitches the per-arch digests into one multi-arch
# tag, so `docker pull` resolves the right arch automatically.
build:
if: github.repository == 'sgl-project/sglang'
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
arch: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest
run: |
docker buildx build . -f docker/sgl-router.Dockerfile \
--platform ${{ matrix.platform }} \
--output "type=image,name=${IMAGE_REPO},push-by-digest=true,name-canonical=true,push=true" \
--metadata-file /tmp/metadata.json
DIGEST=$(python3 -c "import json; print(json.load(open('/tmp/metadata.json'))['containerimage.digest'])")
echo "Pushed ${{ matrix.platform }} digest: ${DIGEST}"
mkdir -p /tmp/digests
echo "${DIGEST}" > "/tmp/digests/${{ matrix.arch }}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.arch }}
path: /tmp/digests/${{ matrix.arch }}
retention-days: 1
create-manifest:
if: github.repository == 'sgl-project/sglang'
needs: build
runs-on: ubuntu-24.04
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push multi-arch manifest
run: |
AMD64_DIGEST=$(cat /tmp/digests/amd64)
ARM64_DIGEST=$(cat /tmp/digests/arm64)
docker buildx imagetools create \
-t "${IMAGE_REPO}:${IMAGE_TAG}" \
"${IMAGE_REPO}@${AMD64_DIGEST}" \
"${IMAGE_REPO}@${ARM64_DIGEST}"
- name: Inspect manifest
run: docker buildx imagetools inspect "${IMAGE_REPO}:${IMAGE_TAG}"
+32
View File
@@ -0,0 +1,32 @@
name: Nightly Link Check
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
concurrency:
group: nightly-link-check-${{ github.ref }}
cancel-in-progress: true
jobs:
lychee-online:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run lychee online link checks
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
with:
fail: true
args: >-
--config .github/linters/lychee-ci.toml
README.md
docs/**/*.md
docs/**/*.rst
docs/**/*.ipynb
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,196 @@
# Nightly release workflow for SGLang Model Gateway
name: Nightly Release SGLang Model Gateway to PyPI
on:
schedule:
# Run at 2 AM UTC every day
- cron: '0 2 * * *'
workflow_dispatch: # Allow manual trigger
jobs:
build:
name: build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }})
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
target: [x86_64, aarch64]
manylinux: [auto]
include:
- os: ubuntu
platform: linux
- os: windows
ls: dir
target: x86_64
python-architecture: x64
interpreter: 3.9 3.10 3.11 3.12 3.13
- os: macos
target: aarch64
interpreter: 3.9 3.10 3.11 3.12 3.13
- os: ubuntu
platform: linux
target: aarch64
# musllinux
- os: ubuntu
platform: linux
target: x86_64
manylinux: musllinux_1_1
- os: ubuntu
platform: linux
target: aarch64
manylinux: musllinux_1_1
exclude:
- os: windows
target: aarch64
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root and delete sglang-repo
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
ls -alt
shell: bash
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
architecture: ${{ matrix.python-architecture || 'x64' }}
- name: Modify version for nightly release
run: |
# Get current version from pyproject.toml
CURRENT_VERSION=$(python -c "import tomllib; print(tomllib.load(open('bindings/python/pyproject.toml', 'rb'))['project']['version'])" 2>/dev/null || python -c "import tomli; print(tomli.load(open('bindings/python/pyproject.toml', 'rb'))['project']['version'])")
# Create nightly version with date: e.g., 0.2.1.dev20250128
NIGHTLY_VERSION="${CURRENT_VERSION}.dev$(date +%Y%m%d)"
echo "Nightly version: $NIGHTLY_VERSION"
# Update pyproject.toml with nightly version (temporary, not committed)
sed -i.bak "s/version = \"${CURRENT_VERSION}\"/version = \"${NIGHTLY_VERSION}\"/" bindings/python/pyproject.toml
# Verify the change
cat bindings/python/pyproject.toml | grep "^version"
shell: bash
- name: Install twine and tomli
run: pip install -U twine tomli
- name: Install protoc (macOS)
if: matrix.os == 'macos'
run: brew install protobuf
- name: Install protoc (Windows)
if: matrix.os == 'windows'
run: choco install protoc -y
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
working-directory: bindings/python
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux || 'auto' }}
args: --release --out dist --features vendored-openssl --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 3.14' }}
rust-toolchain: stable
docker-options: -e CI -e CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc -e CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
before-script-linux: |
# Install build dependencies (perl/make for vendored OpenSSL, protoc for gRPC)
if command -v yum &> /dev/null; then
yum update -y && yum install -y wget unzip gcc gcc-c++ perl-core make
# Install cross-compilation toolchain for aarch64 if needed
if [ "${{ matrix.target }}" = "aarch64" ]; then
yum install -y gcc-aarch64-linux-gnu gcc-c++-aarch64-linux-gnu || true
fi
elif command -v apt-get &> /dev/null; then
apt-get update && apt-get install -y wget unzip gcc g++ perl make
# Install cross-compilation toolchain for aarch64 if needed
if [ "${{ matrix.target }}" = "aarch64" ]; then
apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu || true
fi
fi
(cd /tmp && \
wget https://github.com/protocolbuffers/protobuf/releases/download/v32.0/protoc-32.0-linux-x86_64.zip && \
unzip protoc-32.0-linux-x86_64.zip -d /usr/local && \
rm protoc-32.0-linux-x86_64.zip)
protoc --version
- name: List built packages
run: ${{ matrix.ls || 'ls -lh' }} bindings/python/dist/
- name: Check packages
run: twine check --strict bindings/python/dist/*
- uses: actions/upload-artifact@v4
with:
name: packages-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'auto' }}
path: bindings/python/dist/
build-sdist:
name: Build SDist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root and delete sglang-repo
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
ls -alt
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Modify version for nightly release
run: |
# Get current version from pyproject.toml
CURRENT_VERSION=$(python -c "import tomllib; print(tomllib.load(open('bindings/python/pyproject.toml', 'rb'))['project']['version'])" 2>/dev/null || python -c "import tomli; print(tomli.load(open('bindings/python/pyproject.toml', 'rb'))['project']['version'])")
# Create nightly version with date: e.g., 0.2.1.dev20250128
NIGHTLY_VERSION="${CURRENT_VERSION}.dev$(date +%Y%m%d)"
echo "Nightly version: $NIGHTLY_VERSION"
# Update pyproject.toml with nightly version (temporary, not committed)
sed -i "s/version = \"${CURRENT_VERSION}\"/version = \"${NIGHTLY_VERSION}\"/" bindings/python/pyproject.toml
# Verify the change
cat bindings/python/pyproject.toml | grep "^version"
- name: Build SDist
uses: PyO3/maturin-action@v1
with:
working-directory: bindings/python
command: sdist
args: --out dist
rust-toolchain: stable
- uses: actions/upload-artifact@v4
with:
name: sdist
path: bindings/python/dist/*.tar.gz
upload:
name: Upload to TestPyPI
if: github.repository == 'sgl-project/sglang' # Ensure this job only runs for the sgl-project/sglang repository
needs: [build, build-sdist]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Upload to TestPyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN_ROUTER }}
run: |
pip install twine
twine upload --repository testpypi dist/* --verbose
@@ -0,0 +1,93 @@
name: Nightly Test Miles (AMD ROCm 7.0)
on:
schedule:
# Runs after the 12:00 UTC miles image build has pushed the dated image.
- cron: "30 17 * * *"
workflow_dispatch:
inputs:
image_tag:
description: "Override miles image tag (e.g. miles-rocm700-mi35x-20260621); empty = newest by date"
required: false
type: string
default: ""
continue_on_error:
description: "Continue on error (do not fail the workflow on test failures)"
required: false
type: boolean
default: true
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || github.ref }}
cancel-in-progress: false
jobs:
test-miles-mi35x:
if: github.repository == 'sgl-project/sglang'
strategy:
fail-fast: false
matrix:
include:
- suite: stage-c-2-gpu-mi350
runner: linux-mi35x-gpu-2
# Use 8-GPU MI35X pool for 4-GPU suite.
- suite: stage-c-4-gpu-mi350
runner: linux-mi35x-gpu-8
- suite: stage-c-8-gpu-mi350
runner: linux-mi35x-gpu-8
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Resolve newest miles ROCm 7.0 image
run: |
repo="rocm/sgl-dev"
base="miles-rocm700-mi35x"
tag="${{ inputs.image_tag }}"
if [ -z "${tag}" ]; then
# Walk back from today; the build job pushes a dated tag at 12:00 UTC.
# Probe with `manifest inspect` so we don't pull a multi-GB image just
# to test existence; the real pull happens in amd_ci_start_container.sh.
for i in $(seq 0 6); do
d=$(date -u -d "-${i} day" +%Y%m%d)
candidate="${base}-${d}"
if docker manifest inspect "${repo}:${candidate}" >/dev/null 2>&1; then
tag="${candidate}"
break
fi
done
fi
if [ -z "${tag}" ]; then
echo "::error::No ${repo}:${base}-<date> image found on Docker Hub for the last 7 days"
exit 1
fi
echo "MILES_IMAGE=${repo}:${tag}" >> "$GITHUB_ENV"
echo "Using miles image: ${repo}:${tag}"
- name: Ensure VRAM is clear
run: bash scripts/ci/amd/ensure_vram_clear.sh rocm
- name: Start container from the miles image
run: bash scripts/ci/amd/amd_ci_start_container.sh --custom-image "${MILES_IMAGE}"
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Run miles CI suite ${{ matrix.suite }}
timeout-minutes: 120
run: |
touch "${GITHUB_WORKSPACE}/github_summary.md"
TEST_EXIT_CODE=0
bash scripts/ci/amd/amd_ci_exec.sh -w /root/miles \
-e PYTHONPATH=/root/miles:/opt/tilelang \
-e GITHUB_STEP_SUMMARY=/sglang-checkout/github_summary.md \
python3 tests/ci/run_suite.py --hw rocm --suite ${{ matrix.suite }} --match-all-labels \
${{ (github.event_name != 'workflow_dispatch' || inputs.continue_on_error) && '--continue-on-error' || '' }} || TEST_EXIT_CODE=$?
cat "${GITHUB_WORKSPACE}/github_summary.md" >> "$GITHUB_STEP_SUMMARY" || true
exit ${TEST_EXIT_CODE}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+181
View File
@@ -0,0 +1,181 @@
name: Nightly Test (Intel)
on:
schedule:
# 22:15 IST (UTC+5:30) → 16:45 UTC daily.
- cron: '45 16 * * *'
push:
branches:
- main
paths:
- "python/sglang/version.py"
workflow_dispatch:
inputs:
continue_on_error:
description: 'Continue on error (do not fail the workflow on test failures)'
required: false
type: boolean
default: true
workflow_call:
inputs:
ref:
description: "Branch, tag or SHA to checkout"
required: false
type: string
default: ""
continue_on_error:
description: 'Continue on error (do not fail the workflow on test failures)'
required: false
type: boolean
default: true
concurrency:
group: nightly-test-intel-${{ github.event_name == 'workflow_dispatch' && format('manual-{0}', github.run_id) || inputs.ref && format('caller-{0}', github.run_id) || github.ref }}
cancel-in-progress: ${{ !inputs.ref && github.event_name != 'workflow_call' && github.event_name != 'workflow_dispatch' }}
jobs:
nightly-xpu-1-gpu:
# Placeholder: no models currently registered to nightly-xpu-1-gpu run on
# intel-bmg-nightly. Add a model test to this suite to re-enable.
if: false
runs-on: intel-bmg-nightly
steps:
- name: Placeholder
run: echo "nightly-xpu-1-gpu has no validated models; skipping."
nightly-xpu-2-gpu:
if: github.repository == 'sgl-project/sglang'
runs-on: intel-bmg-nightly
env:
DOCKERHUB_INTEL_USERNAME: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
DOCKERHUB_INTEL_TOKEN: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
steps:
- name: Reset workspace ownership
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.sha }}
- name: Start CI container (pull intel/sglang-dev:latest)
run: |
export HF_TOKEN="$(cat ~/huggingface_token.txt)"
bash scripts/ci/xpu/xpu_ci_start_container.sh
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: HF login + install run_suite extras
timeout-minutes: 10
run: |
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir tabulate
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir --no-deps xgrammar==0.1.33
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir "lm-eval==0.4.9"
docker exec ci_sglang_xpu /bin/bash -c '/opt/venv/bin/hf auth login --token ${HF_TOKEN}'
- name: Nightly Test (2-GPU XPU)
timeout-minutes: 60
run: |
touch github_summary.md
docker exec ci_sglang_xpu bash -c "
source /opt/venv/bin/activate &&
cd /sglang-checkout/test &&
GITHUB_STEP_SUMMARY=/sglang-checkout/github_summary.md \
python3 run_suite.py --hw xpu --suite nightly-xpu-2-gpu --nightly --timeout-per-file 7200 ${{ (github.event_name == 'schedule' || inputs.continue_on_error) && '--continue-on-error' || '' }}
" || TEST_EXIT_CODE=$?
echo "$(<github_summary.md)" >> $GITHUB_STEP_SUMMARY || true
exit ${TEST_EXIT_CODE:-0}
- name: Cleanup container
if: always()
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
rm -rf test/result.jsonl test/results test/.pytest_cache .pytest_cache || true
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
find . -type f -name "*.pyc" -delete || true
docker rm -f ci_sglang_xpu || true
if [[ -n "${CI_SGLANG_XPU_IMAGE:-}" ]]; then
docker rmi -f "${CI_SGLANG_XPU_IMAGE}" || true
fi
nightly-xpu-4-gpu:
if: github.repository == 'sgl-project/sglang'
runs-on: intel-bmg-nightly
env:
DOCKERHUB_INTEL_USERNAME: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
DOCKERHUB_INTEL_TOKEN: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
steps:
- name: Reset workspace ownership
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.sha }}
- name: Start CI container (pull intel/sglang-dev:latest)
run: |
export HF_TOKEN="$(cat ~/huggingface_token.txt)"
bash scripts/ci/xpu/xpu_ci_start_container.sh
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: HF login + install run_suite extras
timeout-minutes: 10
run: |
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir tabulate
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir --no-deps xgrammar==0.1.33
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir "lm-eval==0.4.9"
docker exec ci_sglang_xpu /bin/bash -c '/opt/venv/bin/hf auth login --token ${HF_TOKEN}'
- name: Nightly Test (4-GPU XPU)
timeout-minutes: 120
run: |
touch github_summary.md
docker exec ci_sglang_xpu bash -c "
source /opt/venv/bin/activate &&
cd /sglang-checkout/test &&
GITHUB_STEP_SUMMARY=/sglang-checkout/github_summary.md \
python3 run_suite.py --hw xpu --suite nightly-xpu-4-gpu --nightly --timeout-per-file 7200 ${{ (github.event_name == 'schedule' || inputs.continue_on_error) && '--continue-on-error' || '' }}
" || TEST_EXIT_CODE=$?
echo "$(<github_summary.md)" >> $GITHUB_STEP_SUMMARY || true
exit ${TEST_EXIT_CODE:-0}
- name: Cleanup container
if: always()
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
rm -rf test/result.jsonl test/results test/.pytest_cache .pytest_cache || true
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
find . -type f -name "*.pyc" -delete || true
docker rm -f ci_sglang_xpu || true
if [[ -n "${CI_SGLANG_XPU_IMAGE:-}" ]]; then
docker rmi -f "${CI_SGLANG_XPU_IMAGE}" || true
fi
check-all-jobs:
if: always() && (github.repository == 'sgl-project/sglang' || github.event_name == 'workflow_dispatch')
needs:
- nightly-xpu-2-gpu
- nightly-xpu-4-gpu
runs-on: ubuntu-latest
steps:
- name: Check if any job failed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more nightly test jobs failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more nightly test jobs were cancelled"
exit 1
fi
echo "All nightly test jobs passed"
+301
View File
@@ -0,0 +1,301 @@
name: Nightly Test (MUSA)
on:
schedule:
- cron: '0 16 * * *'
workflow_dispatch:
inputs:
job_filter:
description: "Select which job to run (empty/all to run all jobs)"
required: false
type: choice
default: 'all'
options:
- 'all'
- 'nightly-test-musa-general-kernel'
- 'nightly-test-musa-general-multimodal-layer'
- 'nightly-test-llm-server-1-gpu-musa'
- 'nightly-test-multimodal-server-1-gpu-musa'
- 'nightly-test-multimodal-server-2-gpu-musa'
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
concurrency:
group: nightly-test-musa-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
env:
SGLANG_IS_IN_CI: true
jobs:
# ==================== General: kernel ====================
nightly-test-musa-general-kernel:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
TORCHADA_ENABLE_CPP_OPS: 1
HF_HUB_CACHE: /hf-cache/hub
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-musa-general-kernel" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
timeout-minutes: 10
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run sgl-kernel unit tests (MUSA)
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 30
run: |
pytest sgl-kernel/tests/test_dsv3_router_gemm.py
pytest sgl-kernel/tests/test_per_token_quant_fp8.py
pytest sgl-kernel/tests/speculative/test_eagle_utils.py
pytest sgl-kernel/tests/speculative/test_ngram_utils.py
pytest sgl-kernel/tests/speculative/test_speculative_sampling.py
pytest sgl-kernel/tests/test_torch_defaults_reset.py
# ==================== General: multimodal layer ====================
nightly-test-musa-general-multimodal-layer:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-musa-general-multimodal-layer" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
timeout-minutes: 10
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
if: steps.gate.outputs.run_job == 'true'
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run multimodal MUSA layer unit tests
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 30
run: |
pytest python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_rmsnorm.py
pytest python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_silu_and_mul.py
# ==================== LLM server: 1-GPU ====================
nightly-test-llm-server-1-gpu-musa:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-llm-server-1-gpu-musa" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
timeout-minutes: 10
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
if: steps.gate.outputs.run_job == 'true'
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run DeepSeek-V2-Lite-Chat LLM server smoke test
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 120
run: |
cd test
python3 run_suite.py \
--hw musa \
--suite nightly-musa-1-gpu \
--nightly \
--timeout-per-file 7200
# ==================== Multimodal: 1-GPU (split) ====================
nightly-test-multimodal-server-1-gpu-musa:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-1-gpu-runner
strategy:
fail-fast: false
max-parallel: 2
matrix:
part: [0, 1]
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-multimodal-server-1-gpu-musa" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
timeout-minutes: 10
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
if: steps.gate.outputs.run_job == 'true'
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run diffusion server tests (1-GPU)
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 60
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd python
python3 sglang/multimodal_gen/test/server/musa/run_suite.py \
--suite 1-gpu-musa-nightly \
--partition-id ${{ matrix.part }} \
--total-partitions 2 \
--continue-on-error
# ==================== Multimodal: 2-GPU ====================
nightly-test-multimodal-server-2-gpu-musa:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
runs-on: s5000-2-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Gate by job_filter
id: gate
run: |
filter="${{ inputs.job_filter || 'all' }}"
if [[ -z "$filter" || "$filter" == "all" || "$filter" == "nightly-test-multimodal-server-2-gpu-musa" ]]; then
echo "run_job=true" >> "$GITHUB_OUTPUT"
else
echo "run_job=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout code
timeout-minutes: 10
if: steps.gate.outputs.run_job == 'true'
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
if: steps.gate.outputs.run_job == 'true'
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run diffusion server tests (2-GPU)
if: steps.gate.outputs.run_job == 'true'
timeout-minutes: 60
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd python
python3 sglang/multimodal_gen/test/server/musa/run_suite.py \
--suite 2-gpu-musa \
--continue-on-error
# ==================== finish ====================
nightly-test-musa-finish:
needs:
[
nightly-test-musa-general-kernel,
nightly-test-musa-general-multimodal-layer,
nightly-test-llm-server-1-gpu-musa,
nightly-test-multimodal-server-1-gpu-musa,
nightly-test-multimodal-server-2-gpu-musa,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
json_needs='${{ toJson(needs) }}'
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
echo "$job: $result"
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "Nightly failed."
exit 1
fi
done
echo "All jobs completed successfully."
@@ -0,0 +1,232 @@
name: 'Nightly Test (NPU) Multi Node Template'
on:
workflow_call:
inputs:
runner:
required: false
type: string
default: linux-amd64-cpu-8
test_type:
required: false
type: string
default: perf
description: perf or accuracy
test_config_name:
required: true
type: string
description: test config name
prefill_size:
required: false
type: number
default: 0
description: number of prefill nodes for pd-separation
decode_size:
required: false
type: number
default: 0
description: number of decode nodes for pd-separation
router_size:
required: false
type: number
default: 0
description: number of router nodes for pd-separation
node_size:
required: false
type: number
default: 0
description: number of nodes for pd-mix
test_case:
required: true
type: string
description: path of test case file
image:
required: false
type: string
description: image for pods
default: "swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann8.5.0-a3"
install_sglang_from_source:
required: false
type: boolean
default: true
description: use sglang from source code or from docker image
prefill_decode_deployment:
required: true
type: string
description: "The deployment of prefill and decode nodes. ['separation', 'mix']"
transformers_version:
required: false
type: string
default: ""
description: "The transformers version number for running sglang. Use default version in image if keep empty."
concurrency:
group: ascend-nightly-multi-node-${{ github.workflow_ref }}-${{ github.ref }}-${{ inputs.test_config_name }}
cancel-in-progress: true
jobs:
e2e:
name: ${{ inputs.test_config_name }}
runs-on: ${{ inputs.runner }}
container:
image: swr.ap-southeast-1.myhuaweicloud.com/base_image/ascend-ci/sglang:main-x86
env:
KUBECONFIG: /root/.cache/.cache/kb.yaml
KUBECTL: /root/.cache/.cache/kubectl
NAMESPACE: sgl-project
KUBE_JOB_NAME: ascend-sglang-${{ inputs.test_type }}-test
SGLANG_IS_IN_CI: true
ASCEND_E2E_TEST_CONFIG_PATH: python/sglang/test/ascend/e2e
SGLANG_USE_MODELSCOPE: true
HF_ENDPOINT: https://hf-mirror.com
TRANSFORMERS_VERBOSITY: "error"
GDN_ATTN_BACKEND_TRITON: 1
steps:
- name: Install dependencies
run: |
pip3 install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple jinja2
pip3 install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple psutil
# Install kubernetes
pip3 install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple kubernetes
cp $KUBECTL /usr/local/sbin/
- name: Checkout code
uses: actions/checkout@v4
- name: Prepare code for testing
run: |
# copy source code to shared-disk
current_path=$(pwd)
target_path=/root/.cache/tests/sglang
rm -rf ${target_path}
mkdir -p ${target_path}
cp -r ${current_path}/* ${target_path}/
- name: Clear resources
run: |
cd ${ASCEND_E2E_TEST_CONFIG_PATH}
kubectl delete -f ./k8s_multi_pd_*.yaml --ignore-not-found=true || true
pod_name_prefix="${KUBE_JOB_NAME}-"
echo "kube name space: $NAMESPACE, pod name prefix: ${pod_name_prefix}"
while true; do
if kubectl get po -A -n $NAMESPACE | grep -q "${pod_name_prefix}"; then
echo "Found exist sglang job, sleeping for 30 seconds..."
sleep 30
kubectl get pods | grep "${pod_name_prefix}" | awk '{print $1}' | xargs kubectl delete pod -n $NAMESPACE || true
else
echo "No sglang job exist, start test case..."
break
fi
done
- name: Run test
timeout-minutes: 300
run: |
# sglang_source_relative_path is shared-disk path
sglang_source_relative_path=tests/sglang
sglang_source_path=/root/.cache/${sglang_source_relative_path}
echo "Source code path: ${sglang_source_path}"
test_case="${{ inputs.test_case }}"
if [ ! -f "${sglang_source_path}/${test_case}" ]; then
echo "The testcase does not exit: ${sglang_source_path}/${test_case}"
exit 1
fi
# prepare for output data
current_date=$(date +%Y%m%d)
test_data_output_path=/root/.cache/tests/output/${{ inputs.test_type }}/${current_date}
mkdir -p ${test_data_output_path}
tc_name=${test_case##*/}
tc_name=${tc_name%.*}
metrics_data_path=${test_data_output_path}/${tc_name}
mkdir -p ${metrics_data_path}
echo "Metrics file path: ${metrics_data_path}"
prefill_decode_deployment="${{ inputs.prefill_decode_deployment }}"
kube_job_type=""
if [ "$prefill_decode_deployment" = "separation" ];then
kube_job_type=multi-pd-separation
elif [ "$prefill_decode_deployment" = "mix" ];then
kube_job_type=multi-pd-mix
else
echo "Unsupported deployment type: ${prefill_decode_deployment}"
exit 1
fi
transformers_version="${{ inputs.transformers_version }}"
cd ${ASCEND_E2E_TEST_CONFIG_PATH}
CMD="python3 -u run_npu_e2e_test.py \
--env ci \
--image ${{ inputs.image }} \
--sglang-source-relative-path ${sglang_source_relative_path} \
--metrics-data-file ${metrics_data_path} \
--test-case ${test_case} \
--kube-name-space ${NAMESPACE} \
--kube-job-type ${kube_job_type} \
--kube-job-name-prefix ${KUBE_JOB_NAME}"
if [ "$prefill_decode_deployment" = "separation" ];then
CMD="${CMD} \
--prefill-size ${{ inputs.prefill_size }} \
--decode-size ${{ inputs.decode_size }} \
--router-size ${{ inputs.router_size }}"
elif [ "$prefill_decode_deployment" = "mix" ];then
CMD="${CMD} --node-size ${{ inputs.node_size }}"
else
echo "Unsupported deployment type: ${prefill_decode_deployment}"
exit 1
fi
if [ "$SGLANG_IS_IN_CI" = "true" ] || [ "$SGLANG_IS_IN_CI" = "True" ];then
echo "Run test in ci."
CMD="${CMD} --sglang-is-in-ci"
fi
install_sglang_from_source="${{ inputs.install_sglang_from_source }}"
if [ "$install_sglang_from_source" = "true" ] || [ "$install_sglang_from_source" = "True" ];then
echo "Install sglang from source."
CMD="${CMD} --install-sglang-from-source"
commit_id=${{ github.sha }}
echo "commit id: ${commit_id}" > ${test_data_output_path}/commit_id
else
echo "Use sglang from image: ${{ inputs.image }}"
fi
if [ "$transformers_version" != "" ];then
CMD="${CMD} --transformers-version ${transformers_version}"
else
echo "Use default transformers version in image."
fi
echo "Run command: ${CMD}"
eval "${CMD}"
- name: Upload metrics
if: always()
uses: actions/upload-artifact@v4
with:
name: metrics-${{ inputs.test_config_name }}
path: /tmp/metrics.json
retention-days: 7
- name: Post process
if: always()
run: |
cd ${ASCEND_E2E_TEST_CONFIG_PATH}
kubectl get pods -n $NAMESPACE | grep $KUBE_JOB_NAME
kubectl delete -f ./k8s_multi_pd_*.yaml --ignore-not-found=true || true
pod_name_prefix="${KUBE_JOB_NAME}-"
echo "kube name space: $NAMESPACE, pod name prefix: ${pod_name_prefix}"
while true; do
if kubectl get po -A -n $NAMESPACE | grep -q "${pod_name_prefix}"; then
echo "Found exist sglang job, sleeping for 30 seconds..."
sleep 30
kubectl get pods | grep "${pod_name_prefix}" | awk '{print $1}' | xargs kubectl delete pod -n $NAMESPACE || true
else
echo "No sglang job exist, start test case..."
break
fi
done
@@ -0,0 +1,255 @@
name: 'Nightly Test (NPU) Single Node Template'
on:
workflow_call:
inputs:
runner:
required: true
type: string
default: linux-aarch64-a3-16
test_type:
required: false
type: string
default: perf
description: perf or accuracy
test_config_name:
required: true
type: string
description: test config name
test_case:
required: true
type: string
description: path of test case file
image:
required: false
type: string
description: image for pods
default: "swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-a3"
install_sglang_from_source:
required: false
type: boolean
default: false
description: use sglang from source code or from docker image
install_sglang_deps:
required: false
type: boolean
default: false
description: install sglang dependencies (e.g. PyTorch, CANN packages) when using source installation
device_type_for_deps:
required: false
type: string
default: 'a3'
description: device type for dependency installation (a3 or 910b)
transformers_version:
required: false
type: string
default: ""
description: "The transformers version number for running sglang. Use default version in image if keep empty."
concurrency:
group: ascend-nightly-e2e-singlenode-${{ github.workflow_ref }}-${{ github.ref }}-${{ inputs.test_config_name }}
cancel-in-progress: true
jobs:
e2e:
name: ${{ inputs.test_config_name }}
runs-on: ${{ inputs.runner }}
container:
image: ${{ inputs.image }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check npu info
run: |
npu-smi info
- name: Install sglang dependencies
if: ${{ inputs.install_sglang_deps == true }}
shell: bash
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh ${{ inputs.device_type_for_deps }}
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
- name: Run test
timeout-minutes: 300
env:
SGLANG_USE_MODELSCOPE: true
HF_ENDPOINT: https://hf-mirror.com
SGLANG_IS_IN_CI: true
TRANSFORMERS_VERBOSITY: "error"
GDN_ATTN_BACKEND_TRITON: 1
SGLANG_TEST_METRICS_OUTPUT: /root/.cache/tests/output/metrics/metrics
shell: bash
run: |
sglang_source_path=$(pwd)
echo "Source code path: ${sglang_source_path}"
ln -sf ${sglang_source_path} /root/sglang
test_case=${{ inputs.test_case }}
if [ ! -f "${sglang_source_path}/${test_case}" ]; then
echo "The testcase does not exit: ${sglang_source_path}/${test_case}"
exit 1
fi
# prepare for output data
current_date=$(date +%Y%m%d)
test_data_output_path=/root/.cache/tests/output/${{ inputs.test_type }}/${current_date}
mkdir -p ${test_data_output_path}
tc_name=${test_case##*/}
tc_name=${tc_name%.*}
export METRICS_DATA_FILE=${test_data_output_path}/${tc_name}
mkdir -p ${METRICS_DATA_FILE}
echo "Metrics file path: ${METRICS_DATA_FILE}"
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
curl -o /tmp/test.jsonl -L https://gh-proxy.test.osinfra.cn/https://raw.githubusercontent.com/openai/grade-school-math/master/grade_school_math/data/test.jsonl
export TRANSFORMERS_VERSION_FOR_SGLANG="${{ inputs.transformers_version }}"
PYTHON_FOR_SGLANG="python"
PIP_FOR_SGLANG="pip"
if [ -n "${TRANSFORMERS_VERSION_FOR_SGLANG}" ];then
echo "===== Install transformers for sglang - Begin ====="
TRANSFORMERS_PKG_PATH_SOURCE=/root/.cache/.cache/transformers/${TRANSFORMERS_VERSION_FOR_SGLANG}
if [ ! -d "${TRANSFORMERS_PKG_PATH_SOURCE}" ]; then
echo "The dependent transformers package does not exist: ${TRANSFORMERS_PKG_PATH_SOURCE}."
echo "Install transformers ${TRANSFORMERS_VERSION_FOR_SGLANG} online."
pip install transformers=="${TRANSFORMERS_VERSION_FOR_SGLANG}" -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
else
echo "Install transformers ${TRANSFORMERS_VERSION_FOR_SGLANG} locally."
TRANSFORMERS_PKG_PATH_TARGET=/tmp/transformers/${TRANSFORMERS_VERSION_FOR_SGLANG}
mkdir -p "${TRANSFORMERS_PKG_PATH_TARGET}"
cp "${TRANSFORMERS_PKG_PATH_SOURCE}/*" "${TRANSFORMERS_PKG_PATH_TARGET}/"
pip install --no-index --find-links="${TRANSFORMERS_PKG_PATH_TARGET}" transformers=="${TRANSFORMERS_VERSION_FOR_SGLANG}"
fi
echo "===== Install transformers for sglang in virtual env - End ====="
fi
echo "Transformers version for sglang: $(${PIP_FOR_SGLANG} show transformers | grep Version | cut -d: -f2)"
echo "scaling_governor performance num: \
$(cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | grep performance | wc -l)"
echo "swappiness: $(cat /proc/sys/vm/swappiness)"
echo "numa_balancing: $(cat /proc/sys/kernel/numa_balancing)"
echo "sched_migration_cost_ns: $(cat /proc/sys/kernel/sched_migration_cost_ns)"
export SGLANG_TEST_MAX_RETRY=0
export SGLANG_SET_CPU_AFFINITY=1
echo "SGLANG_SET_CPU_AFFINITY: $SGLANG_SET_CPU_AFFINITY"
install_sglang_from_source=${{ inputs.install_sglang_from_source }}
if [ "$install_sglang_from_source" = "true" ] || [ "$install_sglang_from_source" = "True" ];then
echo "Install sglang from source"
commit_id=${{ github.sha }}
echo "commit id: ${commit_id}" > ${test_data_output_path}/commit_id
export PYTHONPATH=${sglang_source_path}/python:$PYTHONPATH
else
echo "Use sglang from image: ${{ inputs.image }}"
sglang_pkg_path=/sgl-workspace/sglang/python
ascend_test_util_path=${sglang_pkg_path}/sglang/test/ascend
mkdir -p ${ascend_test_util_path}
mv ${ascend_test_util_path} ${ascend_test_util_path}_bak
cp -r ${sglang_source_path}/python/sglang/test/ascend ${ascend_test_util_path}
fi
source /usr/local/Ascend/cann/set_env.sh || true
source /usr/local/Ascend/nnal/atb/set_env.sh || true
# Set environment of cann
log_path="/root/.cache/tests/logs/log/${current_date}/${tc_name}/${HOSTNAME}"
rm -rf ${log_path}
mkdir -p ${log_path}
echo "Log path: ${log_path}"
echo "Running test case ${test_case}"
test_exit_code=0
${PYTHON_FOR_SGLANG} -u ${test_case} 2>&1 | tee /tmp/test_output.log || test_exit_code=$?
echo "Finished test case ${test_case}"
if [ "${test_exit_code}" = "0" ]; then
test_status="pass"
status_icon="✅"
else
test_status="fail"
status_icon="❌"
fi
echo "test_status=${test_status}" >> $GITHUB_ENV
echo "tc_name=${tc_name}" >> $GITHUB_ENV
export test_status tc_name
echo "## ${tc_name} ${status_icon} ${test_status}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
metric_count=$(grep -c '\[METRIC\]' /tmp/test_output.log 2>/dev/null || echo 0)
if [ "${metric_count}" -gt 0 ]; then
echo "| Metric | Value | Pass |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|------|" >> $GITHUB_STEP_SUMMARY
grep '\[METRIC\]' /tmp/test_output.log | while IFS= read -r line; do
metric_name=$(echo "$line" | sed -E 's/.*\[METRIC\] ([^=]+)=.*/\1/')
metric_value=$(echo "$line" | sed -E 's/.*\[METRIC\] [^=]+=([^ ]+).*/\1/')
echo "| ${metric_name} | ${metric_value} | ${status_icon} |" >> $GITHUB_STEP_SUMMARY
done
else
echo "No metrics collected (test may have failed before producing results)." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "import json, re, os" > /tmp/dump_metrics.py
echo "tc = os.environ.get('tc_name', 'unknown')" >> /tmp/dump_metrics.py
echo "st = os.environ.get('test_status', 'unknown')" >> /tmp/dump_metrics.py
echo "tp = '${{ inputs.test_type }}'" >> /tmp/dump_metrics.py
echo "metrics = {}" >> /tmp/dump_metrics.py
echo "with open('/tmp/test_output.log') as f:" >> /tmp/dump_metrics.py
echo " for line in f:" >> /tmp/dump_metrics.py
echo " m = re.match(r'\[METRIC\] (\S+)=(\S+)', line)" >> /tmp/dump_metrics.py
echo " if m:" >> /tmp/dump_metrics.py
echo " v = m.group(2)" >> /tmp/dump_metrics.py
echo " try:" >> /tmp/dump_metrics.py
echo " v = float(v)" >> /tmp/dump_metrics.py
echo " except ValueError:" >> /tmp/dump_metrics.py
echo " pass" >> /tmp/dump_metrics.py
echo " metrics[m.group(1)] = v" >> /tmp/dump_metrics.py
echo "baselines = {}" >> /tmp/dump_metrics.py
echo "for k, v in list(metrics.items()):" >> /tmp/dump_metrics.py
echo " if k.endswith('_baseline'):" >> /tmp/dump_metrics.py
echo " baselines[k[:-9]] = v" >> /tmp/dump_metrics.py
echo " del metrics[k]" >> /tmp/dump_metrics.py
echo "with open('/tmp/metrics.json', 'w') as f:" >> /tmp/dump_metrics.py
echo " json.dump({'test_case': tc, 'test_type': tp, 'status': st, 'metrics': metrics, 'baselines': baselines}, f)" >> /tmp/dump_metrics.py
python3 /tmp/dump_metrics.py
exit ${test_exit_code}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@v4
with:
name: metrics-${{ inputs.test_config_name }}
path: /tmp/metrics.json
retention-days: 7
- name: Backup plog
if: always()
shell: bash
run: |
plog_path="/root/ascend/log/debug/plog"
if [ -d "$plog_path" ];then
echo "Plog files found. Begin to backup them."
tc_name=${{ inputs.test_case }}
tc_name=${tc_name##*/}
tc_name=${tc_name%.*}
target_plog_path="/root/.cache/tests/logs/plog/${tc_name}/${HOSTNAME}"
echo "Save path: ${target_plog_path}"
rm -rf ${target_plog_path}
mkdir -p ${target_plog_path}
cp ${plog_path}/* ${target_plog_path}
fi
+406
View File
@@ -0,0 +1,406 @@
name: Nightly Test (NPU)
on:
schedule:
- cron: '0 18 * * *' # Execute at 2:00 a.m. Beijing Time every day
pull_request:
branches:
- main
paths:
- ".github/workflows/nightly-test-npu.yml"
workflow_dispatch:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
image_a2:
description: 'The a2 running docker image of the test task.'
required: false
type: string
default: 'swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-910b'
image_a3:
description: 'The a3 running docker image of the test task.'
required: false
type: string
default: 'swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-a3'
skip_install_flag:
description: 'Indicates whether to skip the installation of sglang, defaulting to false.'
required: false
type: string
default: 'true'
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
image_a2:
description: 'The a2 running docker image of the test task.'
required: false
type: string
default: ''
image_a3:
description: 'The a3 running docker image of the test task.'
required: false
type: string
default: ''
skip_install_flag:
description: 'Indicates whether to skip the installation of sglang, defaulting to false.'
required: false
type: string
default: ''
concurrency:
group: nightly-test-npu-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
set-image-config:
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.set-vars.outputs.ref }}
job_filter: ${{ steps.set-vars.outputs.job_filter }}
image_a2: ${{ steps.set-vars.outputs.image_a2 }}
image_a3: ${{ steps.set-vars.outputs.image_a3 }}
skip_install_flag: ${{ steps.set-vars.outputs.skip_install_flag }}
steps:
# When triggered by PR, no inputs parameters are used. The latest community code is tested by default.
- name: Set image config
id: set-vars
run: |
if [ -z "${{ inputs.ref }}" ]; then
echo "ref=" >> $GITHUB_OUTPUT
else
echo "ref=${{ inputs.ref }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.job_filter }}" ]; then
echo "job_filter=all" >> $GITHUB_OUTPUT
else
echo "job_filter=${{ inputs.job_filter }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.image_a2 }}" ]; then
echo "image_a2=swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-910b" >> $GITHUB_OUTPUT
else
echo "image_a2=${{ inputs.image_a2 }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.image_a3 }}" ]; then
echo "image_a3=swr.cn-southwest-2.myhuaweicloud.com/base_image/dockerhub/lmsysorg/sglang:main-cann9.0.0-a3" >> $GITHUB_OUTPUT
else
echo "image_a3=${{ inputs.image_a3 }}" >> $GITHUB_OUTPUT
fi
if [ -z "${{ inputs.skip_install_flag }}" ]; then
echo "skip_install_flag=false" >> $GITHUB_OUTPUT
else
echo "skip_install_flag=${{ inputs.skip_install_flag }}" >> $GITHUB_OUTPUT
fi
nightly-poc-single-node-a2-tests:
name: single-node-poc-a2
if: ${{ !cancelled() }}
needs: [ set-image-config ]
strategy:
fail-fast: false
max-parallel: 6
matrix:
test_config:
- name: qwen3_32b_w8a8_2p_in3k5_out1k5_50ms_a2
runner: linux-aarch64-a2-4
test_case: test/registered/ascend/performance/qwen3_32b/test_npu_qwen3_32b_w8a8_2p_in3k5_out1k5_50ms_a2.py
test_type: 'perf'
uses: ./.github/workflows/nightly-test-npu-e2e-single-node.yml
with:
runner: ${{ matrix.test_config.runner }}
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.image_a2 }}
install_sglang_from_source: false
transformers_version: ''
nightly-poc-single-node-tests:
name: single-node-poc
if: ${{ !cancelled() }}
needs: [set-image-config]
strategy:
fail-fast: false
max-parallel: 6
matrix:
test_config:
# qwen3_6_35b_a3b performance tests
- name: qwen3_6_35b_a3b_1p_in3k5_out1k5_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in3k5_out1k5_50ms.py
test_type: 'perf'
- name: qwen3_6_35b_a3b_1p_aime26
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_aime26.py
- name: qwen3_6_35b_a3b_1p_in64k_out1k_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in64k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_35b_a3b_1p_in128k_out1k_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in128k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_35b_a3b_1p_in64k_out1k_prefix90_50ms_aime26
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in64k_out1k_prefix90_50ms_aime26.py
test_type: 'perf'
- name: qwen3_6_35b_a3b_1p_in128k_out1k_prefix90_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_35b_a3b/test_npu_qwen3_6_35b_a3b_1p_in128k_out1k_prefix90_50ms.py
test_type: 'perf'
# qwen3_6_27b performance tests
- name: qwen3_6_27b_w8a8_1p_in3k5_out1k5_50ms_gpqa
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in3k5_out1k5_50ms_gpqa.py
test_type: 'perf'
- name: qwen3_6_27b_w8a8_2p_in16k_out1k_50ms
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_2p_in16k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_w8a8_1p_in64k_out1k_50ms
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_w8a8_2p_in128k_out1k_50ms
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_2p_in128k_out1k_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_2p_in64k_out1k_prefix90_50ms
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_2p_in64k_out1k_prefix90_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_1p_in1024x1024_30_out1024_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_1p_in1024x1024_30_out1024_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_1p_in1080p_30_out256_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_1p_in1080p_30_out256_50ms.py
test_type: 'perf'
- name: qwen3_6_27b_1p_gpqa
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/qwen3_6_27b/test_npu_qwen3_6_27b_1p_gpqa.py
# qwen3_32b performance tests
- name: qwen3_32b_w8a8_2p_in3k5_out1k5_50ms_gpqa
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_32b/test_npu_qwen3_32b_w8a8_2p_in3k5_out1k5_50ms_gpqa.py
test_type: 'perf'
- name: qwen3_32b_bf16_8p_in18k_out4k_6ms
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/performance/qwen3_32b/test_npu_qwen3_32b_bf16_8p_in18k_out4k_6ms.py
test_type: 'perf'
- name: qwen3_32b_bf16_8p_gpqa
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/accuracy/qwen3_32b/test_npu_qwen3_32b_bf16_8p_gpqa.py
# qwen3_30b_a3b performance tests
- name: qwen3_30b_w8a8_1p_in3k5_out1k5_50ms_aime25
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_30b_a3b/test_npu_qwen3_30b_w8a8_1p_in3k5_out1k5_50ms_aime25.py
test_type: 'perf'
# qwen3-8b performance tests
- name: qwen3_8b_w8a8_1p_in3k5_out1k5_50ms_gpqa
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3-8b/test_npu_qwen3_8b_w8a8_1p_in3k5_out1k5_50ms_gpqa.py
test_type: 'perf'
- name: qwen3_8b_w8a8_1p_in6k_out1k5_bs16_gpqa
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3-8b/test_npu_qwen3_8b_w8a8_1p_in6k_out1k5_bs16_gpqa.py
test_type: 'perf'
# qwen3_next_80b_a3b_instruct performance tests
- name: qwen3_next_80b_w8a8_2p_in6k_out1k5_bs16_aime25
runner: linux-aarch64-a3-4
test_case: test/registered/ascend/performance/qwen3_next_80b_a3b_instruct/test_npu_qwen3_next_80b_w8a8_2p_in6k_out1k5_bs16_aime25.py
test_type: 'perf'
# minimax_m2_5 performance tests
- name: minimax_m2_5_w8a8_8p_in3k5_out1k5_50ms_gpqa
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/performance/minimax_m2_5/test_npu_minimax_m2_5_w8a8_8p_in3k5_out1k5_50ms_gpqa.py
test_type: 'perf'
- name: minimax_m2_5_w8a8_4p_in64k_out1k_prefix90_50ms_gpqa
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/performance/minimax_m2_5/test_npu_minimax_m2_5_w8a8_4p_in64k_out1k_prefix90_50ms_gpqa.py
test_type: 'perf'
# deepseek_v3_2 accuracy tests
- name: deepseek_v3_2_8p_aime25
runner: linux-aarch64-a3-16
test_case: test/registered/ascend/accuracy/deepseek_v3_2/test_npu_deepseek_v3_2_8p_aime25.py
# glm4_7_flash accuracy tests
- name: glm4_7_flash_1p_aime25
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/glm4_7_flash/test_npu_glm4_7_flash_1p_aime25.py
# glm4_6v_flash accuracy tests
- name: glm4_6v_flash_1p_mmmu
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/glm4_6v_flash/test_npu_glm4_6v_flash_1p_mmmu.py
# qwen3_vl_8b_thinking accuracy tests
- name: qwen3_vl_8b_thinking_1p_mmmu
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/qwen3_vl_8b_thinking/test_npu_qwen3_vl_8b_thinking_1p_mmmu.py
# qwen3_vl_30b_a3b_thinking accuracy tests
- name: qwen3_vl_30b_a3b_thinking_1p_mmmu
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/accuracy/qwen3_vl_30b_a3b_thinking/test_npu_qwen3_vl_30b_a3b_thinking_1p_mmmu.py
uses: ./.github/workflows/nightly-test-npu-e2e-single-node.yml
with:
runner: ${{ matrix.test_config.runner }}
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.image_a3 }}
install_sglang_from_source: false
transformers_version: ''
nightly-poc-multi-node-tests:
name: multi-node-poc
if: ${{ !cancelled() }}
needs: [set-image-config, nightly-poc-single-node-tests]
strategy:
fail-fast: false
max-parallel: 1
matrix:
test_config:
# glm5_1 performance tests
- name: glm5_1_w4a8_1p1d_32p_in64k_out1k_50ms_aime26
prefill_size: 2
decode_size: 2
router_size: 1
test_case: test/registered/ascend/performance/glm5_1/test_npu_glm5_1_w4a8_1p1d_32p_in64k_out1k_50ms_aime26.py
test_type: 'perf'
prefill_decode_deployment: 'separation'
uses: ./.github/workflows/nightly-test-npu-e2e-multi-node.yml
with:
runner: linux-amd64-cpu-8
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
prefill_size: ${{ matrix.test_config.prefill_size }}
decode_size: ${{ matrix.test_config.decode_size }}
router_size: ${{ matrix.test_config.router_size }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.image_a3 }}
install_sglang_from_source: false
prefill_decode_deployment: ${{ matrix.test_config.prefill_decode_deployment }}
transformers_version: ''
nightly-poc-multi-node-mix-tests:
name: multi-node-mix-poc
if: ${{ !cancelled() }}
needs: [set-image-config, nightly-poc-single-node-tests, nightly-poc-multi-node-tests]
strategy:
fail-fast: false
max-parallel: 1
matrix:
test_config:
# kimi_k2_6 performance tests
- name: kimi_k2_6_w4a8_16p_in64k_out1k_100ms_aime25
node_size: 2
test_case: test/registered/ascend/performance/kimi_k2_6/test_npu_kimi_k2_6_w4a8_16p_in64k_out1k_100ms_aime25.py
test_type: 'perf'
uses: ./.github/workflows/nightly-test-npu-e2e-multi-node.yml
with:
runner: linux-amd64-cpu-8
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
node_size: ${{ matrix.test_config.node_size }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.image_a3 }}
install_sglang_from_source: false
prefill_decode_deployment: 'mix'
transformers_version: ''
check-all-jobs:
if: ${{ !cancelled() }}
needs:
- nightly-poc-single-node-a2-tests
- nightly-poc-single-node-tests
- nightly-poc-multi-node-tests
- nightly-poc-multi-node-mix-tests
runs-on: ubuntu-latest
steps:
- name: Download all metrics
uses: actions/download-artifact@v4
with:
pattern: metrics-*
path: /tmp/metrics
merge-multiple: false
- name: Generate results table
run: |
status_emoji() {
case "$1" in
pass) echo "✅" ;;
fail) echo "❌" ;;
*) echo "❓" ;;
esac
}
single_result_a2="${{ needs.nightly-poc-single-node-a2-tests.result }}"
single_result="${{ needs.nightly-poc-single-node-tests.result }}"
multi_result="${{ needs.nightly-poc-multi-node-tests.result }}"
mix_result="${{ needs.nightly-poc-multi-node-mix-tests.result }}"
group_icon() {
case "$1" in
success) echo "✅" ;;
failure) echo "❌" ;;
cancelled) echo "⏭️" ;;
skipped) echo "⏭️" ;;
*) echo "❓" ;;
esac
}
echo "## Nightly Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Group | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| single-node-poc-a2 | $(group_icon ${single_result_a2}) ${single_result_a2} |" >> $GITHUB_STEP_SUMMARY
echo "| single-node-poc | $(group_icon ${single_result}) ${single_result} |" >> $GITHUB_STEP_SUMMARY
echo "| multi-node-poc | $(group_icon ${multi_result}) ${multi_result} |" >> $GITHUB_STEP_SUMMARY
echo "| multi-node-mix-poc | $(group_icon ${mix_result}) ${mix_result} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Per-Test Metrics" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
has_metrics=false
for dir in /tmp/metrics/metrics-*/; do
file="${dir}metrics.json"
if [ -f "$file" ]; then
has_metrics=true
echo "import json" > /tmp/parse_metrics.py
echo "with open('$file') as f:" >> /tmp/parse_metrics.py
echo " d = json.load(f)" >> /tmp/parse_metrics.py
echo "tc = d.get('test_case', '-')" >> /tmp/parse_metrics.py
echo "tp = d.get('test_type', '-')" >> /tmp/parse_metrics.py
echo "st = d.get('status', 'unknown')" >> /tmp/parse_metrics.py
echo "icon = '✅' if st == 'pass' else '❌'" >> /tmp/parse_metrics.py
echo "metrics = d.get('metrics', {})" >> /tmp/parse_metrics.py
echo "baselines = d.get('baselines', {})" >> /tmp/parse_metrics.py
echo "mstr = ', '.join(f'{k}={v}' for k,v in metrics.items()) if metrics else '-'" >> /tmp/parse_metrics.py
echo "bstr = ', '.join(f'{k}={v}' for k,v in baselines.items()) if baselines else '-'" >> /tmp/parse_metrics.py
echo "print(f'| {tc} | {tp} | {icon} {st} | {mstr} | {bstr} |')" >> /tmp/parse_metrics.py
python3 /tmp/parse_metrics.py
fi
done > /tmp/metrics_table.txt
if [ "${has_metrics}" = "true" ]; then
echo "| Test Case | Type | Status | Metrics | Baseline |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|------|--------|---------|----------|" >> $GITHUB_STEP_SUMMARY
cat /tmp/metrics_table.txt >> $GITHUB_STEP_SUMMARY
else
echo "No per-test metrics available (artifacts not found)." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
FAIL=0
if [ "${single_result}" != "success" ] && [ "${single_result}" != "skipped" ]; then FAIL=1; fi
exit $FAIL
+808
View File
@@ -0,0 +1,808 @@
name: Nightly Test (Nvidia)
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: choice
default: 'all'
options:
- 'all'
- 'nightly-test-general-1-gpu-h100'
- 'nightly-test-general-4-gpu-h100'
- 'nightly-test-general-8-gpu-h200'
- 'nightly-test-general-8-gpu-h20'
- 'nightly-test-general-8-gpu-b200'
- 'nightly-test-text-accuracy-2-gpu-h100'
- 'nightly-test-text-perf-2-gpu-h100'
- 'nightly-test-vlm-accuracy-2-gpu-h100'
- 'nightly-test-vlm-perf-2-gpu-h100'
- 'nightly-test-perf-4-gpu-b200'
- 'nightly-test-perf-8-gpu-b200'
- 'nightly-test-specialized-8-gpu-b200'
- 'nightly-test-perf-4-gpu-gb300'
- 'nightly-test-kernel-1-gpu-h100'
- 'nightly-test-diffusion'
- 'nightly-test-kernel-8-gpu-h200'
- 'nightly-test-precision-8-gpu-h200'
force_baseline_update:
description: 'precision job only: refresh the rolling baseline instead of comparing (sets SGLANG_PRECISION_FORCE_UPDATE=1). Dispatch once after an intentional forward-path dtype/precision change stales the baseline; later scheduled runs compare against it and go green.'
required: false
type: boolean
default: false
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
job_filter:
description: 'Select which job to run (leave empty or "all" to run all jobs)'
required: false
type: string
default: 'all'
concurrency:
group: nightly-test-nvidia-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
env:
SGLANG_IS_IN_CI: true
SGLANG_ENABLE_ASYNC_ASSERT: true
SGLANG_CUDA_COREDUMP: "1"
HF_HUB_DOWNLOAD_TIMEOUT: 300
HF_HUB_ETAG_TIMEOUT: 300
jobs:
# General tests - 1 GPU
nightly-test-general-1-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-1-gpu-h100')
runs-on: 1-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-1-gpu --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# JIT kernel full unit tests (expanded parameter ranges via SGLANG_JIT_KERNEL_RUN_FULL_TESTS)
nightly-test-kernel-1-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-kernel-1-gpu-h100')
runs-on: 1-gpu-h100
timeout-minutes: 60
env:
# Full jit_kernel test grids (see sglang.jit_kernel.utils.should_run_full_tests)
SGLANG_JIT_KERNEL_RUN_FULL_TESTS: "1"
# Match pr-test-jit-kernel workflow for consistent JIT warmup behavior
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
# Allow maintenance bypass on default branch (same semantics as PR JIT workflow)
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
timeout-minutes: 20
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run jit kernel nightly suite
timeout-minutes: 60
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-kernel-1-gpu --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
nightly-test-kernel-8-gpu-h200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-kernel-8-gpu-h200')
runs-on: 8-gpu-h200
timeout-minutes: 240
env:
SGLANG_JIT_KERNEL_RUN_FULL_TESTS: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
timeout-minutes: 20
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run multi-GPU jit kernel nightly suite
timeout-minutes: 90
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-kernel-8-gpu-h200 --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# General tests - 4 GPU H100
nightly-test-general-4-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-4-gpu-h100')
runs-on: 4-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-4-gpu --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# General tests - 8 GPU H200
nightly-test-general-8-gpu-h200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-8-gpu-h200')
runs-on: 8-gpu-h200
strategy:
fail-fast: false
max-parallel: 2
matrix:
partition: [0, 1, 2, 3]
env:
RUNNER_LABELS: 8-gpu-h200
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run common 8-GPU model tests
if: always()
timeout-minutes: 300
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "8-gpu-h200"
IS_H200: "1"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-common --nightly --timeout-per-file=18000 --continue-on-error --auto-partition-id=${{ matrix.partition }} --auto-partition-size=4
- name: Publish traces to storage repo
if: always()
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
TRACE_ARGS=""
for dir in test/performance_profiles_*/; do
[ -d "$dir" ] && TRACE_ARGS="$TRACE_ARGS --traces-dir $dir"
done
if [ -n "$TRACE_ARGS" ]; then
python3 scripts/ci/utils/publish_traces.py $TRACE_ARGS
find test/performance_profiles_*/ -name '*.json.gz' -delete
else
echo "No trace directories found, skipping publish"
fi
- name: Run test
timeout-minutes: 30
env:
GPU_CONFIG: "8-gpu-h200"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-h200 --nightly --continue-on-error --auto-partition-id=${{ matrix.partition }} --auto-partition-size=4
- name: Collect performance metrics
if: always()
run: |
python3 scripts/ci/utils/save_metrics.py \
--gpu-config 8-gpu-h200 \
--partition ${{ matrix.partition }} \
--run-id ${{ github.run_id }} \
--output test/metrics-8gpu-h200-partition-${{ matrix.partition }}.json \
--search-dir test/performance_profiles_8_gpu \
--search-dir test
- name: Upload partition metrics
if: always()
uses: actions/upload-artifact@v4
with:
name: metrics-8gpu-h200-partition-${{ matrix.partition }}
path: test/metrics-8gpu-h200-partition-${{ matrix.partition }}.json
retention-days: 5
if-no-files-found: ignore
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.partition }}
# General tests - 8 GPU H20
nightly-test-general-8-gpu-h20:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-8-gpu-h20')
runs-on: 8-gpu-h20
env:
SGLANG_CI_RDMA_ALL_DEVICES: "mlx5_1,mlx5_2,mlx5_3,mlx5_4"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 30
env:
GPU_CONFIG: "8-gpu-h20"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-h20 --nightly --continue-on-error
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# General tests - 8 GPU B200
nightly-test-general-8-gpu-b200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-general-8-gpu-b200')
runs-on: 8-gpu-b200
strategy:
fail-fast: false
max-parallel: 2
matrix:
partition: [0, 1, 2, 3]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run common 8-GPU model tests
if: always()
timeout-minutes: 200
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "8-gpu-b200"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-common --nightly --timeout-per-file=12000 --continue-on-error --auto-partition-id=${{ matrix.partition }} --auto-partition-size=4
- name: Publish traces to storage repo
if: always()
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
TRACE_ARGS=""
for dir in test/performance_profiles_*/; do
[ -d "$dir" ] && TRACE_ARGS="$TRACE_ARGS --traces-dir $dir"
done
if [ -n "$TRACE_ARGS" ]; then
python3 scripts/ci/utils/publish_traces.py $TRACE_ARGS
find test/performance_profiles_*/ -name '*.json.gz' -delete
else
echo "No trace directories found, skipping publish"
fi
- name: Collect performance metrics
if: always()
run: |
python3 scripts/ci/utils/save_metrics.py \
--gpu-config 8-gpu-b200 \
--partition ${{ matrix.partition }} \
--run-id ${{ github.run_id }} \
--output test/metrics-8gpu-b200-partition-${{ matrix.partition }}.json \
--search-dir test/performance_profiles_8_gpu \
--search-dir test
- name: Upload partition metrics
if: always()
uses: actions/upload-artifact@v4
with:
name: metrics-8gpu-b200-partition-${{ matrix.partition }}
path: test/metrics-8gpu-b200-partition-${{ matrix.partition }}.json
retention-days: 5
if-no-files-found: ignore
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.partition }}
# Text model accuracy tests
nightly-test-text-accuracy-2-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-text-accuracy-2-gpu-h100')
runs-on: 2-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run eval test for text models
timeout-minutes: 120
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-eval-text-2-gpu --nightly --continue-on-error --timeout-per-file 4500
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# Text model performance tests
nightly-test-text-perf-2-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-text-perf-2-gpu-h100')
runs-on: 2-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run performance test for text models
timeout-minutes: 30
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "2-gpu-h100"
run: |
cd test
rm -rf performance_profiles_text_models/
python3 run_suite.py --hw cuda --suite nightly-perf-text-2-gpu --nightly --continue-on-error --timeout-per-file 3600
- name: Publish traces to storage repo
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
python3 scripts/ci/utils/publish_traces.py --traces-dir test/performance_profiles_text_models
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# VLM accuracy tests
nightly-test-vlm-accuracy-2-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-vlm-accuracy-2-gpu-h100')
runs-on: 2-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run eval test for VLM models (fixed MMMU-100)
timeout-minutes: 120
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-eval-vlm-2-gpu --nightly --continue-on-error --timeout-per-file 9000
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# VLM performance tests
nightly-test-vlm-perf-2-gpu-h100:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-vlm-perf-2-gpu-h100')
runs-on: 2-gpu-h100
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run perf test for VLM models (MMMU)
timeout-minutes: 30
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "2-gpu-h100"
run: |
cd test
rm -rf performance_profiles_vlms/
python3 run_suite.py --hw cuda --suite nightly-perf-vlm-2-gpu --nightly --continue-on-error --timeout-per-file 3600
- name: Publish traces to storage repo
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
python3 scripts/ci/utils/publish_traces.py --traces-dir test/performance_profiles_vlms
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# B200 Performance tests - 4 GPU
nightly-test-perf-4-gpu-b200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-perf-4-gpu-b200')
runs-on: 4-gpu-b200
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 200
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "4-gpu-b200"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-4-gpu-b200 --nightly --continue-on-error --timeout-per-file 12000
- name: Publish traces to storage repo
if: always()
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
TRACE_ARGS=""
for dir in test/performance_profiles_*/; do
[ -d "$dir" ] && TRACE_ARGS="$TRACE_ARGS --traces-dir $dir"
done
if [ -n "$TRACE_ARGS" ]; then
python3 scripts/ci/utils/publish_traces.py $TRACE_ARGS
find test/performance_profiles_*/ -name '*.json.gz' -delete
else
echo "No trace directories found, skipping publish"
fi
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# GB300 (Grace-Blackwell NVL4) performance tests - 4 GPU (ARM64)
nightly-test-perf-4-gpu-gb300:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-perf-4-gpu-gb300')
name: nightly-test-perf-4-gpu-gb300 (${{ matrix.model }})
runs-on: 4-gpu-gb300-nightly
strategy:
fail-fast: false
matrix:
include:
- model: glm5-nvfp4
suite: nightly-4-gpu-gb300-glm5-nvfp4
- model: qwen35-fp8
suite: nightly-4-gpu-gb300-qwen35-fp8
- model: deepseek-v4-pro-fp4
suite: nightly-4-gpu-gb300-deepseek-v4-pro-fp4
- model: kimi-k25-nvfp4
suite: nightly-4-gpu-gb300-kimi-k25-nvfp4
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
env:
GRACE_BLACKWELL: "1"
run: |
bash scripts/ci/cuda/ci_install_deepep.sh
- name: Run test
timeout-minutes: 600
env:
TRACE_BASE_URL: https://raw.githubusercontent.com/sglang-bot/sglang-ci-data/main/traces/${{ github.run_id }}
PERFETTO_RELAY_URL: ${{ vars.PERFETTO_RELAY_URL }}
GPU_CONFIG: "4-gpu-gb300"
run: |
cd test
python3 run_suite.py --hw cuda --suite ${{ matrix.suite }} --nightly --continue-on-error --timeout-per-file 7200
- name: Publish traces to storage repo
if: always()
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
TRACE_ARGS=""
for dir in test/performance_profiles_*/; do
[ -d "$dir" ] && TRACE_ARGS="$TRACE_ARGS --traces-dir $dir"
done
if [ -n "$TRACE_ARGS" ]; then
python3 scripts/ci/utils/publish_traces.py $TRACE_ARGS
find test/performance_profiles_*/ -name '*.json.gz' -delete
else
echo "No trace directories found, skipping publish"
fi
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# Specialized B200 tests - 8 GPU, for specific backends and configs
nightly-test-specialized-8-gpu-b200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-perf-8-gpu-b200' || inputs.job_filter == 'nightly-test-specialized-8-gpu-b200')
runs-on: 8-gpu-b200
env:
RUNNER_LABELS: 8-gpu-b200
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
env:
GPU_CONFIG: "8-gpu-b200"
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-8-gpu-b200 --nightly --continue-on-error --timeout-per-file 2400
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# SGLang-Diffusion nightly benchmark
nightly-test-diffusion:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-diffusion')
runs-on: 4-gpu-h100
timeout-minutes: 300
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion benchmark
env:
GITHUB_SHA: ${{ github.sha }}
GITHUB_RUN_ID: ${{ github.run_id }}
PYTHONUNBUFFERED: "1"
timeout-minutes: 210
run: |
python3 -u scripts/ci/utils/diffusion/run_comparison.py \
--output comparison-results.json
- name: Generate dashboard
if: always()
env:
GH_PAT_FOR_NIGHTLY_CI_DATA: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
GH_TOKEN: ${{ github.token }}
run: |
python3 scripts/ci/utils/diffusion/generate_diffusion_dashboard.py \
--results comparison-results.json \
--output dashboard.md \
--charts-dir comparison-charts \
--fetch-history \
--step-summary
- name: Publish to sglang-ci-data
if: always()
env:
GH_PAT_FOR_NIGHTLY_CI_DATA: ${{ secrets.GH_PAT_FOR_NIGHTLY_CI_DATA }}
run: |
python3 scripts/ci/utils/diffusion/publish_comparison_results.py \
--results comparison-results.json \
--dashboard dashboard.md \
--charts-dir comparison-charts
- name: Upload benchmark artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-benchmark-${{ github.run_id }}
path: |
comparison-results.json
dashboard.md
comparison-charts/
comparison-logs/
retention-days: 90
if-no-files-found: ignore
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# Nightly precision regression - per-layer hidden state comparison
nightly-test-precision-8-gpu-h200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'nightly-test-precision-8-gpu-h200')
runs-on: 8-gpu-h200
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run precision regression test
timeout-minutes: 120
env:
SGLANG_PRECISION_BASELINE_DIR: /tmp/sglang_precision_baselines
# Required: the test errors if SGLANG_PRECISION_HF_REPO is unset (no
# local-only mode). Set the var + the HF_TOKEN_PRECISION_STORE secret.
SGLANG_PRECISION_HF_REPO: ${{ vars.SGLANG_PRECISION_HF_REPO }}
SGLANG_PRECISION_HF_REVISION: ${{ vars.SGLANG_PRECISION_HF_REVISION || 'main' }}
HF_TOKEN: ${{ secrets.HF_TOKEN_PRECISION_STORE }}
SGLANG_PRECISION_COMMIT: ${{ github.sha }}
# '0' on scheduled runs (input unset); '1' only on a manual baseline-refresh dispatch.
SGLANG_PRECISION_FORCE_UPDATE: ${{ inputs.force_baseline_update && '1' || '0' }}
run: |
cd test
python3 run_suite.py --hw cuda --suite nightly-precision-8-gpu-h200 --nightly --continue-on-error --timeout-per-file 3600
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
# Consolidate performance metrics from all jobs
consolidate-metrics:
if: github.repository == 'sgl-project/sglang' && always()
needs:
- nightly-test-general-8-gpu-h200
- nightly-test-general-8-gpu-b200
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Download all partition metrics
uses: actions/download-artifact@v4
with:
pattern: "*metrics-*"
path: metrics/
merge-multiple: true
- name: List downloaded metrics
run: |
echo "Downloaded metrics files:"
find metrics/ -name "*.json" -type f 2>/dev/null || echo "No metrics files found"
- name: Merge metrics
run: |
python3 scripts/ci/utils/merge_metrics.py \
--input-dir metrics/ \
--output consolidated-metrics-${{ github.run_id }}.json \
--run-id ${{ github.run_id }} \
--commit-sha ${{ github.sha }} \
--branch ${{ github.ref_name }}
- name: Upload consolidated metrics
uses: actions/upload-artifact@v4
with:
name: consolidated-metrics-${{ github.run_id }}
path: consolidated-metrics-${{ github.run_id }}.json
retention-days: 90
if-no-files-found: warn
# Final check job
check-all-jobs:
if: github.repository == 'sgl-project/sglang' && always()
needs:
- nightly-test-general-1-gpu-h100
- nightly-test-general-4-gpu-h100
- nightly-test-general-8-gpu-h200
- nightly-test-general-8-gpu-h20
- nightly-test-general-8-gpu-b200
- nightly-test-text-accuracy-2-gpu-h100
- nightly-test-text-perf-2-gpu-h100
- nightly-test-vlm-accuracy-2-gpu-h100
- nightly-test-vlm-perf-2-gpu-h100
- nightly-test-perf-4-gpu-b200
- nightly-test-specialized-8-gpu-b200
- nightly-test-perf-4-gpu-gb300
- nightly-test-diffusion
- nightly-test-precision-8-gpu-h200
- consolidate-metrics
runs-on: ubuntu-latest
steps:
- name: Check if any job failed
run: |
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
echo "One or more nightly test jobs failed"
exit 1
fi
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more nightly test jobs were cancelled"
exit 1
fi
echo "All nightly test jobs passed"
@@ -0,0 +1,28 @@
name: Open A PR to Copy Code From OSS
on:
workflow_dispatch:
# schedule:
# - cron: '0 10 * * *'
permissions:
contents: write
jobs:
copy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: 'main'
- name: Install GitHub CLI (if not present)
run: |
bash scripts/code_sync/install_github_cli.sh
- name: Copy from OSS code
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_OPEN_PR_TO_PRIVATE }}
run: |
python3 scripts/code_sync/copy_from_oss.py
+31
View File
@@ -0,0 +1,31 @@
name: Open A PR to Copy Diff To OSS
on:
workflow_dispatch:
inputs:
commit_sha:
description: 'The commit SHA to copy. Defaults to LAST to copy the latest commit.'
required: false
default: 'LAST'
permissions:
contents: write
jobs:
copy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install GitHub CLI (if not present)
run: |
bash scripts/code_sync/install_github_cli.sh
- name: Copy to OSS code
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_OPEN_PR_TO_OSS }}
run: |
python3 scripts/code_sync/copy_to_oss.py --commit ${{ github.event.inputs.commit_sha }}
+118
View File
@@ -0,0 +1,118 @@
name: Patch Docker Image
on:
workflow_dispatch:
inputs:
pr_numbers:
description: "Comma-separated PR numbers to apply (e.g. 18962,19010)"
required: false
default: ""
image_tag:
description: "Base image tag to patch (e.g. dev-x86, dev-x86-cu13)"
required: true
concurrency:
group: patch-docker-${{ inputs.image_tag }}
cancel-in-progress: true
jobs:
patch:
if: github.repository == 'sgl-project/sglang'
runs-on: x64-docker-build-node
steps:
- name: Cleanup workspace (remove root-owned files from prior runs)
run: sudo rm -rf "$GITHUB_WORKSPACE"/* || true
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull base image and extract commit
run: |
IMAGE="lmsysorg/sglang:${{ inputs.image_tag }}"
docker pull "${IMAGE}"
if BASE_SHA=$(docker run --rm "${IMAGE}" git -C /sgl-workspace/sglang rev-parse HEAD 2>/dev/null); then
echo "Image built from commit: ${BASE_SHA}"
else
BASE_SHA=""
echo "::warning::Image has no .git directory — cannot extract base commit"
fi
echo "BASE_SHA=${BASE_SHA}" >> "$GITHUB_ENV"
- name: Generate patches
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git fetch origin main
mkdir -p /tmp/patch-ctx
if [ -n "${{ inputs.pr_numbers }}" ]; then
IFS=',' read -ra PRS <<< "${{ inputs.pr_numbers }}"
for pr in "${PRS[@]}"; do
pr=$(echo "${pr}" | xargs)
echo "Fetching PR #${pr}"
git fetch origin "pull/${pr}/head:pr-${pr}"
MERGE_BASE=$(git merge-base origin/main "pr-${pr}")
echo " PR #${pr}: merge-base=${MERGE_BASE}"
git diff "${MERGE_BASE}..pr-${pr}" > "/tmp/patch-ctx/${pr}.patch"
echo " PR #${pr}: $(wc -l < /tmp/patch-ctx/${pr}.patch) lines"
done
elif [ -n "${BASE_SHA}" ]; then
echo "Generating diff: image ${BASE_SHA} → latest main"
git fetch origin "${BASE_SHA}"
git diff "${BASE_SHA}..origin/main" > /tmp/patch-ctx/main.patch
echo " main: $(wc -l < /tmp/patch-ctx/main.patch) lines"
else
echo "::error::No PR numbers specified and image has no .git — cannot generate diff against main"
exit 1
fi
TOTAL=$(cat /tmp/patch-ctx/*.patch | wc -l)
if [ "${TOTAL}" -eq 0 ]; then
echo "::warning::All patches are empty — image is already up to date"
echo "SKIP_BUILD=true" >> "$GITHUB_ENV"
fi
- name: Build patched image
if: env.SKIP_BUILD != 'true'
run: |
IMAGE="lmsysorg/sglang:${{ inputs.image_tag }}"
cat <<'DOCKERFILE' > /tmp/patch-ctx/Dockerfile
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
COPY *.patch /tmp/patches/
RUN cd /sgl-workspace/sglang \
&& for p in /tmp/patches/*.patch; do \
if [ ! -s "${p}" ]; then \
echo "Skipping ${p} (empty)"; \
else \
echo "Applying ${p}..." \
&& patch -p1 --fuzz=2 --no-backup-if-mismatch -f < "${p}" \
|| { echo "ERROR: Failed to apply ${p}"; exit 1; }; \
fi; \
done \
&& rm -rf /tmp/patches
DOCKERFILE
docker build \
--no-cache \
--build-arg BASE_IMAGE="${IMAGE}" \
-t "${IMAGE}" \
/tmp/patch-ctx/
- name: Push patched image
if: env.SKIP_BUILD != 'true'
run: |
IMAGE="lmsysorg/sglang:${{ inputs.image_tag }}"
docker push "${IMAGE}"
echo "### Patched \`${IMAGE}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- **Base commit:** \`${BASE_SHA:-unknown (no .git)}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- **Source:** ${{ inputs.pr_numbers && format('PRs: {0}', inputs.pr_numbers) || 'latest main' }}" >> "$GITHUB_STEP_SUMMARY"
+198
View File
@@ -0,0 +1,198 @@
name: PR Benchmark (SMG Components)
on:
push:
branches: [ main ]
paths:
- "sgl-model-gateway/**"
pull_request:
branches: [ main ]
paths:
- "sgl-model-gateway/**"
workflow_dispatch:
concurrency:
group: pr-benchmark-rust-${{ github.ref }}
cancel-in-progress: true
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
permissions:
contents: read
pull-requests: write
issues: write
jobs:
benchmark-compile-check:
name: Benchmark Compilation Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_gateway_dependencies.sh
- name: Configure sccache
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: sgl-model-gateway
shared-key: "rust-cache"
save-if: true
cache-all-crates: true
cache-on-failure: true
- name: Check benchmarks compile
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
cargo check --benches
- name: Show sccache stats
if: always()
run: sccache --show-stats
benchmark:
name: Benchmark - ${{ matrix.name }}
if: |
github.repository == 'sgl-project/sglang' &&
(github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
(contains(github.event.pull_request.labels.*.name, 'router-benchmark') &&
contains(github.event.pull_request.labels.*.name, 'run-ci')))
strategy:
fail-fast: false
matrix:
include:
- name: Request Processing
bench_name: request_processing
bench_args: "benchmark_summary --exact"
runner: ubuntu-latest
sccache_version: "v0.12.0"
artifact_name: request-processing-results
artifact_path: criterion/benchmark_summary/
- name: Manual Policy
bench_name: manual_policy_benchmark
bench_args: ""
runner: ubuntu-latest
sccache_version: "v0.12.0"
artifact_name: manual-policy-results
artifact_path: criterion/manual_policy*/
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 100
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_gateway_dependencies.sh
- name: Configure sccache
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: ${{ matrix.sccache_version }}
disable_annotations: true
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: sgl-model-gateway
shared-key: "rust-cache"
cache-all-crates: true
cache-on-failure: true
save-if: true
- name: Run benchmark
timeout-minutes: 30
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
if command -v sccache &> /dev/null; then
echo "Testing sccache availability..."
export RUSTC_WRAPPER=sccache
export SCCACHE_GHA_ENABLED="true"
if sccache --start-server 2>/dev/null && sccache --show-stats 2>/dev/null; then
echo "sccache is working, using it for compilation"
else
echo "sccache failed to start, falling back to regular cargo"
unset RUSTC_WRAPPER
unset SCCACHE_GHA_ENABLED
fi
else
echo "sccache not available, using regular cargo"
fi
cargo bench --bench ${{ matrix.bench_name }} -- ${{ matrix.bench_args }} 2>&1 | tee benchmark_output.txt
- name: Upload benchmark results
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}-${{ github.sha }}
path: |
sgl-model-gateway/target/${{ matrix.artifact_path }}
sgl-model-gateway/benchmark_output.txt
retention-days: 30
- name: Show sccache stats
if: always()
run: sccache --show-stats
benchmark-summary:
name: Benchmark Summary
needs: [benchmark]
if: always() && (github.repository == 'sgl-project/sglang' || github.event_name == 'pull_request')
runs-on: ubuntu-latest
steps:
- name: Download all benchmark results
uses: actions/download-artifact@v4
with:
pattern: '*-results-${{ github.sha }}'
path: benchmark-results
- name: Generate summary
run: |
generate_section() {
local title="$1" dir_name="$2" lines="${3:-100}"
local dir="benchmark-results/${dir_name}-${{ github.sha }}"
echo "### $title" >> summary.md
if [ -d "$dir" ]; then
echo "✅ **Completed**" >> summary.md
if [ -f "$dir/benchmark_output.txt" ]; then
echo -e "\n<details>\n<summary>View Results</summary>\n\n\`\`\`" >> summary.md
tail -"$lines" "$dir/benchmark_output.txt" >> summary.md
echo -e "\`\`\`\n</details>" >> summary.md
fi
else
echo "❌ Failed or skipped" >> summary.md
fi
echo "" >> summary.md
}
echo "## 🚀 Benchmark Results Summary" > summary.md
echo "" >> summary.md
generate_section "Request Processing" "request-processing-results" 60
generate_section "Manual Policy (Sticky Sessions)" "manual-policy-results" 100
echo -e "---\n_Generated at $(date -u '+%Y-%m-%d %H:%M:%S UTC')_" >> summary.md
cat summary.md
cat summary.md >> $GITHUB_STEP_SUMMARY
- name: Upload summary
uses: actions/upload-artifact@v4
with:
name: benchmark-summary-${{ github.sha }}
path: summary.md
retention-days: 30
+272
View File
@@ -0,0 +1,272 @@
on:
workflow_call:
inputs:
require-run-ci:
description: "Whether the PR must have the run-ci label"
type: boolean
default: true
require-run-ci-extra:
description: "Whether the PR must also have the run-ci-extra label (in addition to run-ci)"
type: boolean
default: false
cool-down-minutes:
description: "Cooldown period in minutes for low-permission users; 0 disables rate limiting"
type: number
default: 120
jobs:
pr-gate:
# 1. for commits on main: no gating needed
# 2. for workflow_dispatch: this can only be triggered by users with write access
runs-on: ubuntu-latest
steps:
- name: Fetch latest PR info
if: github.event_name == 'pull_request'
id: pr
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
core.setOutput("labels", JSON.stringify(pr.data.labels.map(l => l.name)));
core.setOutput("draft", pr.data.draft);
core.setOutput("user", pr.data.user.login);
- name: Log PR info
if: github.event_name == 'pull_request'
run: |
echo "===== PR Info ====="
echo "PR Event: ${{ github.event_name }}"
echo "PR Labels: ${{ steps.pr.outputs.labels }}"
echo "PR Draft: ${{ steps.pr.outputs.draft }}"
echo "PR User: ${{ steps.pr.outputs.user }}"
echo "Require run-ci: ${{ inputs.require-run-ci }}"
echo "Require run-ci-extra: ${{ inputs.require-run-ci-extra }}"
echo "Cool down minutes: ${{ inputs.cool-down-minutes }}"
echo "==================="
- name: Block draft PR
if: github.event_name == 'pull_request' && fromJson(steps.pr.outputs.draft)
run: |
echo "PR is draft. Blocking CI."
exit 1
- name: Require run-ci label (optional)
if: github.event_name == 'pull_request' && inputs.require-run-ci == true
run: |
labels='${{ steps.pr.outputs.labels }}'
if [[ "${{ contains(fromJson(steps.pr.outputs.labels), 'run-ci') }}" == "false" ]]; then
echo "Missing required label 'run-ci'. See https://docs.sglang.io/developer_guide/contribution_guide.html#how-to-trigger-ci-tests for more details."
exit 1
fi
# Live-fetch label gate for pr-test-extra.yml. Workflow-level `if`
# checks would read the frozen event payload, which doesn't update on
# rerun — so /tag-and-rerun-ci extra (slash handler adds the label
# then reruns) could never un-skip. A runtime step using gh-api-fetched
# labels lets reruns pick up the new label set.
- name: Require run-ci-extra label (optional)
if: github.event_name == 'pull_request' && inputs.require-run-ci-extra == true
run: |
if [[ "${{ contains(fromJson(steps.pr.outputs.labels), 'run-ci-extra') }}" == "false" ]]; then
echo "Missing required label 'run-ci-extra'. Add the label (e.g. via /tag-and-rerun-ci extra) to opt this PR into the extra test workflow."
exit 1
fi
- name: Enforce rate limit for low-permission actors (optional)
if: github.event_name == 'pull_request' && inputs.cool-down-minutes > 0
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const DEFAULT_MINUTES = Number("${{ inputs.cool-down-minutes }}");
const owner = context.repo.owner;
const repo = context.repo.repo;
const eventName = context.eventName;
const curRun = await github.rest.actions.getWorkflowRun({
owner, repo, run_id: context.runId
});
let triggeringActor = curRun.data.triggering_actor?.login || context.actor;
if (triggeringActor === "github-actions[bot]") {
triggeringActor = `${{ steps.pr.outputs.user }}`;
core.info(
`triggering_actor is github-actions[bot]; substituting PR author '${triggeringActor}'.`
);
}
async function hasHighPermission(username) {
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username });
const perm = data.permission || 'none';
return perm === 'write' || perm === 'maintain' || perm === 'admin';
} catch (e) {
if (e.status === 404 || e.status === 403) return false;
throw e;
}
}
if (await hasHighPermission(triggeringActor)) {
core.info(`Triggering user '${triggeringActor}' has high permission. No rate limit applied.`);
return;
}
let effectiveCooldownMinutes = DEFAULT_MINUTES;
let perUserCooldownMinutes = null;
try {
const contentResp = await github.rest.repos.getContent({
owner,
repo,
path: ".github/CI_PERMISSIONS.json",
ref: "main",
});
if (!Array.isArray(contentResp.data) && contentResp.data && "content" in contentResp.data) {
const raw = Buffer.from(
contentResp.data.content,
contentResp.data.encoding || "base64"
).toString();
const ciPermissions = JSON.parse(raw);
const userPerm = ciPermissions[triggeringActor];
if (userPerm && typeof userPerm.cooldown_interval_minutes === "number") {
perUserCooldownMinutes = userPerm.cooldown_interval_minutes;
core.info(
`Per-user cooldown for '${triggeringActor}' from CI_PERMISSIONS.json: ${perUserCooldownMinutes} minutes.`
);
} else {
core.info(`No per-user cooldown found for '${triggeringActor}' in CI_PERMISSIONS.json.`);
}
} else {
core.info("CI_PERMISSIONS.json content response is not a file; skipping per-user cooldown.");
}
} catch (e) {
core.info(`CI_PERMISSIONS.json not found or unreadable: ${e.message}. Using default rate limit only.`);
}
if (perUserCooldownMinutes !== null) {
effectiveCooldownMinutes = Math.min(effectiveCooldownMinutes, perUserCooldownMinutes);
}
if (effectiveCooldownMinutes <= 0) {
core.info(
`Effective cooldown for '${triggeringActor}' is 0 minutes; no rate limit enforced for this user.`
);
return;
}
const cutoff = new Date(Date.now() - effectiveCooldownMinutes * 60 * 1000);
core.info(
`Checking for workflow runs since ${cutoff.toISOString()} (last ${effectiveCooldownMinutes} minutes) for event '${eventName}'.`
);
const { data } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'pr-test.yml',
event: eventName,
per_page: 100,
});
const runs = data.workflow_runs || [];
// Rate Limiting Logic:
// We only count workflow runs that actually consumed CI resources (i.e., passed the gate).
// A run "passes the gate" if any jobs beyond the gate jobs (check-changes, pr-gate, call-gate)
// actually executed (not skipped/cancelled). This prevents scenarios where:
// - User has PR A with missing 'run-ci' label (fails at gate)
// - User opens PR B with 'run-ci' label
// - PR B should be able to run even though PR A triggered a run recently
// Helper function to check if a run passed the gate (i.e., actually consumed CI resources)
async function didRunPassGate(run) {
try {
// Note: Fetching up to 100 jobs (API maximum). If a workflow has >100 jobs,
// we may miss some, but this is unlikely in practice.
const { data: jobsData } = await github.rest.actions.listJobsForWorkflowRun({
owner, repo, run_id: run.id, per_page: 100
});
const jobs = jobsData.jobs || [];
// If no jobs exist yet, the run hasn't started consuming resources
if (jobs.length === 0) {
core.info(`Run ${run.id} has no jobs yet; not counting against rate limit.`);
return false;
}
// Gate jobs that don't consume significant CI resources
const gateJobs = ['check-changes', 'pr-gate', 'call-gate', 'pr-test-finish'];
const jobsBeyondGate = jobs.filter(j => !gateJobs.some(g => j.name === g || j.name.startsWith(g + ' ')));
// A job "ran" if it reached a terminal conclusion state that indicates actual execution
const ranStates = ['success', 'failure', 'timed_out', 'action_required'];
const hasJobsThatRan = jobsBeyondGate.some(j => j.conclusion && ranStates.includes(j.conclusion));
return hasJobsThatRan;
} catch (e) {
core.warning(`Could not check jobs for run ${run.id}: ${e.message}`);
// If it's a rate limit error, count it conservatively to prevent abuse
if (e.status === 429) {
core.warning(`Hit rate limit checking run ${run.id}; counting it to be safe.`);
return true;
}
// For cancelled/skipped runs, they likely didn't consume resources
if (run.conclusion === 'cancelled' || run.conclusion === 'skipped') {
return false;
}
// Default to counting it to prevent abuse
return true;
}
}
// Limit the number of runs we'll check in detail to avoid API rate limits
const MAX_RUNS_TO_CHECK = 5;
let runsChecked = 0;
let runsSkippedAtGate = 0;
let recentFound = null;
for (const run of runs) {
if (String(run.id) === String(context.runId)) continue;
if (new Date(run.created_at) < cutoff) continue;
const isUserRun = (run.actor?.login === triggeringActor) || (run.triggering_actor?.login === triggeringActor);
if (!isUserRun) continue;
runsChecked++;
core.info(`Checking run ${run.id} (created: ${run.created_at}, conclusion: ${run.conclusion})`);
// Safety limit: if we've checked too many runs, assume the next one passed to be conservative
if (runsChecked > MAX_RUNS_TO_CHECK) {
core.warning(`Checked ${MAX_RUNS_TO_CHECK} runs; assuming this one passed gate to avoid API limits.`);
recentFound = run;
break;
}
// Only count runs that actually passed the gate and consumed CI resources
if (await didRunPassGate(run)) {
recentFound = run;
core.info(`Found recent run ${run.id} that passed gate.`);
break;
} else {
runsSkippedAtGate++;
core.info(`Run ${run.id} failed at gate; not counting against rate limit.`);
}
}
core.info(`Rate limit check summary: checked ${runsChecked} runs, ${runsSkippedAtGate} failed at gate.`);
if (recentFound) {
core.setFailed(
`User '${triggeringActor}' already triggered '${context.workflow}' via '${eventName}' at ${recentFound.created_at}. ` +
`Please wait ${effectiveCooldownMinutes} minutes before triggering again.`
);
} else {
core.info(
`No recent runs detected for '${triggeringActor}' within the last ${effectiveCooldownMinutes} minutes; proceeding.`
);
}
+219
View File
@@ -0,0 +1,219 @@
name: PR States
# Maintains the CI-states block at the bottom of the PR body. Uses
# pull_request_target (not pull_request) for fork-PR write access; safe
# because we never check out PR head code, only API-read and PATCH the body.
#
# Three triggers, each covering a gap:
# - pull_request_target: push / open / label change (all PRs incl forks).
# - workflow_run: initial pr-test* completion (incl fork PRs; the
# notify-job below can't fire on forks — their pull_request
# GITHUB_TOKEN is forced read-only). Does NOT re-fire for rerun
# attempts, which is why workflow_dispatch is needed too.
# - workflow_dispatch: rerun completions for non-fork PRs, dispatched by
# pr-test*'s notify-pr-states job. workflow_dispatch is the only event
# GITHUB_TOKEN can cascade-create.
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled, unlabeled]
workflow_run:
workflows: ["PR Test Base", "PR Test Extra"]
types: [requested, completed]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number whose CI-states block should be refreshed.'
required: true
type: string
permissions:
pull-requests: write
actions: read
concurrency:
# Per-SHA dedupe (PR number for workflow_dispatch). Queues concurrent
# fires across the three triggers so they don't race on the body PATCH.
group: pr-states-${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha || inputs.pr_number }}
cancel-in-progress: false
jobs:
update-pr-body:
runs-on: ubuntu-latest
steps:
- name: Update CI states block in PR body
uses: actions/github-script@v8
with:
script: |
const RETRY_ATTEMPTS = 3;
const RETRY_DELAY_MS = 6000;
const LABEL_ON_PRESLEEP_MS = 3000;
const LABEL_OFF_PRESLEEP_MS = 6000;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Event-agnostic PR lookup.
// - pull_request_target: PR is in the payload.
// - workflow_run: pull_requests[] is populated for same-repo
// PRs; fork PRs need a head-ref reverse lookup
// ("forkOwner:branch") because
// listPullRequestsAssociatedWithCommit requires the commit to
// be in the default branch. Bail when no open PR matches.
// - workflow_dispatch: pr_number is an explicit input.
let prNumber;
if (context.payload.pull_request) {
prNumber = context.payload.pull_request.number;
} else if (context.eventName === 'workflow_run') {
const wr = context.payload.workflow_run;
if (wr.pull_requests && wr.pull_requests.length > 0) {
prNumber = wr.pull_requests[0].number;
} else if (wr.head_repository && wr.head_branch) {
const headSpec = `${wr.head_repository.owner.login}:${wr.head_branch}`;
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: headSpec,
});
// Multiple PRs could share a head ref if a branch was reused;
// match by head_sha to pick the right one.
const match = prs.find(p => p.head.sha === wr.head_sha) || prs[0];
if (!match) {
core.info(`No open PR for head=${headSpec}; skipping.`);
return;
}
prNumber = match.number;
} else {
core.info(`workflow_run has no PR linkage; skipping.`);
return;
}
} else if (context.eventName === 'workflow_dispatch') {
// Reject non-digit pr_number; parseInt('123abc', 10) returns
// 123 and would silently truncate trailing garbage.
const raw = context.payload.inputs.pr_number;
if (!/^\d+$/.test(raw)) {
core.setFailed(`workflow_dispatch: invalid pr_number input '${raw}' (must be digits only)`);
return;
}
prNumber = parseInt(raw, 10);
} else {
core.info(`Unsupported event '${context.eventName}'; skipping.`);
return;
}
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
const sha = pr.head.sha;
const labels = pr.labels.map(l => l.name);
const hasCI = labels.includes('run-ci');
const hasExtra = labels.includes('run-ci-extra');
// Pre-sleep below only matters when a label was just removed;
// other no-label fires don't race against fresh indexing.
const justRemovedLabel = context.eventName === 'pull_request_target'
&& context.payload.action === 'unlabeled';
async function findRunBySha(workflowFile) {
const { data } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: workflowFile,
head_sha: sha,
per_page: 1,
});
return data.workflow_runs[0] || null;
}
async function findRun(workflowFile, { preSleepMs = 0, attempts = 1, delayMs = 0 }) {
if (preSleepMs > 0) await sleep(preSleepMs);
for (let i = 0; i < attempts; i++) {
const run = await findRunBySha(workflowFile);
if (run) return run;
if (i < attempts - 1) await sleep(delayMs);
}
return null;
}
// Query unconditionally: removing a label does NOT cancel an
// in-flight workflow, so a run from before removal can still be
// live. Label state only picks the fallback text. labelOnOpts
// covers fresh-push indexing race; labelOffOpts only pre-sleeps
// on just-removed-label to catch removal-after-push.
const labelOnOpts = { preSleepMs: LABEL_ON_PRESLEEP_MS, attempts: RETRY_ATTEMPTS, delayMs: RETRY_DELAY_MS };
const labelOffOpts = { preSleepMs: justRemovedLabel ? LABEL_OFF_PRESLEEP_MS : 0, attempts: 1 };
const ptRun = await findRun('pr-test.yml', hasCI ? labelOnOpts : labelOffOpts);
// pr-test-extra gates on BOTH labels (see check-changes there).
const peRun = await findRun('pr-test-extra.yml', (hasCI && hasExtra) ? labelOnOpts : labelOffOpts);
// Skipped run = "no real run" -- happens when a label is added
// after a commit, because GHA doesn't retrigger on `labeled`.
const isReal = (run) => run && run.conclusion !== 'skipped';
const missingCIText = ':x: **Missing `run-ci` label** -- add it to run CI tests.';
const peBlockedByCIText = ':x: **Blocked** -- `run-ci` is required first.';
const notExtraEnabledText = ':warning: **Not enabled** -- add `run-ci-extra` label to opt in.';
const stalePushText = ':warning: **Not run on latest push** -- push again to dispatch.';
// Status icon: body otherwise looks uniformly "dispatched" even
// for gate-failed runs (call-gate exits on missing label).
function runIcon(run) {
if (run.status !== 'completed') return ':hourglass_flowing_sand:';
switch (run.conclusion) {
case 'success': return ':white_check_mark:';
case 'failure': return ':x:';
case 'cancelled': return ':no_entry_sign:';
case 'timed_out': return ':alarm_clock:';
case 'action_required': return ':warning:';
case 'neutral': return ':grey_question:';
default: return ':grey_question:';
}
}
const runLink = (run) => `${runIcon(run)} [Run #${run.id}](${run.html_url})`;
const ptText = isReal(ptRun)
? runLink(ptRun)
: (!hasCI ? missingCIText : '_Not run yet_');
const peText = isReal(peRun)
? runLink(peRun)
: !hasCI
? peBlockedByCIText
: !hasExtra
? notExtraEnabledText
: stalePushText;
const outerStart = '<!-- pr-states:start -->';
const outerEnd = '<!-- pr-states:end -->';
const ptStart = '<!-- slot:pr-test:start -->';
const ptEnd = '<!-- slot:pr-test:end -->';
const peStart = '<!-- slot:pr-test-extra:start -->';
const peEnd = '<!-- slot:pr-test-extra:end -->';
const newBlock = [
outerStart,
'---',
'### CI States',
'',
`Latest PR Test (Base): ${ptStart}${ptText}${ptEnd}`,
`Latest PR Test (Extra): ${peStart}${peText}${peEnd}`,
outerEnd,
].join('\n');
const body = pr.body || '';
const blockRe = new RegExp(`(?:\\n+---\\n+)?${outerStart}[\\s\\S]*?${outerEnd}`);
let newBody;
if (blockRe.test(body)) {
newBody = body.replace(blockRe, `\n\n${newBlock}`);
} else {
const sep = body.length === 0
? ''
: (body.endsWith('\n') ? '\n' : '\n\n');
newBody = `${body}${sep}${newBlock}`;
}
if (newBody !== body) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
body: newBody,
});
}
+244
View File
@@ -0,0 +1,244 @@
name: PR Test Extra (AMD)
# Label-gated AMD extra CI workflow — the AMD mirror of pr-test-extra.yml.
#
# Adds AMD runtime to a PR only when the author opts in: the PR must carry
# BOTH `run-ci` (basic-CI prerequisite) and `run-ci-extra` (explicit opt-in).
# The label check happens at runtime in pr-gate.yml via a live
# `gh pr view`-style fetch, so reruns after adding the labels (e.g. via a
# slash command) pick up the new label set — a workflow-level `if` would read
# the frozen event payload, which never updates on rerun. The job graph also
# runs unconditionally on workflow_dispatch / workflow_call so it can be
# triggered manually or chained from the AMD scheduler.
#
# Stage: extra-a. Each job mirrors the container bring-up of pr-test-amd.yml
# and dispatches `run_suite.py --hw amd --suite extra-a-test-{config}-amd`:
# - 1-gpu-small: mock-model / kv_canary unit + single-GPU canary e2e tests
# - 1-gpu-large: single-GPU model e2e tests (quant fp8kv-triton,
# sessions streaming-session, spec standalone triton,
# kv_canary self-bench)
# - 2-gpu-large: multi-GPU (TP/PP/PD) mock-model + kv_canary e2e tests
# kv_canary e2e is registered to the same extra-a stage as its CUDA siblings
# (it now exercises the ROCm canary kernels end-to-end).
on:
pull_request:
# `labeled` lets the workflow re-fire when `run-ci-extra` (or `run-ci`)
# is added after the latest push. See call-gate.if for the matching guard
# that prevents unrelated label additions from dispatching a full run.
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
inputs:
runner_arch:
description: 'AMD runner pool to dispatch GPU jobs to'
required: false
type: choice
default: mi325
options:
- mi300
- mi325
rocm_version:
description: 'ROCm container variant (empty = Dockerfile default; rocm720 = ROCm 7.2.0)'
required: false
type: choice
default: ''
options:
- ''
- rocm720
aiter_ref:
description: 'Override AITER commit (optional, leave empty to use Dockerfile default)'
required: false
type: string
default: ''
continue_on_error:
description: 'Continue on error (do not fail the workflow on test failures)'
required: false
type: boolean
default: false
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
rocm_version:
description: 'ROCm container variant (empty = Dockerfile default; rocm720 = ROCm 7.2.0)'
required: false
type: string
default: ''
aiter_ref:
description: 'Override AITER commit (optional, leave empty to use Dockerfile default)'
required: false
type: string
default: ''
continue_on_error:
description: 'Continue on error (do not fail the workflow on test failures)'
required: false
type: boolean
default: false
env:
AITER_COMMIT_OVERRIDE: ${{ inputs.aiter_ref }}
DOCKERHUB_AMD_USERNAME: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
DOCKERHUB_AMD_TOKEN: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
concurrency:
# Mirror pr-test-amd.yml: schedule/dispatch runs key on run_id (unique group,
# never cancel each other); PR runs share a per-branch group so pushes cancel
# stale runs. (In a reusable workflow github.event_name is the originating
# event, never workflow_call, so the guard keys off the real events.)
group: pr-test-amd-extra-${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && format('full-{0}', github.run_id) || github.head_ref || github.ref_name || inputs.ref || 'default' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
actions: write
contents: read
issues: read
pull-requests: read
jobs:
# =============================================== PR Gate ====================================================
# Runtime live-fetch label gate (mirrors pr-test-extra.yml's call-gate):
# requires both `run-ci` and `run-ci-extra`. A failure here cascades to
# every test job via `needs`, so a PR without the labels ends in one red
# ~30s gate job plus a row of skipped jobs instead of consuming AMD runners.
#
# The job-level `if` only filters the `labeled` event type so that adding an
# unrelated label doesn't dispatch a full run; the actual label-presence
# gate is enforced at runtime inside pr-gate.yml.
call-gate:
if: |
github.event_name != 'pull_request' ||
github.event.action != 'labeled' ||
github.event.label.name == 'run-ci' ||
github.event.label.name == 'run-ci-extra'
uses: ./.github/workflows/pr-gate.yml
with:
require-run-ci: true
require-run-ci-extra: true
secrets: inherit
# =============================================== extra-a (1-gpu-small) ===============================================
# Single unpartitioned job: the 21 onboarded unit tests total ~233s, so the
# expensive per-job setup (container bring-up + sgl-kernel ROCm build + dep
# install, several minutes) dominates. Partitioning would multiply that
# setup across scarce AMD GPUs to shave only a couple minutes of test time,
# so one GPU running the whole suite sequentially is the better trade.
extra-a-test-1-gpu-small-amd:
name: ${{ format('extra-a-test-1-gpu-small-amd{0} (linux-{1}-1gpu-sglang)', inputs.rocm_version && format('-{0}', inputs.rocm_version) || '', inputs.runner_arch || 'mi325') }}
needs: [call-gate]
if: ${{ !cancelled() && needs.call-gate.result == 'success' }}
runs-on: ${{ format('linux-{0}-1gpu-sglang', inputs.runner_arch || 'mi325') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Ensure VRAM is clear
run: bash scripts/ci/amd/ensure_vram_clear.sh rocm
- name: Start CI container
# `rocm_version` (e.g. rocm720) selects an alternate ROCm container; empty uses the Dockerfile default.
run: bash scripts/ci/amd/amd_ci_start_container.sh ${{ inputs.rocm_version && format('--rocm-version {0}', inputs.rocm_version) || '' }}
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install dependencies
run: bash scripts/ci/amd/amd_ci_install_dependency.sh
- name: Run test
timeout-minutes: 45
run: |
bash scripts/ci/amd/amd_ci_exec.sh -w "/sglang-checkout/test" python3 run_suite.py --hw amd --suite extra-a-test-1-gpu-small-amd --timeout-per-file 1800 ${{ inputs.continue_on_error == true && '--continue-on-error' || '' }}
# =============================================== extra-a (1-gpu-large) ===============================================
# Single-GPU lora / spec / quant model e2e tests. Runs on the same 1-gpu
# pool as small (AMD GPUs carry enough VRAM that "large" here is a CUDA
# memory-tier label, not a separate AMD runner pool).
extra-a-test-1-gpu-large-amd:
name: ${{ format('extra-a-test-1-gpu-large-amd{0} (linux-{1}-1gpu-sglang)', inputs.rocm_version && format('-{0}', inputs.rocm_version) || '', inputs.runner_arch || 'mi325') }}
needs: [call-gate]
if: ${{ !cancelled() && needs.call-gate.result == 'success' }}
runs-on: ${{ format('linux-{0}-1gpu-sglang', inputs.runner_arch || 'mi325') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Ensure VRAM is clear
run: bash scripts/ci/amd/ensure_vram_clear.sh rocm
- name: Start CI container
run: bash scripts/ci/amd/amd_ci_start_container.sh ${{ inputs.rocm_version && format('--rocm-version {0}', inputs.rocm_version) || '' }}
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install dependencies
run: bash scripts/ci/amd/amd_ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
run: |
bash scripts/ci/amd/amd_ci_exec.sh -w "/sglang-checkout/test" python3 run_suite.py --hw amd --suite extra-a-test-1-gpu-large-amd --timeout-per-file 1800 ${{ inputs.continue_on_error == true && '--continue-on-error' || '' }}
# =============================================== extra-a (2-gpu-large) ===============================================
# Multi-GPU TP / PP / PD mock-model + kv_canary e2e tests. Mirrors CUDA's
# extra-a 2-gpu-large stage; runs on the 2-GPU AMD pool.
extra-a-test-2-gpu-large-amd:
name: ${{ format('extra-a-test-2-gpu-large-amd{0} (linux-{1}-2gpu-sglang)', inputs.rocm_version && format('-{0}', inputs.rocm_version) || '', inputs.runner_arch || 'mi325') }}
needs: [call-gate]
if: ${{ !cancelled() && needs.call-gate.result == 'success' }}
runs-on: ${{ format('linux-{0}-2gpu-sglang', inputs.runner_arch || 'mi325') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Ensure VRAM is clear
run: bash scripts/ci/amd/ensure_vram_clear.sh rocm
- name: Start CI container
run: bash scripts/ci/amd/amd_ci_start_container.sh ${{ inputs.rocm_version && format('--rocm-version {0}', inputs.rocm_version) || '' }}
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install dependencies
run: bash scripts/ci/amd/amd_ci_install_dependency.sh
- name: Run test
timeout-minutes: 60
run: |
bash scripts/ci/amd/amd_ci_exec.sh -w "/sglang-checkout/test" python3 run_suite.py --hw amd --suite extra-a-test-2-gpu-large-amd --timeout-per-file 1800 ${{ inputs.continue_on_error == true && '--continue-on-error' || '' }}
# =============================================== aggregator ====================================================
# Single fan-in job so branch protection / notifications depend on one job
# rather than every matrix leg. Fails if any dependent failed or was
# cancelled; `skipped` (e.g. PR without the opt-in labels) is not a failure.
pr-test-amd-extra-finish:
needs:
[
call-gate,
extra-a-test-1-gpu-small-amd,
extra-a-test-1-gpu-large-amd,
extra-a-test-2-gpu-large-amd,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
json_needs='${{ toJson(needs) }}'
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
echo "$job: $result"
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
echo "All jobs completed successfully"
exit 0
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
name: PR Test (Arm64)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-arm64-${{ inputs.ref || github.ref }}
cancel-in-progress: false
jobs:
check-changes:
runs-on: ubuntu-latest
outputs:
main_package: ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests}}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_cpu.toml"
- "test/**/!(*.md)"
- "sgl-kernel/**/*.!(md|txt)"
- ".github/workflows/pr-test-arm64.yml"
- "docker/arm64.Dockerfile"
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.main_package == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
build-test:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Build container
run: |
PR_REPO=${{ github.event.pull_request.head.repo.clone_url }}
PR_HEAD_REF=${{ github.head_ref }}
docker build \
${PR_REPO:+--build-arg SGLANG_REPO=$PR_REPO} \
${PR_HEAD_REF:+--build-arg VER_SGLANG=$PR_HEAD_REF} \
. -f docker/arm64.Dockerfile -t sglang_arm64 --no-cache
- name: Run container
run: |
docker run -dt \
-v ${{ github.workspace }}:/sglang-checkout/ --ipc=host \
--name ci_sglang_arm64 \
sglang_arm64
- name: Arm sanity check
timeout-minutes: 5
run: |
docker exec -w /sglang-checkout/ ci_sglang_arm64 \
bash -c "source /opt/.venv/bin/activate && python3 -c 'import platform; import torch; import sgl_kernel; from sglang.srt.utils.common import is_host_cpu_arm64; assert platform.machine() in (\"aarch64\", \"arm64\"); assert is_host_cpu_arm64(); assert hasattr(torch.ops.sgl_kernel, \"decode_attention_cpu\"); assert hasattr(torch.ops.sgl_kernel, \"initialize\");'"
- name: Run unit tests
timeout-minutes: 36
run: |
docker exec -w /sglang-checkout/ ci_sglang_arm64 \
bash -c "source /opt/.venv/bin/activate && cd ./test && python3 run_suite.py --hw cpu --suite base-b-test-cpu-arm64 --timeout-per-file 1500"
- name: Change permission
timeout-minutes: 2
run: |
docker exec -u root ci_sglang_arm64 bash -c "
rm -rf /tmp/ci-home &&
chown -R $(id -u):$(id -g) /sglang-checkout/ 2>/dev/null || true
"
- name: Cleanup container
if: always()
run: |
docker rm -f ci_sglang_arm64 || true
+320
View File
@@ -0,0 +1,320 @@
name: PR Test Extra
# Label-gated extra CI workflow opted into per-PR via labels.
#
# Adds runtime to a PR only when the author asks for it: the PR must carry
# BOTH `run-ci` (basic-CI prerequisite) and `run-ci-extra` (explicit opt-in
# to this workflow). The label check happens at runtime in pr-gate.yml via
# live `gh pr view --json labels`, so reruns after adding the labels via
# slash command pick up the new label set (workflow-level `if` checks would
# read the frozen event payload, which never updates on rerun). The same
# job graph runs unconditionally on workflow_dispatch / workflow_call so it
# can be triggered manually or chained from another workflow.
#
# Stages: extra-a (1-/2-gpu) and extra-b (4-/8-gpu) caller stubs reuse
# `_pr-test-stage.yml` and `_pr-test-check-changes.yml` from pr-test.yml.
on:
pull_request:
# `labeled` lets the workflow re-fire when `run-ci-extra` (or `run-ci`)
# is added after the latest push — otherwise GHA leaves the stale
# skipped run in place and there's no way to enable extra without
# another push. See check-changes.if below for the matching guard.
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
inputs:
force_continue_on_error:
description: "Force continue-on-error (test scheduled CI behavior)"
required: false
type: boolean
default: false
test_parallel_dispatch:
description: "Test parallel dispatch behavior (simulates scheduled run)"
required: false
type: boolean
default: false
run_all_tests:
description: "Run all tests — bypasses paths-filter (needed when dispatching against long-lived branches whose diff against main can't resolve a merge-base in a shallow clone)"
required: false
type: boolean
default: false
workflow_call:
inputs:
git_ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
skip_pr_test_health_check:
description: "Skip PR test health check fast-fail (e.g. for release branch cuts)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-extra-${{ github.event_name }}-${{ github.head_ref || github.ref_name || 'default' }}-${{ inputs.git_ref || 'all' }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
SKIP_PR_TEST_HEALTH_CHECK: ${{ (inputs.skip_pr_test_health_check == true || inputs.run_all_tests == true) && 'true' || 'false' }}
FORCE_REBUILD_DEEPEP: '1'
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
USE_VENV: false
permissions:
actions: write
contents: read
issues: read
pull-requests: read
jobs:
# =============================================== check changes ====================================================
# The actual `run-ci` + `run-ci-extra` label gate now lives in the
# `call-gate` job below, which calls pr-gate.yml. pr-gate.yml live-fetches
# the PR's current labels at runtime, so a rerun (e.g. via
# /tag-and-rerun-ci extra, which adds labels via GITHUB_TOKEN — these
# don't cascade-trigger labeled events) picks up the new label set and
# passes. The previous workflow-level `if` checked the frozen event
# payload, which never updates on rerun and made the slash-add-label flow
# impossible to recover.
#
# We only keep one filter here: for `labeled` events, only proceed if the
# just-added label is one of the gating labels. This prevents every
# unrelated label addition from dispatching a full CI run.
check-changes:
if: |
github.event_name != 'pull_request' ||
github.event.action != 'labeled' ||
github.event.label.name == 'run-ci' ||
github.event.label.name == 'run-ci-extra'
uses: ./.github/workflows/_pr-test-check-changes.yml
with:
git_ref: ${{ inputs.git_ref || '' }}
run_all_tests: ${{ inputs.run_all_tests == true }}
force_continue_on_error: ${{ inputs.force_continue_on_error == true }}
pr_test_yml: '.github/workflows/pr-test-extra.yml'
secrets: inherit
# =============================================== PR Gate ====================================================
# Runtime live-fetch gate. Mirrors pr-test.yml's call-gate, but additionally
# requires `run-ci-extra`. Failure here cascades down to every test job
# via `needs`, so the workflow ends with one red gate job (~30s) plus a
# row of skipped test jobs instead of consuming a runner.
call-gate:
needs: check-changes
if: github.event_name != 'schedule' && needs.check-changes.result == 'success'
uses: ./.github/workflows/pr-gate.yml
with:
require-run-ci: true
require-run-ci-extra: true
secrets: inherit
# =============================================== sgl-kernel ====================================================
sgl-kernel-build-wheels:
needs: [check-changes, call-gate]
if: |
needs.check-changes.result == 'success' &&
(needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') &&
needs.check-changes.outputs.sgl_kernel == 'true'
uses: ./.github/workflows/_pr-test-sgl-kernel-build.yml
with:
runs_on: x64-kernel-build-node
job_display_name: Build Wheel
git_ref: ${{ inputs.git_ref || '' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
# =============================================== extra-a (1-/2-gpu) ===============================================
extra-a-test-1-gpu-small:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-a-test-1-gpu-small
runner_config: 1-gpu-small
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
extra-a-test-1-gpu-large:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-a-test-1-gpu-large
runner_config: 1-gpu-large
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
timeout_per_file: '1800'
secrets: inherit
extra-a-test-2-gpu-large:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-a-test-2-gpu-large
runner_config: 2-gpu-large
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
# =============================================== extra-b (4-/8-gpu) ===============================================
extra-b-test-4-gpu-h100:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-4-gpu-h100
runner_config: 4-gpu-h100
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
extra-b-test-4-gpu-b200:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-4-gpu-b200
runner_config: 4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
timeout_per_file: '1800'
secrets: inherit
extra-b-test-8-gpu-h200:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-8-gpu-h200
runner_config: 8-gpu-h200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
extra-b-test-deepep-4-gpu-h100:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-deepep-4-gpu-h100
runner_config: deepep-4-gpu-h100
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
extra-b-test-deepep-4-gpu-b200:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-deepep-4-gpu-b200
runner_config: deepep-4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
timeout_per_file: '1800'
secrets: inherit
extra-b-test-deepep-8-gpu-h200:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() && needs.check-changes.result == 'success' && (needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped') }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: extra-b-test-deepep-8-gpu-h200
runner_config: deepep-8-gpu-h200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '60'
secrets: inherit
# =============================================== aggregator ====================================================
# Mirrors pr-test.yml's `pr-test-finish` so notify-pr-states below only
# depends on one job rather than re-listing every stage. Fails if any
# dependent failed/cancelled.
pr-test-extra-finish:
needs:
[
check-changes,
call-gate,
sgl-kernel-build-wheels,
extra-a-test-1-gpu-small,
extra-a-test-1-gpu-large,
extra-a-test-2-gpu-large,
extra-b-test-4-gpu-h100,
extra-b-test-4-gpu-b200,
extra-b-test-8-gpu-h200,
extra-b-test-deepep-4-gpu-h100,
extra-b-test-deepep-4-gpu-b200,
extra-b-test-deepep-8-gpu-h200,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
json_needs='${{ toJson(needs) }}'
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
echo "$job: $result"
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
echo "All jobs completed successfully"
exit 0
# =============================================== notify pr-states ====================================================
# Dispatches pr-states.yml after every attempt (initial + every rerun).
# workflow_run.completed only fires on the initial completion (one
# workflow_run record across attempts), so we explicitly dispatch —
# workflow_dispatch is the only event GITHUB_TOKEN can cascade-create.
#
# Fork PRs are excluded: their pull_request GITHUB_TOKEN is forced
# read-only regardless of declared `actions: write`, so dispatch 403s.
# Fork PRs get pr-states updates via the workflow_run subscription
# there (initial completion) and pull_request_target (push / label).
notify-pr-states:
needs: [pr-test-extra-finish]
if: |
always() &&
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Dispatch pr-states refresh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh workflow run pr-states.yml \
--repo ${{ github.repository }} \
--ref main \
-f pr_number="$PR_NUMBER"
+197
View File
@@ -0,0 +1,197 @@
name: PR Test - JIT Kernel
on:
workflow_call:
inputs:
jit_kernel:
required: true
type: string
sgl_kernel:
required: true
type: string
runner_config:
required: true
type: string
runs_on_map:
required: true
type: string
git_ref:
required: false
type: string
default: ''
test_parallel_dispatch:
required: false
type: string
default: 'false'
skip_pr_test_health_check:
required: false
type: boolean
default: false
# Workflow-level env is NOT inherited from the caller in reusable workflows (verified by CI test).
# The github context (including github.event_name) IS inherited from the caller.
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
SKIP_PR_TEST_HEALTH_CHECK: ${{ inputs.skip_pr_test_health_check == true && 'true' || 'false' }}
jobs:
jit-kernel-unit-test:
# Runs whenever call-jit-kernel-tests dispatches this workflow. That caller is the
# single gate (PR jit_kernel changes, or scheduled/parallel-dispatch full runs), so
# the sub-jobs no longer re-exclude schedule/parallel-dispatch here.
runs-on: 1-gpu-h100
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
if: inputs.sgl_kernel == 'true'
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda13.0
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ inputs.sgl_kernel }} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run test
timeout-minutes: 30
run: |
cd test/
python3 run_suite.py --hw cuda --suite base-b-kernel-unit-test-1-gpu-large
jit-kernel-multigpu-unit-test:
# Runs whenever call-jit-kernel-tests dispatches this workflow. That caller is the
# single gate (PR jit_kernel changes, or scheduled/parallel-dispatch full runs), so
# the sub-jobs no longer re-exclude schedule/parallel-dispatch here.
runs-on: 8-gpu-h200
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-maintenance
- name: Cleanup
if: inputs.sgl_kernel == 'true'
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda13.0
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ inputs.sgl_kernel }} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run multi-GPU test
timeout-minutes: 45
run: |
cd test/
python3 run_suite.py --hw cuda --suite base-b-kernel-unit-test-8-gpu-h200
jit-kernel-benchmark-test:
# Runs whenever call-jit-kernel-tests dispatches this workflow. That caller is the
# single gate (PR jit_kernel changes, or scheduled/parallel-dispatch full runs), so
# the sub-jobs no longer re-exclude schedule/parallel-dispatch here.
runs-on: 1-gpu-h100
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
if: inputs.sgl_kernel == 'true'
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda13.0
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ inputs.sgl_kernel }} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run benchmark tests
timeout-minutes: 45
run: |
cd test/
python3 run_suite.py --hw cuda --suite base-b-kernel-benchmark-test-1-gpu-large
jit-kernel-b200-test:
# Runs whenever call-jit-kernel-tests dispatches this workflow. That caller is the
# single gate (PR jit_kernel changes, or scheduled/parallel-dispatch full runs), so
# the sub-jobs no longer re-exclude schedule/parallel-dispatch here.
runs-on: ${{ fromJson(inputs.runs_on_map)[inputs.runner_config] }}
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
if: inputs.sgl_kernel == 'true'
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda13.0
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{ inputs.sgl_kernel }} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run B200 diffusion test
timeout-minutes: 30
run: |
cd test/
python3 run_suite.py --hw cuda --suite base-b-kernel-unit-test-4-gpu-b200
+184
View File
@@ -0,0 +1,184 @@
name: PR Test (MLX)
on:
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
# target_stage is the job id (e.g. stage-b-e2e-test-mlx), distinct from
# the run_suite.py suite name it dispatches (e.g. stage-b-e2e-mlx).
target_stage:
description: "Select a stage to run from dropdown (leave empty for auto-detect)"
required: false
type: choice
default: ''
options:
- ''
- stage-a-unit-test-mlx
- stage-b-e2e-test-mlx
concurrency:
group: pr-test-mlx-${{ github.ref }}
cancel-in-progress: true
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
changes_exist: ${{ steps.filter.outputs.main_package == 'true'
|| steps.filter.outputs.sgl_kernel == 'true'
|| inputs.target_stage != '' }}
main_package: ${{ steps.filter.outputs.main_package }}
sgl_kernel: ${{ steps.filter.outputs.sgl_kernel }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
with:
# Ignored on pull_request; workflow_dispatch has no PR base without it.
base: main
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_other.toml"
- "test/**/!(*.md)"
- ".github/workflows/pr-test-mlx.yml"
sgl_kernel:
- "sgl-kernel/**/!(*.md|THIRDPARTYNOTICES.txt|LICENSE)"
- ".github/workflows/pr-test-mlx.yml"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.changes_exist == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
stage-a-unit-test-mlx:
needs: [check-changes, pr-gate]
# !cancelled() lets dispatch past a skipped pr-gate; the pull_request
# path still requires pr-gate success, keeping the run-ci gate intact.
if: |
!cancelled() && (
(needs.pr-gate.result == 'success' &&
(needs.check-changes.outputs.main_package == 'true' ||
needs.check-changes.outputs.sgl_kernel == 'true')) ||
(github.event_name == 'workflow_dispatch' &&
inputs.target_stage == 'stage-a-unit-test-mlx')
)
runs-on: macos-26
timeout-minutes: 60
env:
SGLANG_IS_IN_CI: true
# use_mlx() needs this; without it the profiler takes the MPS branch.
SGLANG_USE_MLX: 1
# Forbid HF downloads to enforce the model-free guarantee.
HF_HUB_OFFLINE: 1
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: 'python/pyproject_other.toml'
- name: Verify Apple Silicon runner
run: |
echo "uname -m: $(uname -m)"
python3 -c "import platform; assert platform.machine()=='arm64', platform.machine(); print('machine:', platform.machine(), 'system:', platform.system())"
- name: Install dependencies (MLX / srt_mps extra)
timeout-minutes: 30
run: |
test -f python/pyproject_other.toml || { echo "alt pyproject_other.toml missing"; exit 1; }
# Swap in the Apple Silicon pyproject; srt_mps skips the all_mps diffusion chain.
rm -f python/pyproject.toml
mv python/pyproject_other.toml python/pyproject.toml
uv venv
uv pip install -e "python[srt_mps,test]"
- name: Report MLX / torch versions
run: |
uv run python -c "import mlx.core as mx; print('mlx', mx.__version__)"
uv run python -c "import torch; print('torch', torch.__version__)"
- name: Run model-free MLX unit tests
timeout-minutes: 15
run: |
uv run python test/run_suite.py --hw mlx --suite stage-a-unit-test-mlx
stage-b-e2e-test-mlx:
needs: [check-changes, pr-gate]
# Manual-only: no self hosted Apple Silicon runner is generally available; hosted runners are too small for e2e.
if: |
!cancelled() && inputs.target_stage == 'stage-b-e2e-test-mlx'
runs-on: [self-hosted, macOS, ARM64]
timeout-minutes: 120
env:
SGLANG_IS_IN_CI: true
SGLANG_USE_MLX: 1
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: 'python/pyproject_other.toml'
- name: Verify Apple Silicon runner
run: |
echo "uname -m: $(uname -m)"
python3 -c "import platform; assert platform.machine()=='arm64', platform.machine(); print('machine:', platform.machine(), 'system:', platform.system())"
- name: Install dependencies (MLX / srt_mps extra)
timeout-minutes: 30
run: |
test -f python/pyproject_other.toml || { echo "alt pyproject_other.toml missing"; exit 1; }
rm -f python/pyproject.toml
mv python/pyproject_other.toml python/pyproject.toml
uv venv --python 3.11
uv pip install -e "python[srt_mps,test]"
# Needs a warm HF cache or network and roughly 24 GB of unified memory.
- name: Run MLX e2e smoke tests
timeout-minutes: 90
run: |
uv run python test/run_suite.py --hw mlx --suite stage-b-e2e-mlx
pr-test-mlx-finish:
needs:
[
pr-gate,
check-changes,
stage-a-unit-test-mlx,
stage-b-e2e-test-mlx,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
json_needs='${{ toJson(needs) }}'
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
echo "$job: $result"
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
echo "All jobs completed successfully"
@@ -0,0 +1,505 @@
name: PR Test - Multimodal Gen
on:
workflow_call:
inputs:
multimodal_gen:
required: true
type: string
sgl_kernel:
required: true
type: string
runner_config:
required: true
type: string
runs_on_map:
required: true
type: string
continue_on_error:
required: false
type: string
default: 'false'
git_ref:
required: false
type: string
default: ''
test_parallel_dispatch:
required: false
type: string
default: 'false'
caller_needs_failure:
required: false
type: string
default: 'false'
skip_pr_test_health_check:
required: false
type: string
default: 'false'
# Workflow-level env is NOT inherited from the caller in reusable workflows.
# The github context (including github.event_name) IS inherited from the caller.
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
SKIP_PR_TEST_HEALTH_CHECK: ${{ inputs.skip_pr_test_health_check == 'true' }}
jobs:
compute-diffusion-partitions:
if: inputs.multimodal_gen == 'true'
runs-on: ubuntu-latest
outputs:
matrix-1gpu: ${{ steps.compute.outputs.matrix-1gpu }}
matrix-2gpu: ${{ steps.compute.outputs.matrix-2gpu }}
partition-count-1gpu: ${{ steps.compute.outputs['partition-count-1gpu'] }}
partition-count-2gpu: ${{ steps.compute.outputs['partition-count-2gpu'] }}
plan-1gpu: ${{ steps.compute.outputs.plan-1gpu }}
plan-2gpu: ${{ steps.compute.outputs.plan-2gpu }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Compute partitions
id: compute
run: |
python scripts/ci/utils/diffusion/compute_diffusion_partitions.py --min-time 1200 --target-time 1800 --max-time 2400 --max-partitions 10
multimodal-gen-test-1-gpu:
needs: compute-diffusion-partitions
if: |
always() &&
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-1gpu != '{"include":[]}' &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 1-gpu-h100
timeout-minutes: 240
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-1gpu) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion server tests
timeout-minutes: 240
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-1gpu }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite 1-gpu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-1gpu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
$CONTINUE_ON_ERROR_FLAG
- name: Upload execution report
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-report-1gpu-${{ matrix.part }}
path: python/sglang/multimodal_gen/test/execution_report_*.json
retention-days: 1
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-1gpu-${{ matrix.part }}-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.part }}
multimodal-gen-test-1-5090:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 1-gpu-5090
timeout-minutes: 180
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run 5090 diffusion canary tests
timeout-minutes: 120
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite 1-gpu-5090 \
$CONTINUE_ON_ERROR_FLAG
- name: Upload execution report
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-5090-report-${{ github.run_attempt }}
path: python/sglang/multimodal_gen/test/execution_report_*.json
retention-days: 7
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-5090-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: 1-gpu-5090
bcg-diffusion:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 1-gpu-h100
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run BCG diffusion tests
timeout-minutes: 60
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-bcg-artifacts
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite bcg-diffusion \
$CONTINUE_ON_ERROR_FLAG
- name: Upload BCG diffusion artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-bcg-artifacts-${{ github.run_attempt }}
path: diffusion-bcg-artifacts/
if-no-files-found: ignore
retention-days: 7
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: bcg-diffusion
multimodal-gen-test-2-gpu:
needs: compute-diffusion-partitions
if: |
always() &&
needs.compute-diffusion-partitions.result == 'success' &&
needs.compute-diffusion-partitions.outputs.matrix-2gpu != '{"include":[]}' &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 2-gpu-h100
timeout-minutes: 240
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.compute-diffusion-partitions.outputs.matrix-2gpu) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion server tests
timeout-minutes: 240
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
PARTITION_PLAN_JSON: ${{ needs.compute-diffusion-partitions.outputs.plan-2gpu }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite 2-gpu \
--partition-id ${{ matrix.part }} \
--total-partitions ${{ needs.compute-diffusion-partitions.outputs['partition-count-2gpu'] }} \
--partition-plan-json "$PARTITION_PLAN_JSON" \
$CONTINUE_ON_ERROR_FLAG
- name: Upload execution report
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-report-2gpu-${{ matrix.part }}
path: python/sglang/multimodal_gen/test/execution_report_*.json
retention-days: 1
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-2gpu-${{ matrix.part }}-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
with:
artifact-suffix: ${{ matrix.part }}
multimodal-gen-component-accuracy:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 2-gpu-h100
timeout-minutes: 240
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion component accuracy tests
timeout-minutes: 240
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite component-accuracy \
$CONTINUE_ON_ERROR_FLAG
- uses: ./.github/actions/upload-cuda-coredumps
if: always()
with:
artifact-suffix: component-accuracy
multimodal-gen-test-1-b200:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: ${{ fromJson(inputs.runs_on_map)[inputs.runner_config] }}
timeout-minutes: 240
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
env:
SGLANG_CI_EARLY_LD_LIBRARY_PATH: "1"
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion server tests
timeout-minutes: 240
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
CONTINUE_ON_ERROR_FLAG: ${{ inputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py \
--suite 1-gpu-b200 \
$CONTINUE_ON_ERROR_FLAG
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-${{ github.job }}-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
multimodal-gen-unit-test:
if: |
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == 'true') || (inputs.caller_needs_failure != 'true' && !cancelled())) &&
inputs.multimodal_gen == 'true'
runs-on: 1-gpu-h100
timeout-minutes: 120
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Download artifacts
if: inputs.sgl_kernel == 'true'
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run diffusion unit tests
timeout-minutes: 60
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py --suite unit
diffusion-coverage-check:
needs: [multimodal-gen-test-1-gpu, multimodal-gen-test-2-gpu]
if: |
always() &&
inputs.multimodal_gen == 'true' &&
(
needs.multimodal-gen-test-1-gpu.result == 'success' ||
needs.multimodal-gen-test-1-gpu.result == 'failure' ||
needs.multimodal-gen-test-2-gpu.result == 'success' ||
needs.multimodal-gen-test-2-gpu.result == 'failure'
)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Download all execution reports
uses: actions/download-artifact@v4
with:
path: reports/
pattern: diffusion-report-*
merge-multiple: true
- name: Verify coverage
run: |
python scripts/ci/utils/diffusion/verify_diffusion_coverage.py --reports-dir reports/
+264
View File
@@ -0,0 +1,264 @@
name: PR Test (MUSA)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
input:
target_stage:
description: "Specific test stage to run (Optional)"
required: false
type: string
default: ""
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-musa-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
changes_exist: ${{ steps.filter.outputs.main_package == 'true'
|| steps.filter.outputs.multimodal_gen == 'true'
|| steps.filter.outputs.sgl_kernel == 'true'
|| steps.run-mode.outputs.run_all_tests == 'true'
|| inputs.target_stage != '' }}
main_package: ${{ steps.filter.outputs.main_package == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
multimodal_gen: ${{ steps.filter.outputs.multimodal_gen == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
sgl_kernel: ${{ steps.filter.outputs.sgl_kernel == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
# Run all tests for workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.ref
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- ".github/workflows/pr-test-musa.yml"
- "python/pyproject_other.toml"
- "python/sglang/!(multimodal_gen)/**"
- "python/sglang/srt/hardware_backend/musa/**"
- "scripts/ci/musa/*"
- "scripts/ci/utils/*"
multimodal_gen:
- "python/pyproject_other.toml"
- "python/sglang/multimodal_gen/runtime/platforms/musa.py"
- "python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_rmsnorm.py"
- "python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_silu_and_mul.py"
- "python/sglang/multimodal_gen/test/server/musa/run_suite.py"
- "python/sglang/multimodal_gen/test/server/musa/**"
sgl_kernel:
- ".github/workflows/pr-test-musa.yml"
- "sgl-kernel/csrc/musa/**"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.changes_exist == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
# ==================== Multimodal Gen Tests ==================== #
multimodal-gen-test-1-gpu-musa:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.multimodal_gen == 'true' || inputs.target_stage == 'multimodal-gen-test-1-gpu-musa'
strategy:
fail-fast: false
matrix:
part: [0, 1]
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Checkout code
timeout-minutes: 10
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run diffusion server tests (1-GPU)
timeout-minutes: 30
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd python
python3 sglang/multimodal_gen/test/server/musa/run_suite.py \
--suite 1-gpu-musa \
--partition-id ${{ matrix.part }} \
--total-partitions 2
multimodal-gen-test-2-gpu-musa:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.multimodal_gen == 'true' || inputs.target_stage == 'multimodal-gen-test-2-gpu-musa'
runs-on: s5000-2-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Checkout code
timeout-minutes: 10
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run diffusion server tests (2-GPU)
timeout-minutes: 30
env:
RUNAI_STREAMER_MEMORY_LIMIT: 0
run: |
cd python
python3 sglang/multimodal_gen/test/server/musa/run_suite.py \
--suite 2-gpu-musa
multimodal-gen-layer-unit-test-musa:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.multimodal_gen == 'true' || inputs.target_stage == 'multimodal-gen-layer-unit-test-musa'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
SGLANG_USE_MODELSCOPE: false
SGLANG_IS_IN_CI: true
TORCHADA_ENABLE_CPP_OPS: 1
HF_HOME: /hf-cache
HF_HUB_CACHE: /hf-cache/hub
HF_HUB_OFFLINE: 1
steps:
- name: Checkout code
timeout-minutes: 10
uses: actions/checkout@v4
- name: Install dependencies
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run multimodal gen layer unit test
timeout-minutes: 30
run: |
pytest python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_rmsnorm.py
pytest python/sglang/multimodal_gen/test/unit/musa/layers/test_musa_silu_and_mul.py
# =============================================== sgl-kernel ====================================================
sgl-kernel-unit-test-musa:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.sgl_kernel == 'true' || inputs.target_stage == 'sgl-kernel-unit-test-musa'
runs-on: s5000-1-gpu-runner
timeout-minutes: 240
env:
TORCHADA_ENABLE_CPP_OPS: 1
HF_HUB_CACHE: /hf-cache/hub
steps:
- name: Checkout code
timeout-minutes: 10
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install dependencies
timeout-minutes: 15
run: |
bash scripts/ci/musa/musa_install_dependency.sh
- name: Run sgl-kernel test
timeout-minutes: 20
run: |
pytest sgl-kernel/tests/test_dsv3_router_gemm.py
pytest sgl-kernel/tests/test_per_token_quant_fp8.py
pytest sgl-kernel/tests/speculative/test_eagle_utils.py
pytest sgl-kernel/tests/speculative/test_ngram_utils.py
pytest sgl-kernel/tests/speculative/test_speculative_sampling.py
pytest sgl-kernel/tests/test_torch_defaults_reset.py
pr-test-musa-finish:
needs:
[
pr-gate,
check-changes,
multimodal-gen-test-1-gpu-musa,
multimodal-gen-test-2-gpu-musa,
multimodal-gen-layer-unit-test-musa,
sgl-kernel-unit-test-musa,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
# Convert the 'needs' context to a JSON string
json_needs='${{ toJson(needs) }}'
# Get a list of all job names from the JSON keys
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
# For each job, extract its result
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
# Print the job name and its result
echo "$job: $result"
# Check for failure or cancellation and exit if found
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
# If the loop completes, all jobs were successful
echo "All jobs completed successfully"
exit
+478
View File
@@ -0,0 +1,478 @@
name: PR Test (NPU)
on:
push:
branches: [ main ]
pull_request:
workflow_dispatch:
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-npu-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
changes_exist: ${{ steps.filter.outputs.main_package == 'true' || steps.filter.outputs.multimodal_gen == 'true' || steps.run-mode.outputs.run_all_tests == 'true'}}
main_package: ${{ steps.filter.outputs.main_package == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
multimodal_gen: ${{ steps.filter.outputs.multimodal_gen == 'true' || steps.run-mode.outputs.run_all_tests == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
# Run all tests for workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.ref
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_npu.toml"
- "scripts/ci/npu/npu_ci_install_dependency.sh"
- "test/registered/ascend/**"
- ".github/workflows/pr-test-npu.yml"
multimodal_gen:
- "python/sglang/multimodal_gen/**/!(*.md|*.ipynb)"
- "python/sglang/jit_kernel/diffusion/triton/npu_fallback.py"
- "python/sglang/srt/**"
- "python/pyproject_npu.toml"
- "scripts/ci/npu/npu_ci_install_dependency.sh"
- ".github/workflows/pr-test-npu.yml"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.changes_exist == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
set-image-config:
runs-on: ubuntu-latest
outputs:
CANN_image_a3: ${{ steps.set-vars.outputs.CANN_image_a3 }}
CANN_image_910b: ${{ steps.set-vars.outputs.CANN_image_910b }}
steps:
# When triggered by PR, no inputs parameters are used. The latest community code is tested by default.
- name: Set image config
id: set-vars
run: |
echo "CANN_image_a3=swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-a3-ubuntu22.04-py3.11" >> $GITHUB_OUTPUT
echo "CANN_image_910b=swr.cn-southwest-2.myhuaweicloud.com/base_image/ascend-ci/cann:9.0.0-910b-ubuntu22.04-py3.11" >> $GITHUB_OUTPUT
stage-b-test-1-npu-a2:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: linux-aarch64-a2-1
strategy:
fail-fast: false
matrix:
part: [ 0, 1 ]
container:
image: ${{ needs.set-image-config.outputs.CANN_image_910b }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh 910b
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd test
python3 run_suite.py --hw npu --suite stage-b-test-1-npu-a2 --auto-partition-id ${{ matrix.part }} --auto-partition-size 2
stage-b-test-2-npu-a2:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: linux-aarch64-a2-2
strategy:
fail-fast: true
matrix:
part: [0, 1]
container:
image: ${{ needs.set-image-config.outputs.CANN_image_910b }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh 910b
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd test
python3 run_suite.py --hw npu --suite stage-b-test-2-npu-a2 --auto-partition-id ${{ matrix.part }} --auto-partition-size 2
stage-b-test-4-npu-a3:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: linux-aarch64-a3-4
container:
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd test
python3 run_suite.py --hw npu --suite stage-b-test-4-npu-a3 --timeout-per-file 3600
stage-b-test-16-npu-a3:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: linux-aarch64-a3-16
container:
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
run: |
cd test
python3 run_suite.py --hw npu --suite stage-b-test-16-npu-a3 --timeout-per-file 3600
multimodal-gen-test-1-npu-a3:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.multimodal_gen == 'true'
runs-on: linux-aarch64-a3-2
container:
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py --suite 1-npu
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-npu-1-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
multimodal-gen-test-2-npu-a3:
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.multimodal_gen == 'true'
runs-on: linux-aarch64-a3-16
container:
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Mark repository safe
run: |
git config --system --add safe.directory ${GITHUB_WORKSPACE}
- name: Install dependencies
env:
TORCH_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/whl/cpu"
PYPI_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
UV_INDEX_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple"
GITHUB_PROXY_URL: "https://gh-proxy.test.osinfra.cn/"
RUSTUP_CACHE_URL: "http://cache-service.nginx-pypi-cache.svc.cluster.local:8082"
run: |
# speed up by using infra cache services
CACHING_URL="cache-service.nginx-pypi-cache.svc.cluster.local"
sed -Ei "s@(ports|archive).ubuntu.com@${CACHING_URL}:8081@g" /etc/apt/sources.list
pip config set global.index-url http://${CACHING_URL}/pypi/simple
pip config set global.trusted-host "${CACHING_URL}"
bash scripts/ci/npu/npu_ci_install_dependency.sh a3
# copy required file from our daily cache
cp ~/.cache/modelscope/hub/datasets/otavia/ShareGPT_Vicuna_unfiltered/ShareGPT_V3_unfiltered_cleaned_split.json /tmp
# copy gsm8k dataset
cp ~/.cache/modelscope/hub/datasets/tmp/test.jsonl /tmp
- name: Run test
timeout-minutes: 60
env:
SGLANG_USE_MODELSCOPE: true
SGLANG_IS_IN_CI: true
HF_ENDPOINT: https://hf-mirror.com
TORCH_EXTENSIONS_DIR: /tmp/torch_extensions
PYTORCH_NPU_ALLOC_CONF: "expandable_segments:True"
STREAMS_PER_DEVICE: 32
SGLANG_DIFFUSION_ARTIFACT_DIR: ${{ github.workspace }}/diffusion-failures
run: |
cd python
python3 sglang/multimodal_gen/test/run_suite.py --suite 2-npu
- name: Upload diffusion failure artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: diffusion-failures-npu-2-${{ github.run_attempt }}
path: diffusion-failures/
if-no-files-found: ignore
retention-days: 7
pr-single-node-tests:
name: single-node-poc
needs: [check-changes, pr-gate, set-image-config]
if: needs.check-changes.outputs.main_package == 'true'
strategy:
fail-fast: false
max-parallel: 6
matrix:
test_config:
# qwen3_6_27b performance tests
- name: qwen3_6_27b_w8a8_1p_in64k_out1k_50ms
runner: linux-aarch64-a3-2
test_case: test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py
test_type: 'perf'
uses: ./.github/workflows/nightly-test-npu-e2e-single-node.yml
with:
runner: ${{ matrix.test_config.runner }}
test_type: ${{ matrix.test_config.test_type }}
test_config_name: ${{ matrix.test_config.name }}
test_case: ${{ matrix.test_config.test_case }}
image: ${{ needs.set-image-config.outputs.CANN_image_a3 }}
install_sglang_from_source: false
install_sglang_deps: true
device_type_for_deps: 'a3'
transformers_version: ''
pr-test-npu-finish:
needs:
[
check-changes,
stage-b-test-1-npu-a2,
stage-b-test-2-npu-a2,
stage-b-test-4-npu-a3,
stage-b-test-16-npu-a3,
multimodal-gen-test-1-npu-a3,
multimodal-gen-test-2-npu-a3,
pr-single-node-tests,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
# Convert the 'needs' context to a JSON string
json_needs='${{ toJson(needs) }}'
# Get a list of all job names from the JSON keys
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
# For each job, extract its result
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
# Print the job name and its result
echo "$job: $result"
# Check for failure or cancellation and exit if found
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
# If the loop completes, all jobs were successful
echo "All jobs completed successfully"
exit 0
+456
View File
@@ -0,0 +1,456 @@
name: PR Test (SMG)
on:
push:
branches: [ main ]
paths:
- "sgl-model-gateway/**"
- ".github/workflows/pr-test-rust.yml"
- "scripts/ci/cuda/ci_install_dependency.sh"
- "scripts/ci/cuda/ci_install_gateway_dependencies.sh"
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened, labeled]
paths:
- "sgl-model-gateway/**"
- ".github/workflows/pr-test-rust.yml"
- "scripts/ci/cuda/ci_install_dependency.sh"
- "scripts/ci/cuda/ci_install_gateway_dependencies.sh"
workflow_dispatch:
concurrency:
group: gateway-tests-${{ github.ref }}
cancel-in-progress: true
env:
SGLANG_IS_IN_CI: true
jobs:
build-wheel:
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
# Pin to 22.04 so the wheel auditwheel-tags as manylinux_2_35; the
# self-hosted GPU runners are Ubuntu 22.04 (glibc 2.35) and reject
# manylinux_2_39 wheels produced on ubuntu-latest (Ubuntu 24.04).
runs-on: ubuntu-22.04
# sccache is only installed on the GitHub-hosted runners that run this
# job and `unit-tests`; setting RUSTC_WRAPPER workflow-wide leaks it to
# gateway-e2e on the self-hosted GPU runners (which don't have sccache),
# so any pip-install that compiles a Rust extension would fail with
# `could not execute process \`sccache rustc\``.
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install rust dependencies
run: |
bash scripts/ci/cuda/ci_install_gateway_dependencies.sh
- name: Configure sccache
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: sgl-model-gateway
shared-key: "rust-cache"
cache-all-crates: true
cache-on-failure: true
save-if: true
- name: Build python binding
run: |
source "$HOME/.cargo/env"
export RUSTC_WRAPPER=sccache
cd sgl-model-gateway/bindings/python
python3 -m pip install --upgrade pip maturin
maturin build --profile ci --features vendored-openssl --out dist
- name: List built wheel
run: ls -lh sgl-model-gateway/bindings/python/dist/
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: smg-wheel
path: sgl-model-gateway/bindings/python/dist/*.whl
retention-days: 1
- name: Test wheel install
run: |
pip install sgl-model-gateway/bindings/python/dist/*.whl
python3 -c "import sglang_router; print('Python package: OK')"
python3 -c "from sglang_router.sglang_router_rs import Router; print('Rust extension: OK')"
python3 -m sglang_router.launch_router --help > /dev/null && echo "Entry point: OK"
python-unit-tests:
needs: build-wheel
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: smg-wheel
path: dist/
- name: Install wheel
run: pip install dist/*.whl
- name: Run Python unit tests
run: |
cd bindings/python
python3 -m pip install pytest pytest-cov pytest-xdist
pytest -q tests --cov=sglang_router --cov-config=.coveragerc --cov-report=term-missing --cov-fail-under=80
unit-tests:
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-latest
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_gateway_dependencies.sh
- name: Configure sccache
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: sgl-model-gateway
shared-key: "rust-cache"
cache-all-crates: true
cache-on-failure: true
save-if: true
- name: Run lint
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
rustup component add clippy
cargo clippy --all-targets --all-features -- -D warnings
- name: Run fmt
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt
rustup toolchain install nightly --profile minimal
cargo +nightly fmt -- --check
- name: Generate vision golden fixtures
run: |
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
pip install transformers pillow numpy scipy
pip install transformers pillow numpy
cd sgl-model-gateway/
python scripts/generate_vision_golden.py
- name: Run Rust tests
timeout-minutes: 20
run: |
source "$HOME/.cargo/env"
cd sgl-model-gateway/
cargo test
- name: Show sccache stats
if: always()
run: sccache --show-stats
gateway-e2e:
name: ${{ matrix.name }}
needs: build-wheel
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
# The `responses` matrix entry is intentionally omitted. It needs
# `docker run gvenzl/oracle-xe` + `docker run shoofio/brave-search-mcp-sse`
# on the runner host, but the 2-/4-gpu-h100 runners are themselves
# containers without a Docker daemon. Re-enable by adding back:
# - name: responses
# runner: 2-gpu-h100
# timeout: 45
# test_dirs: "e2e_test/responses"
# extra_deps: ""
# env_vars: "SHOW_WORKER_LOGS=0 SHOW_ROUTER_LOGS=1"
# reruns: "--reruns 2 --reruns-delay 5"
# setup_oracle: true
# setup_brave: true
# parallel_opts: ""
# plus the Oracle Instant Client / `gvenzl/oracle-xe` /
# `shoofio/brave-search-mcp-sse` setup + cleanup steps (see commit
# cf346bb15 for the exact step bodies) once a runner with
# `docker.sock` (or binary-installed deps) is available.
strategy:
fail-fast: false
matrix:
include:
- name: benchmarks
# 4 GPUs: test_pd_perf.py uses workers(prefill=2, decode=2) and
# test_regular_perf.py uses workers(count=4) — both need tp*workers=4.
runner: 4-gpu-h100
timeout: 32
test_dirs: "e2e_test/benchmarks"
extra_deps: "genai-bench==0.0.3"
env_vars: ""
reruns: ""
upload_benchmarks: true
parallel_opts: "" # No parallel for benchmarks (performance measurement)
- name: e2e
runner: 2-gpu-h100
timeout: 45
test_dirs: "e2e_test/router e2e_test/embeddings"
extra_deps: ""
env_vars: "SHOW_WORKER_LOGS=0 SHOW_ROUTER_LOGS=1"
reruns: "--reruns 2 --reruns-delay 5"
# Run tests serially. pytest-parallel (unmaintained since 2019)
# has buggy fixture-finalize handling under thread dispatch:
# both class- and function-scoped fixture references leaked
# between tests, leaving model_pool instances pinned at
# _ref_count > 0 and deadlocking later tests that needed
# eviction (50+ min hangs). On a 2-GPU runner with 5 distinct
# model:mode combos in router+embeddings, the suite is
# eviction-bound anyway, so the parallel speedup was illusory.
parallel_opts: ""
- name: chat-completions
runner: 2-gpu-h100
timeout: 45
test_dirs: "e2e_test/chat_completions"
extra_deps: ""
env_vars: "SHOW_WORKER_LOGS=0 SHOW_ROUTER_LOGS=1"
reruns: "--reruns 2 --reruns-delay 5"
parallel_opts: ""
- name: chat-completions-4gpu
runner: 4-gpu-h100
timeout: 45
# qwen-30b (tp=4) tests can't fit on the 2-gpu-h100 matrix entries —
# they get skipped there by hooks.py. Run them here so coverage holds.
test_dirs: "e2e_test/chat_completions/test_enable_thinking.py"
extra_deps: ""
env_vars: "SHOW_WORKER_LOGS=0 SHOW_ROUTER_LOGS=1"
reruns: "--reruns 2 --reruns-delay 5"
parallel_opts: ""
runs-on: ${{ matrix.runner }}
timeout-minutes: ${{ matrix.timeout }}
# Self-hosted GPU runners are scarce; serialize per hardware type so
# 2-gpu-h100 and 4-gpu-h100 each run one job at a time across all
# in-flight PRs. Queue rather than cancel — different refs shouldn't
# interrupt each other. Include matrix.name in the group so siblings
# within the same run don't collide: GitHub keeps only one pending job
# per concurrency group, so matrix entries sharing a group (e.g.
# benchmarks + chat-completions-4gpu on 4-gpu-h100) would evict each
# other regardless of cancel-in-progress: false.
concurrency:
group: pr-test-rust-${{ matrix.runner }}-${{ matrix.name }}
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install SGLang dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: smg-wheel
path: wheel/
- name: Install wheel
run: |
pip uninstall -y sglang-router || true
pip install wheel/*.whl
- name: Install e2e test dependencies
run: |
python3 -m pip install pytest pytest-rerunfailures httpx openai grpcio grpcio-health-checking numpy
if [ -n "${{ matrix.extra_deps }}" ]; then
if echo "${{ matrix.extra_deps }}" | grep -q "genai-bench"; then
# genai-bench's transitive deps (oci/locust) pull
# transformers<5 which requires huggingface_hub<1.0 — that
# downgrades the 1.11.0 ci_install_dependency.sh settled on
# and breaks `kernels` (requires huggingface_hub>=1.3.0,<2.0).
# Install --no-deps and supply the runtime deps explicitly.
python3 -m pip --no-cache-dir install --no-deps ${{ matrix.extra_deps }}
python3 -m pip --no-cache-dir install \
locust click rich tenacity oci openpyxl gevent matplotlib
else
# Other extras with well-behaved transitive deps —
# normal --upgrade install path.
python3 -m pip --no-cache-dir install --upgrade ${{ matrix.extra_deps }}
fi
fi
- name: Run E2E tests
run: |
python3 python/sglang/cli/killall.py
cd sgl-model-gateway
${{ matrix.env_vars }} pytest ${{ matrix.reruns }} ${{ matrix.parallel_opts }} ${{ matrix.test_dirs }} -s -vv -o log_cli=true --log-cli-level=INFO
- name: Upload benchmark results
if: matrix.upload_benchmarks && success()
uses: actions/upload-artifact@v4
with:
name: genai-bench-results-all-policies
path: sgl-model-gateway/benchmark_**/
docker-build-test:
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image (no push)
uses: docker/build-push-action@v5
with:
context: .
file: docker/gateway.Dockerfile
push: false
tags: sgl-model-gateway:test
cache-from: type=gha
cache-to: type=gha,mode=max
k8s-integration:
# Runs SMG against a kind cluster with fake worker pods to exercise the
# K8s service discovery / reconciliation path. No GPU required (workers
# are python:3.12-slim mocks); the h100 matrix runners are unsuitable
# because they're containers without a Docker daemon.
if: |
github.event_name != 'pull_request' ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run-ci')) ||
(github.event.action == 'labeled' && github.event.label.name == 'run-ci')
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install kind and kubectl
run: |
curl -fsSLo /tmp/kind https://kind.sigs.k8s.io/dl/v0.24.0/kind-linux-amd64
chmod +x /tmp/kind && sudo mv /tmp/kind /usr/local/bin/kind
KUBECTL_VERSION=$(curl -fsSL https://dl.k8s.io/release/stable.txt)
curl -fsSLo /tmp/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
chmod +x /tmp/kubectl && sudo mv /tmp/kubectl /usr/local/bin/kubectl
kind --version
kubectl version --client
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build smg-gateway:test image
uses: docker/build-push-action@v5
with:
context: sgl-model-gateway
file: sgl-model-gateway/e2e_test/k8s_integration/Dockerfile.gateway
tags: smg-gateway:test
load: true
cache-from: type=gha,scope=k8s-integration
cache-to: type=gha,scope=k8s-integration,mode=max
- name: Install Python test dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install pytest httpx
- name: Set up kind cluster and deploy
env:
SKIP_DOCKER_BUILD: "1"
run: |
cd sgl-model-gateway
bash e2e_test/k8s_integration/setup.sh
- name: Run K8s integration tests
run: |
cd sgl-model-gateway
# confcutdir avoids loading the parent e2e_test/conftest.py, which
# pulls in heavy infra deps (requests, sglang_router) that this job
# intentionally doesn't install.
pytest e2e_test/k8s_integration/ \
--confcutdir=e2e_test/k8s_integration \
-v -s -o log_cli=true --log-cli-level=INFO
- name: Dump cluster state on failure
if: failure()
run: |
kubectl --context kind-smg-test get all -A || true
kubectl --context kind-smg-test -n smg-test describe pods || true
kubectl --context kind-smg-test -n smg-test logs deploy/smg-gateway --tail=200 || true
- name: Tear down kind cluster
if: always()
run: |
cd sgl-model-gateway
bash e2e_test/k8s_integration/setup.sh teardown || true
finish:
needs: [build-wheel, python-unit-tests, unit-tests, gateway-e2e, docker-build-test, k8s-integration]
runs-on: ubuntu-latest
steps:
- name: Finish
run: echo "This is an empty step to ensure that all jobs are completed."
summarize-benchmarks:
needs: gateway-e2e
runs-on: ubuntu-latest
if: success()
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download benchmark results
uses: actions/download-artifact@v4
with:
name: genai-bench-results-all-policies
- name: Create benchmark summary
run: python3 sgl-model-gateway/e2e_test/benchmarks/summarize.py .
+178
View File
@@ -0,0 +1,178 @@
name: PR Test - SGL Kernel
on:
workflow_call:
inputs:
sgl_kernel:
required: true
type: string
runner_config:
required: true
type: string
runs_on_map:
required: true
type: string
git_ref:
required: false
type: string
default: ''
skip_pr_test_health_check:
required: false
type: boolean
default: false
# Workflow-level env is NOT inherited from the caller in reusable workflows.
# The github context (including github.event_name) IS inherited from the caller.
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
SKIP_PR_TEST_HEALTH_CHECK: ${{ inputs.skip_pr_test_health_check == true && 'true' || 'false' }}
jobs:
sgl-kernel-unit-test:
runs-on: 1-gpu-h100
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run test
timeout-minutes: 30
run: |
cd sgl-kernel
pytest tests/
sgl-kernel-benchmark-test:
runs-on: 1-gpu-h100
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run benchmark tests
timeout-minutes: 45
run: |
cd sgl-kernel/benchmark
echo "Running sgl-kernel benchmark tests in CI mode..."
echo "CI environment variable: $CI"
echo "GITHUB_ACTIONS environment variable: $GITHUB_ACTIONS"
for bench_file in bench_*.py; do
echo "Testing $bench_file..."
timeout 60 python3 "$bench_file" || echo "Warning: $bench_file timed out or failed, continuing..."
echo "Completed $bench_file"
echo "---"
done
echo "All benchmark tests completed!"
sgl-kernel-b200-test:
runs-on: ${{ fromJson(inputs.runs_on_map)[inputs.runner_config] }}
timeout-minutes: 240
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Cleanup
run: |
ls -alh sgl-kernel/dist || true
rm -rf sgl-kernel/dist/* || true
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-python3.10-cuda*
- name: Install dependencies
timeout-minutes: 20
run: |
CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run sgl-kernel unit tests on B200
timeout-minutes: 30
run: |
cd sgl-kernel
pytest tests/
# Adding a single CUDA13 build-and-run check for the kernel
# TODO: Add back this test when it can pass on CI
# cuda13-kernel-build-check:
# if: inputs.sgl_kernel == 'true'
# runs-on: x64-cu13-kernel-tests
# steps:
# - uses: actions/checkout@v4
# - name: Cleanup
# run: |
# ls -alh sgl-kernel/dist || true
# rm -rf sgl-kernel/dist/* || true
# - name: Download CUDA 13.0 artifacts
# uses: actions/download-artifact@v4
# with:
# path: sgl-kernel/dist/
# merge-multiple: true
# pattern: wheel-python3.10-cuda*
# - name: Install dependencies
# run: |
# CUSTOM_BUILD_SGL_KERNEL=${{inputs.sgl_kernel}} bash scripts/ci/cuda/ci_install_dependency.sh
# - name: Run kernel unit tests
# timeout-minutes: 30
# run: |
# cd sgl-kernel
# pytest tests/
+434
View File
@@ -0,0 +1,434 @@
name: PR Test (sgl-router)
# Trigger contract — modeled on `pr-test.yml`:
#
# * No `paths:` filter at the trigger level. The workflow ALWAYS
# fires on every PR synchronize / push, so the run record always
# appears as a check on the PR (no more "workflow silently didn't
# fire" mystery debugging). Path-based skip decisions are made
# inside the `sgl-router-gate` job below, where we can log the
# reason. Same for the `run-ci` label gate — moved off the
# workflow trigger and into the gate job so its outcome is
# observable.
#
# * `push` on `main` keeps firing so post-merge runs still record
# against the default branch.
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
concurrency:
group: sgl-router-${{ github.event_name }}-${{ github.head_ref || github.ref_name || 'default' }}
cancel-in-progress: true
env:
SGLANG_IS_IN_CI: true
jobs:
# Stage 0 — decide whether to run the heavy tiers. Always runs
# (cheap, ubuntu-latest), and emits a single `should_run` output
# consumed by every tier below. Reasons it might evict downstream
# work:
# * `pull_request` event without the `run-ci` label (budget gate)
# * No files matching the sgl-router paths changed in this PR
# Either outcome is logged on the gate job's page, so operators can
# see *why* the tiers were skipped instead of guessing.
sgl-router-gate:
name: gate
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.decide.outputs.should_run }}
paths_changed: ${{ steps.paths.outputs.sgl_router }}
has_run_ci_label: ${{ steps.label.outputs.has_run_ci }}
steps:
- uses: actions/checkout@v4
# `dorny/paths-filter` computes the diff against the PR base
# (for pull_request) or the push's `before` SHA (for push) and
# exposes `sgl_router=true|false` on whether any tracked path
# changed. Replaces the old workflow-level `paths:` filter so
# the gate is observable.
- name: Detect sgl-router path changes
id: paths
uses: dorny/paths-filter@v3
with:
filters: |
sgl_router:
- 'experimental/sgl-router/**'
- '.github/workflows/pr-test-sgl-router.yml'
- 'scripts/ci/cuda/ci_install_dependency.sh'
# `run-ci` label gate. Cheap workflow runs (e.g. docs-only PRs
# that happen to touch the sgl-router dir) still need the
# opt-in label before consuming the H100 / kind tiers below.
# `push` events on main and `workflow_dispatch` skip this gate.
- name: Check run-ci label
id: label
run: |
if [[ "${{ github.event_name }}" != "pull_request" ]]; then
echo "has_run_ci=true" >> "$GITHUB_OUTPUT"
echo "Non-PR event (${{ github.event_name }}); label gate bypassed."
exit 0
fi
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'run-ci') }}" == "true" ]]; then
echo "has_run_ci=true" >> "$GITHUB_OUTPUT"
echo "PR has run-ci label; downstream tiers will run."
else
echo "has_run_ci=false" >> "$GITHUB_OUTPUT"
echo "::warning::PR is missing the 'run-ci' label; skipping sgl-router tiers. Apply the label to opt in."
fi
- name: Decide
id: decide
run: |
paths='${{ steps.paths.outputs.sgl_router }}'
label='${{ steps.label.outputs.has_run_ci }}'
if [[ "$paths" == "true" && "$label" == "true" ]]; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "Both path filter and run-ci label match; running tiers."
else
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "Skipping tiers (paths_changed=$paths, has_run_ci=$label)."
fi
sgl-router-lint:
name: tier-1 — lint
needs: sgl-router-gate
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: ubuntu-latest
env:
# Scoped per-job (not workflow-wide) to avoid SMG's documented breakage
# of self-hosted-runner pip installs that compile Rust extensions.
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
# Supply-chain guard: deny.toml allowlists the dynamo git source but
# cannot pin a SHA. Bumps to this constant must be a deliberate PR.
DYNAMO_TOKENIZERS_EXPECTED_REV: "1efdd4dcb901caeae636131321094090d252c8d6"
steps:
- uses: actions/checkout@v4
- name: Verify dynamo-* SHA pin
run: |
set -euo pipefail
revs=$(grep -E '^dynamo-[a-z]+[[:space:]]*=.*\brev[[:space:]]*=' experimental/sgl-router/Cargo.toml \
| sed -E 's/.*rev[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/')
if [ -z "${revs:-}" ]; then
echo "::error::no dynamo-* git deps with rev= found in experimental/sgl-router/Cargo.toml"
exit 1
fi
count=0
while IFS= read -r r; do
count=$((count + 1))
if [ "$r" != "$DYNAMO_TOKENIZERS_EXPECTED_REV" ]; then
echo "::error::dynamo-* SHA pin mismatch in experimental/sgl-router/Cargo.toml"
echo " expected: $DYNAMO_TOKENIZERS_EXPECTED_REV"
echo " actual: $r"
echo "All revs found:"
echo "$revs" | sed 's/^/ /'
exit 1
fi
done <<< "$revs"
echo "dynamo-* SHA pin OK: $count entries all at $DYNAMO_TOKENIZERS_EXPECTED_REV"
# sccache install is best-effort: the GitHub release CDN intermittently
# returns 5xx and the action's built-in retry is shallow. If it fails,
# we unset RUSTC_WRAPPER below and proceed without compile caching —
# rust-cache still covers the bulk of warm-cache wins.
- name: Configure sccache
id: sccache
continue-on-error: true
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Disable sccache if install failed
if: steps.sccache.outcome != 'success'
run: |
echo "::warning::sccache install failed (${{ steps.sccache.outcome }}); proceeding without compile caching"
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: experimental/sgl-router
shared-key: "sgl-router-cache"
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo check
working-directory: experimental/sgl-router
run: cargo check --all-targets
- name: cargo clippy
working-directory: experimental/sgl-router
run: cargo clippy --all-targets -- -D warnings
- name: cargo fmt
working-directory: experimental/sgl-router
run: |
rustup toolchain install nightly --profile minimal --component rustfmt
cargo +nightly fmt -- --check
# cargo-deny 0.19.6+ required for CVSS 4.0 parsing.
- name: Install cargo-deny
run: cargo install --locked cargo-deny@0.19.6
- name: cargo deny check
working-directory: experimental/sgl-router
run: cargo deny check licenses bans sources advisories
- name: Show sccache stats
if: always() && steps.sccache.outcome == 'success'
run: sccache --show-stats
sgl-router-build-and-test:
name: tier-2 — build + test
needs: [sgl-router-gate, sgl-router-lint]
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: ubuntu-latest
env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install OS deps (libssl, pkg-config)
run: |
sudo apt-get update
sudo apt-get install -y libssl-dev pkg-config
# sccache install is best-effort: the GitHub release CDN intermittently
# returns 5xx and the action's built-in retry is shallow. If it fails,
# we unset RUSTC_WRAPPER below and proceed without compile caching —
# rust-cache still covers the bulk of warm-cache wins.
- name: Configure sccache
id: sccache
continue-on-error: true
uses: mozilla-actions/sccache-action@v0.0.9
with:
version: "v0.12.0"
disable_annotations: true
- name: Disable sccache if install failed
if: steps.sccache.outcome != 'success'
run: |
echo "::warning::sccache install failed (${{ steps.sccache.outcome }}); proceeding without compile caching"
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: experimental/sgl-router
shared-key: "sgl-router-cache"
cache-all-crates: true
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo build (record wall time)
working-directory: experimental/sgl-router
run: |
START=$(date +%s)
cargo build --release --all-targets
END=$(date +%s)
BUILD_SECONDS=$((END - START))
echo "tier_2_build_duration_seconds=${BUILD_SECONDS}" >> "$GITHUB_ENV"
echo "::notice title=Tier-2 build duration::${BUILD_SECONDS}s"
- name: cargo test
working-directory: experimental/sgl-router
# Skip the tokenizer parity_matrix test here: ubuntu-latest has
# no HuggingFace cache, so the matrix would hard-fail (see the
# test docstring). The e2e job runs it after pytest populates
# the Qwen3-0.6B tokenizer.json. Filter is matched against the
# full test path `tokenizer::parity::parity_matrix`; substring
# `parity_matrix` is unique to that test.
run: cargo test --release -- --skip parity_matrix
# Regenerate the cross-impl block-hash parity fixture and fail if it
# differs from the committed file. The Python script replicates
# SGLang's `radix_cache.hash_page` algorithm verbatim (no
# `import sglang`); a diff here means EITHER the Rust port's algorithm
# drifted from the script, OR the script drifted from SGLang's actual
# code — in both cases the operator must investigate and re-commit
# the fixture in the same PR as the algorithm change.
#
# `set -euo pipefail` makes a Python-side failure (syntax error,
# missing import) cause this step to fail instead of falling
# through to a no-diff "pass" when no fixture was actually
# regenerated.
- name: kv_events hash parity fixture (drift check)
working-directory: experimental/sgl-router
run: |
set -euo pipefail
python3 tests/scripts/generate_kv_events_hash_parity.py
if ! git diff --exit-code -- tests/fixtures/kv_events_hash_parity.json; then
echo "::error::kv_events hash parity fixture drifted." \
"Re-run experimental/sgl-router/tests/scripts/generate_kv_events_hash_parity.py" \
"and commit the updated tests/fixtures/kv_events_hash_parity.json."
exit 1
fi
- name: Show sccache stats
if: always() && steps.sccache.outcome == 'success'
run: sccache --show-stats
- name: Summary
if: always()
run: |
echo "## Tier-2 results" >> "$GITHUB_STEP_SUMMARY"
echo "- Build duration: \`${tier_2_build_duration_seconds:-(not recorded)}\` s" >> "$GITHUB_STEP_SUMMARY"
sgl-router-docker-build-test:
name: tier-3 — docker (placeholder)
needs: [sgl-router-gate, sgl-router-build-and-test]
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Placeholder
run: |
echo "docker-build-test not implemented yet."
exit 0
# tier-3 k8s integration is decoupled from tier-3 e2e: each runs on a
# different runner type (ubuntu kind cluster vs H100), each manages its
# own test scope (k8s_integration/ vs everything-else-in-e2e), and
# each is allowed to fail without blocking the other. They both still
# gate on tier-2's build+test passing — a Rust compile failure blocks
# the GPU/cluster runners from spinning up.
sgl-router-k8s-integration:
name: tier-3 — k8s integration
needs: [sgl-router-gate, sgl-router-build-and-test]
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Install kind
run: |
curl -Lo /tmp/kind https://kind.sigs.k8s.io/dl/v0.24.0/kind-linux-amd64
chmod +x /tmp/kind && sudo mv /tmp/kind /usr/local/bin/kind
- name: Install kubectl
run: |
curl -Lo /tmp/kubectl https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl
chmod +x /tmp/kubectl && sudo mv /tmp/kubectl /usr/local/bin/kubectl
- name: Build router image
run: docker build -t sgl-router:e2e -f experimental/sgl-router/tests/e2e/k8s_integration/Dockerfile.router .
- name: Build fake_worker image
run: |
docker build -t sgl-router-fake-worker:e2e \
-f experimental/sgl-router/tests/e2e/k8s_integration/Dockerfile.fake_worker \
experimental/sgl-router/tests/e2e/k8s_integration/
- name: Bootstrap kind + deploy
run: bash experimental/sgl-router/tests/e2e/k8s_integration/setup.sh
- name: Set up Python
run: |
python3 -m venv /tmp/e2e-venv
/tmp/e2e-venv/bin/pip install -r experimental/sgl-router/tests/e2e/k8s_integration/requirements.txt
- name: Run E2E
run: /tmp/e2e-venv/bin/pytest experimental/sgl-router/tests/e2e/k8s_integration/ -v --tb=short
- name: Dump cluster + router diagnostics on failure
if: failure()
run: |
set +e
kubectl -n sgl-router-test get pods -o wide
kubectl -n sgl-router-test describe pod -l app=sgl-router
kubectl -n sgl-router-test describe pod -l app=sglang
kubectl -n sgl-router-test logs deploy/sgl-router --tail=300
kubectl -n sgl-router-test logs deploy/sgl-router --tail=300 --previous
kubectl -n sgl-router-test get events --sort-by=.lastTimestamp
kubectl -n sgl-router-test get endpointslices -o wide
kubectl -n sgl-router-test get svc
true
sgl-router-e2e:
name: tier-3 — e2e
needs: [sgl-router-gate, sgl-router-build-and-test]
if: needs.sgl-router-gate.outputs.should_run == 'true'
runs-on: 2-gpu-h100
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: bash scripts/ci/utils/install_rust_protoc.sh
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: experimental/sgl-router
shared-key: "sgl-router-cache"
- name: cargo build (release)
working-directory: experimental/sgl-router
run: |
source "$HOME/.cargo/env"
cargo build --release
# Install SGLang from the local checkout in editable mode (no PyPI
# version pin) — mirrors `pr-test-rust.yml` so the router e2e runs
# against whatever SGLang version is on the branch. Replaces the
# previous `pip install sglang[srt]==X.Y.Z` pin, which forced the
# e2e to test against a stale wheel and would silently miss any
# new ServerArgs field (e.g. `disaggregation_mode` /
# `disaggregation_bootstrap_port`) added in the branch.
- name: Install SGLang dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Install e2e test dependencies
run: |
python3 -m pip install -r experimental/sgl-router/tests/e2e/requirements.txt
# IMPORTANT: --ignore=tests/e2e/k8s_integration. The k8s integration
# suite is owned by the `sgl-router-k8s-integration` job which has
# kind + kubectl installed and a real cluster running. The e2e GPU
# runner has neither, so pytest's recursive collection of
# tests/e2e/ would ERROR every k8s test at setup. Both tiers must
# be allowed to fail independently — k8s flakes (cluster boot,
# image pull) must not surface as e2e failures on H100, and e2e
# flakes (model load, HF auth) must not surface as k8s failures.
- name: Run e2e
working-directory: experimental/sgl-router
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
python3 -m pytest tests/e2e/ -v -s --tb=short \
--ignore=tests/e2e/k8s_integration
- name: Tokenizer parity matrix (uses HF cache populated by e2e)
working-directory: experimental/sgl-router
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
source "$HOME/.cargo/env"
cargo test --release --test component tokenizer::parity
sgl-router-finish:
name: finish
needs:
- sgl-router-gate
- sgl-router-lint
- sgl-router-build-and-test
- sgl-router-docker-build-test
- sgl-router-k8s-integration
- sgl-router-e2e
# `always()` lets `finish` run after upstream `skipped` outcomes
# (when the gate decided to skip). The downstream-skipped path is
# an explicit success — the workflow was correctly bypassed for a
# PR that didn't need full CI. The downstream-failure /
# downstream-cancelled path is a real fail. We also accept the
# gate's `skipped` (impossible today, kept for symmetry) and
# require the gate itself to have succeeded.
if: |
always() &&
needs.sgl-router-gate.result == 'success' &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled')
runs-on: ubuntu-latest
steps:
- name: All required checks completed
run: echo "OK"
+148
View File
@@ -0,0 +1,148 @@
name: PR Test (Xeon)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-xeon-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
main_package: ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests}}
env:
PYTHONDONTWRITEBYTECODE: 1
steps:
- name: clean up
run: |
sudo rm -rf ${{ github.workspace }} || true
sudo mkdir -p ${{ github.workspace }}
sudo chown -R $(id -u):$(id -g) ${{ github.workspace }}
- name: Checkout code
uses: actions/checkout@v4
with:
clean: false
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
# Run all tests for workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.ref
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_cpu.toml"
- "test/**/!(*.md)"
- "sgl-kernel/**/!(*.md|THIRDPARTYNOTICES.txt|LICENSE)"
- ".github/workflows/pr-test-xeon.yml"
- "docker/xeon.Dockerfile"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.main_package == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
build-test:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- runner: xeon-gnr
suite: base-b-test-cpu
partition_args: ""
steps:
- name: Cleanup workspace
run: |
sudo rm -rf ${{ github.workspace }} || true
sudo mkdir -p ${{ github.workspace }}
sudo chown $(id -u):$(id -g) ${{ github.workspace }}
- name: Checkout repository
uses: actions/checkout@v4
with:
clean: false
ref: ${{ inputs.ref || github.ref }}
- name: Build and Push
run: |
version=$(cat python/sglang/version.py | cut -d'"' -f2)
tag=v${version}-xeon
PR_REPO=${{ github.event.pull_request.head.repo.clone_url }}
PR_HEAD_REF=${{ github.head_ref }}
docker build \
${PR_REPO:+--build-arg SGLANG_REPO=$PR_REPO} \
${PR_HEAD_REF:+--build-arg VER_SGLANG=$PR_HEAD_REF} \
. -f docker/xeon.Dockerfile -t sglang_xeon --no-cache
- name: Run container
run: |
docker run -dt \
-v ${{ github.workspace }}:/sglang-checkout/ --ipc=host \
-v $HOME/.cache/huggingface:/root/.cache/huggingface \
-e HF_TOKEN="$(cat ~/huggingface_token.txt)" \
--name ci_sglang_${{ matrix.runner }} \
sglang_xeon
- name: Check AMX support
id: check_amx
timeout-minutes: 5
run: |
docker exec -w /sglang-checkout/ ci_sglang_${{ matrix.runner }} \
bash -c "source /opt/.venv/bin/activate && python3 -c 'import torch; import sgl_kernel; assert torch.cpu._is_amx_tile_supported(); assert hasattr(torch.ops.sgl_kernel, \"convert_weight_packed\"); '"
- name: Run unit tests
timeout-minutes: 120
run: |
docker exec -w /sglang-checkout/ ci_sglang_${{ matrix.runner }} \
bash -c "source /opt/.venv/bin/activate && cd ./test && python3 run_suite.py --hw cpu --suite ${{ matrix.suite }} ${{ matrix.partition_args }}"
- name: Change permission
timeout-minutes: 2
run: |
docker exec -u root ci_sglang_${{ matrix.runner }} bash -c "
rm -rf /tmp/ci-home &&
chown -R $(id -u):$(id -g) /sglang-checkout/ 2>/dev/null || true
"
- name: Cleanup container
if: always()
run: |
docker rm -f ci_sglang_${{ matrix.runner }} || true
+238
View File
@@ -0,0 +1,238 @@
name: PR Test (XPU)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
workflow_call:
inputs:
ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
concurrency:
group: pr-test-xpu-${{ inputs.ref || github.ref }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
jobs:
# ==================== Check Changes ==================== #
check-changes:
runs-on: ubuntu-latest
outputs:
main_package: ${{ steps.filter.outputs.main_package || steps.run-mode.outputs.run_all_tests }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Determine run mode
id: run-mode
run: |
# Run all tests for workflow_call (when ref input is provided)
# Note: github.event_name is inherited from caller, so we detect workflow_call by checking inputs.ref
if [[ "${{ inputs.run_all_tests }}" == "true" ]]; then
echo "run_all_tests=true" >> $GITHUB_OUTPUT
echo "Run mode: ALL TESTS (run_all_tests=${{ inputs.run_all_tests }})"
else
echo "run_all_tests=false" >> $GITHUB_OUTPUT
echo "Run mode: FILTERED (triggered by ${{ github.event_name }})"
fi
- name: Detect file changes
id: filter
uses: dorny/paths-filter@v3
if: steps.run-mode.outputs.run_all_tests != 'true'
with:
filters: |
main_package:
- "python/sglang/!(multimodal_gen)/**/!(*.md)"
- "python/pyproject_xpu.toml"
- "test/**/!(*.md)"
- "sgl-kernel/**/!(*.md|THIRDPARTYNOTICES.txt|LICENSE)"
- ".github/workflows/pr-test-xpu.yml"
- "docker/xpu.Dockerfile"
# ==================== PR Gate ==================== #
pr-gate:
needs: check-changes
if: needs.check-changes.outputs.main_package == 'true'
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
# ==================== Stage A ==================== #
stage-a-test-1-gpu-xpu:
needs: [check-changes, pr-gate]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: intel-bmg
env:
DOCKERHUB_INTEL_USERNAME: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
DOCKERHUB_INTEL_TOKEN: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
steps:
- name: Reset workspace ownership
run: |
# Prior runs install sglang as root inside the CI container against
# the bind-mounted workspace, leaving root-owned egg-info on the host.
# actions/checkout's pre-clean step can't unlink those as the runner
# user, so reset ownership before checkout runs.
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.ref }}
- name: Start CI container
run: |
export HF_TOKEN="$(cat ~/huggingface_token.txt)"
bash scripts/ci/xpu/xpu_ci_start_container.sh
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install Dependency
timeout-minutes: 60
run: |
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --upgrade pip
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install pytest expecttest ray huggingface_hub tabulate
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip uninstall -y flashinfer-python sgl-kernel sglang
docker exec ci_sglang_xpu cp /sglang-checkout/python/pyproject_xpu.toml /sglang-checkout/python/pyproject.toml
docker exec -w /sglang-checkout/python ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir . --extra-index-url https://download.pytorch.org/whl/xpu
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir --no-deps xgrammar==0.1.33
docker exec ci_sglang_xpu /bin/bash -c '/opt/venv/bin/hf auth login --token ${HF_TOKEN}'
- name: Run stage-a tests
timeout-minutes: 30
run: |
docker exec ci_sglang_xpu bash -c "source /opt/venv/bin/activate && cd /sglang-checkout/test && python3 run_suite.py --hw xpu --suite stage-a-test-1-gpu-xpu"
- name: Cleanup container
if: always()
run: |
# pip install ran as root inside the container against the
# bind-mounted workspace, so build artifacts are root-owned on
# the host. Chown them back before rm to avoid Permission denied.
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
# Wipe everything the run wrote into the workspace so the next
# job starts from a clean tree.
rm -rf \
python/build \
python/dist \
python/sglang.egg-info \
python/sglang/*.egg-info \
test/result.jsonl \
test/results \
test/.pytest_cache \
.pytest_cache || true
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
find . -type f -name "*.pyc" -delete || true
docker rm -f ci_sglang_xpu || true
if [[ -n "${CI_SGLANG_XPU_IMAGE:-}" ]]; then
docker rmi -f "${CI_SGLANG_XPU_IMAGE}" || true
fi
# ==================== Wait for Stage A ==================== #
wait-for-stage-a:
needs: [stage-a-test-1-gpu-xpu]
runs-on: ubuntu-latest
steps:
- run: echo "stage-a passed"
# ==================== Stage B ==================== #
stage-b-test-1-gpu-xpu:
needs: [check-changes, pr-gate, wait-for-stage-a]
if: needs.check-changes.outputs.main_package == 'true'
runs-on: intel-bmg
env:
DOCKERHUB_INTEL_USERNAME: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
DOCKERHUB_INTEL_TOKEN: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
steps:
- name: Reset workspace ownership
run: |
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.ref }}
- name: Start CI container
run: |
export HF_TOKEN="$(cat ~/huggingface_token.txt)"
bash scripts/ci/xpu/xpu_ci_start_container.sh
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Install Dependency
timeout-minutes: 60
run: |
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --upgrade pip
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install pytest expecttest ray huggingface_hub tabulate
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip uninstall -y flashinfer-python sgl-kernel sglang
docker exec ci_sglang_xpu cp /sglang-checkout/python/pyproject_xpu.toml /sglang-checkout/python/pyproject.toml
docker exec -w /sglang-checkout/python ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir . --extra-index-url https://download.pytorch.org/whl/xpu
docker exec ci_sglang_xpu /opt/venv/bin/python3 -m pip install --no-cache-dir --no-deps xgrammar==0.1.33
docker exec ci_sglang_xpu /bin/bash -c '/opt/venv/bin/hf auth login --token ${HF_TOKEN}'
- name: Run stage-b tests
timeout-minutes: 60
run: |
docker exec ci_sglang_xpu bash -c "source /opt/venv/bin/activate && cd /sglang-checkout/test && python3 run_suite.py --hw xpu --suite stage-b-test-1-gpu-xpu"
- name: Cleanup container
if: always()
run: |
# pip install ran as root inside the container against the
# bind-mounted workspace, so build artifacts are root-owned on
# the host. Chown them back before rm to avoid Permission denied.
docker run --rm -v "${{ github.workspace }}:/w" busybox:latest \
chown -R "$(id -u):$(id -g)" /w || true
# Wipe everything the run wrote into the workspace so the next
# job starts from a clean tree.
rm -rf \
python/build \
python/dist \
python/sglang.egg-info \
python/sglang/*.egg-info \
test/result.jsonl \
test/results \
test/.pytest_cache \
.pytest_cache || true
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
find . -type f -name "*.pyc" -delete || true
docker rm -f ci_sglang_xpu || true
if [[ -n "${CI_SGLANG_XPU_IMAGE:-}" ]]; then
docker rmi -f "${CI_SGLANG_XPU_IMAGE}" || true
fi
finish:
if: always()
needs: [stage-a-test-1-gpu-xpu, stage-b-test-1-gpu-xpu, pr-gate]
runs-on: ubuntu-latest
steps:
- name: Check job status
run: |
stage_a="${{ needs.stage-a-test-1-gpu-xpu.result }}"
stage_b="${{ needs.stage-b-test-1-gpu-xpu.result }}"
if [ "$stage_a" != "success" ] && [ "$stage_a" != "skipped" ]; then
echo "stage-a failed with result: $stage_a"
exit 1
fi
if [ "$stage_b" != "success" ] && [ "$stage_b" != "skipped" ]; then
echo "stage-b failed with result: $stage_b"
exit 1
fi
echo "All jobs completed successfully"
exit 0
+620
View File
@@ -0,0 +1,620 @@
name: PR Test Base
run-name: >-
${{
github.event_name == 'schedule' && 'Scheduled Full Run'
|| github.event_name == 'workflow_dispatch' && format('Manual run by {0}', github.actor)
|| github.event_name == 'pull_request' && format('PR #{0} - {1}', github.event.pull_request.number, github.event.pull_request.title)
|| 'PR Test Base'
}}
on:
schedule:
- cron: '0 11,23 * * *' # Run 2x daily, 12h apart, off PR-push peaks
pull_request:
workflow_dispatch:
inputs:
force_continue_on_error:
description: "Force continue-on-error (test scheduled CI behavior)"
required: false
type: boolean
default: false
test_parallel_dispatch:
description: "Test parallel dispatch behavior (simulates scheduled run)"
required: false
type: boolean
default: false
run_all_tests:
description: "Run all tests (mirrors the scheduled cron's behavior — combine with test_parallel_dispatch for full sim)"
required: false
type: boolean
default: false
workflow_call:
inputs:
git_ref:
description: 'Git ref (branch, tag, or SHA) to test. If not provided, uses the default branch.'
required: false
type: string
default: ''
run_all_tests:
description: "Run all tests (for releasing or testing purpose)"
required: false
type: boolean
default: false
skip_pr_test_health_check:
description: "Skip PR test health check fast-fail (e.g. for release branch cuts)"
required: false
type: boolean
default: false
concurrency:
# Concurrency group structure: pr-test-{event}-{branch}-{git_ref}
# - event_name prevents scheduled runs from colliding with fork PRs whose branch is named 'main'
# (without it, both resolve the branch segment to 'main' and block each other)
# - github.head_ref (pull_request) or github.ref_name (workflow_dispatch) normalizes to branch name
group: pr-test-${{ github.event_name }}-${{ github.head_ref || github.ref_name || 'default' }}-${{ inputs.git_ref || 'all' }}
cancel-in-progress: ${{ github.event_name != 'workflow_call' }}
env:
SGLANG_IS_IN_CI: true
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
SKIP_PR_TEST_HEALTH_CHECK: ${{ (inputs.skip_pr_test_health_check == true || inputs.test_parallel_dispatch == true || inputs.run_all_tests == true) && 'true' || 'false' }}
# Schedule / main-branch dispatch / workflow_call from main use refs/heads/main; PR events use refs/pull/*/merge
PR_TEST_BYPASS_MAINTENANCE_ON_MAIN: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
USE_VENV: false
permissions:
actions: write
contents: read
issues: read
pull-requests: read
jobs:
# =============================================== check changes ====================================================
check-changes:
uses: ./.github/workflows/_pr-test-check-changes.yml
with:
git_ref: ${{ inputs.git_ref || '' }}
run_all_tests: ${{ inputs.run_all_tests == true }}
force_continue_on_error: ${{ inputs.force_continue_on_error == true }}
secrets: inherit
call-pr-test-extra:
if: github.event_name == 'schedule' || inputs.test_parallel_dispatch == true || inputs.run_all_tests == true
uses: ./.github/workflows/pr-test-extra.yml
with:
git_ref: ${{ inputs.git_ref || '' }}
run_all_tests: true
secrets: inherit
# =============================================== Wait Jobs for Sequential PR Execution ====================================================
# These jobs poll GitHub API to wait for previous stages to complete.
# For PR runs: wait jobs run and enforce sequential execution via polling.
# For scheduled runs: wait jobs are skipped, enabling parallel execution for easier retry.
# For PRs with the `bypass-fastfail` label: wait jobs run but return success immediately
# (handled inside the wait-for-jobs action), so downstream stages dispatch in parallel.
wait-for-base-a:
needs: [check-changes, call-gate]
if: |
always() &&
!cancelled() &&
github.event_name == 'pull_request' &&
inputs.test_parallel_dispatch != true &&
(needs.check-changes.outputs.main_package == 'true' || needs.check-changes.outputs.sgl_kernel == 'true') &&
(needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped')
runs-on: ubuntu-latest
outputs:
base_a_result: ${{ steps.wait.outputs.result }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/check-maintenance
- uses: ./.github/actions/wait-for-jobs
id: wait
with:
stage-name: base-a
jobs: |
[
{"prefix": "base-a-test-1-gpu-small", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-1-gpu-small'].size }}},
{"prefix": "base-a-test-cpu", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-cpu'].size }}}
]
max-wait-minutes: '240'
wait-for-base-b:
needs: [check-changes, call-gate, wait-for-base-a]
if: |
always() &&
!cancelled() &&
github.event_name == 'pull_request' &&
inputs.test_parallel_dispatch != true &&
(needs.check-changes.outputs.main_package == 'true' || needs.check-changes.outputs.sgl_kernel == 'true') &&
(needs.wait-for-base-a.result == 'success' || needs.wait-for-base-a.result == 'skipped') &&
(needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped')
runs-on: ubuntu-latest
outputs:
base_b_result: ${{ steps.wait.outputs.result }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/check-maintenance
- uses: ./.github/actions/wait-for-jobs
id: wait
with:
stage-name: base-b
jobs: |
[
{"prefix": "base-b-test-1-gpu-small", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-b-test-1-gpu-small'].size }}},
{"prefix": "base-b-test-1-gpu-large", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-b-test-1-gpu-large'].size }}},
{"prefix": "base-b-test-2-gpu-large", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-b-test-2-gpu-large'].size }}},
{"prefix": "base-b-test-4-gpu-b200", "expected_count": ${{ fromJson(needs.check-changes.outputs.partitions)['base-b-test-4-gpu-b200'].size }}}
]
max-wait-minutes: '480'
# =============================================== PR Gate ====================================================
call-gate:
needs: check-changes
# Skip for scheduled runs (they run all tests)
if: |
github.event_name != 'schedule' &&
inputs.test_parallel_dispatch != true &&
(
needs.check-changes.outputs.main_package == 'true' ||
needs.check-changes.outputs.sgl_kernel == 'true' ||
needs.check-changes.outputs.jit_kernel == 'true' ||
needs.check-changes.outputs.multimodal_gen == 'true'
)
uses: ./.github/workflows/pr-gate.yml
secrets: inherit
# =============================================== sgl-kernel ====================================================
sgl-kernel-build-wheels:
needs: [check-changes, call-gate]
# Skip for scheduled runs (they run stages independently).
if: |
github.event_name != 'schedule' &&
inputs.test_parallel_dispatch != true &&
needs.check-changes.result == 'success' &&
needs.check-changes.outputs.sgl_kernel == 'true' &&
needs.call-gate.result == 'success'
uses: ./.github/workflows/_pr-test-sgl-kernel-build.yml
with:
runs_on: x64-kernel-build-node
job_display_name: Build Wheel
git_ref: ${{ inputs.git_ref || '' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
sgl-kernel-build-wheels-arm:
needs: [check-changes, call-gate]
# Skip for scheduled runs (they run stages independently).
if: |
github.event_name != 'schedule' &&
inputs.test_parallel_dispatch != true &&
needs.check-changes.result == 'success' &&
needs.check-changes.outputs.sgl_kernel == 'true' &&
needs.call-gate.result == 'success'
uses: ./.github/workflows/_pr-test-sgl-kernel-build.yml
with:
runs_on: arm-kernel-build-node
job_display_name: Build Wheel Arm
arch_suffix: '-aarch64'
git_ref: ${{ inputs.git_ref || '' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
call-sgl-kernel-tests:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: |
github.event_name != 'schedule' &&
inputs.test_parallel_dispatch != true &&
needs.check-changes.outputs.sgl_kernel == 'true'
uses: ./.github/workflows/pr-test-sgl-kernel.yml
with:
runner_config: 4-gpu-b200
runs_on_map: ${{ needs.check-changes.outputs.runs_on_map }}
sgl_kernel: ${{ needs.check-changes.outputs.sgl_kernel }}
git_ref: ${{ inputs.git_ref || '' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
# =============================================== jit-kernel ====================================================
call-jit-kernel-tests:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
# Run on scheduled/parallel-dispatch runs (same pattern as the base-* stages) so the
# jit_kernel suite is exercised on main 3x daily, not only on PRs that touch jit_kernel/**.
# check-changes already forces jit_kernel='true' on scheduled runs (run_all_tests).
if: |
always() &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == true) || (!failure() && !cancelled())) &&
needs.check-changes.outputs.jit_kernel == 'true'
uses: ./.github/workflows/pr-test-jit-kernel.yml
with:
runner_config: 4-gpu-b200
runs_on_map: ${{ needs.check-changes.outputs.runs_on_map }}
jit_kernel: ${{ needs.check-changes.outputs.jit_kernel }}
# On scheduled/parallel-dispatch runs sgl-kernel-build-wheels is skipped, so the wheel
# artifact does not exist. Force sgl_kernel='false' there so the jit-kernel jobs skip the
# wheel download and run against the released/pinned sgl-kernel instead of a fresh build.
sgl_kernel: ${{ (github.event_name == 'schedule' || inputs.test_parallel_dispatch == true) && 'false' || needs.check-changes.outputs.sgl_kernel }}
git_ref: ${{ inputs.git_ref || '' }}
test_parallel_dispatch: ${{ inputs.test_parallel_dispatch == true && 'true' || 'false' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
# =============================================== primary ====================================================
# Runs on 5090 (32GB, SM120)
base-a-test-1-gpu-small:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-a-test-1-gpu-small
runner_config: 1-gpu-small
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '10'
secrets: inherit
base-a-test-cpu:
needs: [check-changes, call-gate]
if: |
always() &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == true) || (!failure() && !cancelled())) &&
(needs.check-changes.outputs.main_package == 'true')
runs-on: ubuntu-latest
timeout-minutes: 240
env:
HF_HOME: ${{ github.workspace }}/.hf-cache
strategy:
fail-fast: false
max-parallel: ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-cpu'].max_parallel }}
matrix:
partition: ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-cpu'].arr }}
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
df -h
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.git_ref || github.sha }}
- uses: ./.github/actions/check-pr-test-health
- uses: ./.github/actions/check-maintenance
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install uv
uses: astral-sh/setup-uv@v5
# Needed by setuptools-rust to build the bundled native gRPC extension
# (rust/sglang-grpc) when installing the main `sglang` wheel from source.
- name: Install protoc + Rust toolchain
timeout-minutes: 10
run: bash scripts/ci/utils/install_rust_protoc.sh
- name: Rust cache (sglang-grpc)
uses: Swatinem/rust-cache@v2
with:
workspaces: rust/sglang-grpc
shared-key: "sglang-grpc-cpu"
save-if: ${{ matrix.partition == 0 }}
# uv pip targets a venv by default; setup-python has no venv — install into that interpreter (see UV_SYSTEM_PYTHON in https://docs.astral.sh/uv/guides/integration/github/)
- name: Install dependencies
timeout-minutes: 20
env:
UV_SYSTEM_PYTHON: "1"
run: |
uv pip install -e "python[dev]" --index-strategy unsafe-best-match --prerelease allow
# Hosted runners are ephemeral, so models are re-fetched every run and the
# Hub occasionally returns 429s. Persist the HF cache in GitHub's cache
# storage (rolling key + restore-keys) so warm runs never hit the network.
- name: Cache HF hub
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.hf-cache
key: hf-cpu-${{ matrix.partition }}-${{ github.run_id }}
restore-keys: hf-cpu-${{ matrix.partition }}-
- name: Run test
timeout-minutes: 10
env:
CONTINUE_ON_ERROR_FLAG: ${{ needs.check-changes.outputs.continue_on_error == 'true' && '--continue-on-error' || '' }}
run: |
cd test/
python3 run_suite.py --hw cpu --suite base-a-test-cpu --auto-partition-id ${{ matrix.partition }} --auto-partition-size ${{ fromJson(needs.check-changes.outputs.partitions)['base-a-test-cpu'].size }} $CONTINUE_ON_ERROR_FLAG
# Runs on 5090 (32GB, SM120)
base-b-test-1-gpu-small:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-b-test-1-gpu-small
runner_config: 1-gpu-small
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
secrets: inherit
# Runs on H100 (80GB, SM90) - tests that don't pass on 5090 (FA3, FP8, high VRAM, etc.)
base-b-test-1-gpu-large:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-b-test-1-gpu-large
runner_config: 1-gpu-large
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
base-b-test-2-gpu-large:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-b-test-2-gpu-large
runner_config: 2-gpu-large
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
secrets: inherit
base-b-test-4-gpu-b200:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-b-test-4-gpu-b200
runner_config: 4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '40'
secrets: inherit
call-multimodal-gen-tests:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
if: |
always() &&
!cancelled() &&
((github.event_name == 'schedule' || inputs.test_parallel_dispatch == true) || (!failure() && !cancelled())) &&
needs.check-changes.outputs.multimodal_gen == 'true'
uses: ./.github/workflows/pr-test-multimodal-gen.yml
with:
runner_config: 4-gpu-b200
runs_on_map: ${{ needs.check-changes.outputs.runs_on_map }}
multimodal_gen: ${{ needs.check-changes.outputs.multimodal_gen }}
sgl_kernel: ${{ needs.check-changes.outputs.sgl_kernel }}
continue_on_error: ${{ needs.check-changes.outputs.continue_on_error }}
git_ref: ${{ inputs.git_ref || '' }}
test_parallel_dispatch: ${{ inputs.test_parallel_dispatch == true && 'true' || 'false' }}
caller_needs_failure: ${{ (needs.call-gate.result == 'failure' || needs.sgl-kernel-build-wheels.result == 'failure' || needs.check-changes.result == 'failure') && 'true' || 'false' }}
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true && 'true' || 'false' }}
secrets: inherit
base-c-test-4-gpu-h100:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-4-gpu-h100
runner_config: 4-gpu-h100
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
secrets: inherit
base-c-test-8-gpu-h200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-8-gpu-h200
runner_config: 8-gpu-h200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
# Per-model TP must match the test's launch in test/registered/ -- see
# FALLBACK_ARGS in scripts/ci/cuda/warmup_deep_gemm.py for extra dp/ep
# flags. Only models that actually invoke DeepGEMM kernels at runtime
# are listed. Cold-cache ~13 min; warm-cache <=30 s via marker file.
# Server CUDA Graph warmup is combined into this step (warmup_server_models unset).
warmup_deep_gemm_models: 'deepseek-ai/DeepSeek-V3-0324:8 deepseek-ai/DeepSeek-V3.2:8 zai-org/GLM-5-FP8:8 XiaomiMiMo/MiMo-V2-Flash:4 XiaomiMiMo/MiMo-V2.5:8'
warmup_timeout_minutes: '60'
secrets: inherit
base-c-test-8-gpu-h20:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-8-gpu-h20
runner_config: 8-gpu-h20
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
secrets: inherit
base-c-test-deepep-4-gpu-h100:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-deepep-4-gpu-h100
runner_config: deepep-4-gpu-h100
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
warmup_deep_gemm_models: 'lmsys/sglang-ci-dsv3-test:4'
warmup_server_models: 'lmsys/sglang-ci-dsv3-test:4'
secrets: inherit
base-c-test-deepep-4-gpu-b200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-deepep-4-gpu-b200
runner_config: deepep-4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
base-c-test-deepep-8-gpu-h200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-deepep-8-gpu-h200
runner_config: deepep-8-gpu-h200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
base-c-test-4-gpu-b200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-4-gpu-b200
runner_config: 4-gpu-b200
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
base-c-test-4-gpu-gb300:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
self_name: base-c-test-4-gpu-gb300
runner_config: 4-gpu-gb300
check_changes: ${{ toJson(needs.check-changes.outputs) }}
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
secrets: inherit
pr-test-finish:
needs:
[
call-gate,
check-changes,
sgl-kernel-build-wheels,
sgl-kernel-build-wheels-arm,
call-sgl-kernel-tests,
wait-for-base-a,
wait-for-base-b,
call-jit-kernel-tests,
call-multimodal-gen-tests,
base-a-test-1-gpu-small,
base-a-test-cpu,
base-b-test-1-gpu-small,
base-b-test-1-gpu-large,
base-b-test-2-gpu-large,
base-b-test-4-gpu-b200,
base-c-test-4-gpu-h100,
base-c-test-8-gpu-h20,
base-c-test-8-gpu-h200,
base-c-test-deepep-4-gpu-h100,
base-c-test-deepep-4-gpu-b200,
base-c-test-deepep-8-gpu-h200,
base-c-test-4-gpu-b200,
base-c-test-4-gpu-gb300,
]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check all dependent job statuses
run: |
# Convert the 'needs' context to a JSON string
json_needs='${{ toJson(needs) }}'
# Get a list of all job names from the JSON keys
job_names=$(echo "$json_needs" | jq -r 'keys_unsorted[]')
for job in $job_names; do
# For each job, extract its result
result=$(echo "$json_needs" | jq -r --arg j "$job" '.[$j].result')
# Print the job name and its result
echo "$job: $result"
# Check for failure or cancellation and exit if found
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "The above jobs failed."
exit 1
fi
done
# If the loop completes, all jobs were successful
echo "All jobs completed successfully"
exit 0
# =============================================== notify pr-states ====================================================
# Dispatches pr-states.yml after every attempt (initial + every rerun).
# workflow_run.completed only fires on the initial completion (one
# workflow_run record across attempts), so we explicitly dispatch —
# workflow_dispatch is the only event GITHUB_TOKEN can cascade-create.
#
# Fork PRs are excluded: their pull_request GITHUB_TOKEN is forced
# read-only regardless of declared `actions: write`, so dispatch 403s.
# Fork PRs get pr-states updates via the workflow_run subscription
# there (initial completion) and pull_request_target (push / label).
notify-pr-states:
needs: [pr-test-finish]
if: |
always() &&
github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Dispatch pr-states refresh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh workflow run pr-states.yml \
--repo ${{ github.repository }} \
--ref main \
-f pr_number="$PR_NUMBER"
+233
View File
@@ -0,0 +1,233 @@
name: Release Branch Cut
on:
workflow_dispatch:
inputs:
branch_name:
description: 'Branch name to create (e.g., release/v0.5.7)'
required: true
type: string
commit_sha:
description: 'Commit SHA from main to cut the release branch from (defaults to latest main)'
required: false
type: string
default: ''
permissions:
actions: write
contents: write
issues: read
pull-requests: read
jobs:
cut-release-branch:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
environment: 'prod'
outputs:
branch_name: ${{ steps.set_output.outputs.branch_name }}
branch_exists: ${{ steps.check_exists.outputs.branch_exists }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Validate branch name
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
if [ -z "$BRANCH_NAME" ]; then
echo "::error::Branch name is required"
exit 1
fi
# Validate branch name format (should start with release/)
if [[ ! "$BRANCH_NAME" =~ ^release/ ]]; then
echo "::warning::Branch name '$BRANCH_NAME' does not follow convention 'release/vX.Y.Z'"
fi
echo "Branch name: $BRANCH_NAME"
- name: Validate commit SHA
id: validate
run: |
COMMIT_SHA="${{ github.event.inputs.commit_sha }}"
# If no commit SHA provided, use latest main
if [ -z "$COMMIT_SHA" ]; then
COMMIT_SHA=$(git rev-parse HEAD)
echo "No commit SHA provided, using latest main: $COMMIT_SHA"
fi
# Verify the commit exists and is on main
if ! git cat-file -t "$COMMIT_SHA" > /dev/null 2>&1; then
echo "::error::Commit SHA '$COMMIT_SHA' does not exist"
exit 1
fi
# Check if commit is an ancestor of main (i.e., is on main branch)
if ! git merge-base --is-ancestor "$COMMIT_SHA" main; then
echo "::error::Commit SHA '$COMMIT_SHA' is not on the main branch"
exit 1
fi
echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "Validated commit SHA: $COMMIT_SHA"
- name: Check if branch already exists
id: check_exists
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then
echo "Branch '$BRANCH_NAME' already exists, skipping creation and proceeding to downstream tests"
echo "branch_exists=true" >> $GITHUB_OUTPUT
else
echo "Branch '$BRANCH_NAME' does not exist, proceeding with creation"
echo "branch_exists=false" >> $GITHUB_OUTPUT
fi
- name: Create release branch
if: steps.check_exists.outputs.branch_exists != 'true'
run: |
COMMIT_SHA="${{ steps.validate.outputs.COMMIT_SHA }}"
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
# Create branch from the specified commit
git checkout -b "$BRANCH_NAME" "$COMMIT_SHA"
echo "Successfully created branch '$BRANCH_NAME' from commit '$COMMIT_SHA'"
- name: Update version references in documentation
if: steps.check_exists.outputs.branch_exists != 'true'
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
# Extract version from branch name (e.g., release/v0.5.8 -> v0.5.8)
VERSION=$(echo "$BRANCH_NAME" | sed 's/release\///')
# Update git clone version references in docs
sed -i "s/git clone -b v[0-9]\+\.[0-9]\+\.[0-9]\+\.\?post\?[0-9]*/git clone -b $VERSION/" docs/get_started/install.md
sed -i "s/git clone -b v[0-9]\+\.[0-9]\+\.[0-9]\+\.\?post\?[0-9]*/git clone -b $VERSION/" docs/platforms/amd_gpu.md
# Check if any changes were made
if git diff --quiet; then
echo "No version references needed updating"
else
git add docs/get_started/install.md docs/platforms/amd_gpu.md
git commit -m "docs: update version references to $VERSION"
echo "Updated version references to $VERSION"
fi
- name: Push release branch
if: steps.check_exists.outputs.branch_exists != 'true'
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
git push origin "$BRANCH_NAME"
echo "Successfully pushed branch '$BRANCH_NAME'"
- name: Emit branch_name output
id: set_output
run: |
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Summary
run: |
COMMIT_SHA="${{ steps.validate.outputs.COMMIT_SHA }}"
BRANCH_NAME="${{ github.event.inputs.branch_name }}"
BRANCH_EXISTS="${{ steps.check_exists.outputs.branch_exists }}"
if [ "$BRANCH_EXISTS" = "true" ]; then
STATUS="Branch already existed — creation skipped, downstream tests will run"
else
STATUS="Newly created"
fi
echo "## Release Branch Cut Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`$BRANCH_NAME\` |" >> $GITHUB_STEP_SUMMARY
echo "| Commit | \`$COMMIT_SHA\` |" >> $GITHUB_STEP_SUMMARY
echo "| Status | $STATUS |" >> $GITHUB_STEP_SUMMARY
echo "| Triggered by | @${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Next Steps" >> $GITHUB_STEP_SUMMARY
echo "1. Tests are automatically triggered on the release branch" >> $GITHUB_STEP_SUMMARY
echo "2. Apply any hotfixes if needed" >> $GITHUB_STEP_SUMMARY
echo "3. Create a tag to trigger release: \`gh workflow run release-tag.yml -f version=X.Y.Z -f ref=$BRANCH_NAME\`" >> $GITHUB_STEP_SUMMARY
run-pr-tests-nvidia:
needs: cut-release-branch
uses: ./.github/workflows/pr-test.yml
with:
git_ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
skip_pr_test_health_check: true
secrets: inherit
run-pr-tests-amd:
needs: cut-release-branch
uses: ./.github/workflows/pr-test-amd.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
secrets: inherit
run-pr-test-npu:
needs: cut-release-branch
uses: ./.github/workflows/pr-test-npu.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
secrets: inherit
run-pr-tests-xeon:
needs: cut-release-branch
uses: ./.github/workflows/pr-test-xeon.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
secrets: inherit
run-pr-tests-xpu:
needs: cut-release-branch
uses: ./.github/workflows/pr-test-xpu.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
run_all_tests: true
secrets: inherit
run-nightly-tests-nvidia:
needs: cut-release-branch
uses: ./.github/workflows/nightly-test-nvidia.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
secrets: inherit
run-nightly-tests-amd:
needs: cut-release-branch
uses: ./.github/workflows/nightly-test-amd.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
secrets: inherit
run-nightly-tests-npu:
needs: cut-release-branch
uses: ./.github/workflows/nightly-test-npu.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
secrets: inherit
run-nightly-tests-intel:
needs: cut-release-branch
uses: ./.github/workflows/nightly-test-intel.yml
with:
ref: ${{ needs.cut-release-branch.outputs.branch_name }}
secrets: inherit
@@ -0,0 +1,63 @@
name: Release Docker Images Nightly Miles ROCm7.0 (AMD)
on:
workflow_dispatch:
schedule:
- cron: "0 12 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: amd-docker-scale
environment: "prod"
strategy:
fail-fast: false
matrix:
include:
- gpu_arch: gfx950
rocm_tag: rocm700-mi35x
sglang_image_repo: rocm/sgl-dev
sglang_image_tag: v0.5.14-rocm700-mi35x-20260627
- gpu_arch: gfx942
rocm_tag: rocm700-mi30x
sglang_image_tag: v0.5.10-rocm700-mi30x
steps:
- name: Checkout miles repository
uses: actions/checkout@v4
with:
repository: radixark/miles
ref: main
fetch-depth: 0
- name: Set date
run: echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
- name: Record miles commit
id: miles
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Build and push miles image to rocm/sgl-dev
run: |
base="miles-${{ matrix.rocm_tag }}"
dated="${base}-${{ env.DATE }}"
echo "IMAGE_TAG=${dated}" >> $GITHUB_ENV
echo "Building rocm/sgl-dev:${dated} from miles main @ ${{ steps.miles.outputs.sha }}"
docker build . \
-f docker/Dockerfile.rocm \
--build-arg GPU_ARCH=${{ matrix.gpu_arch }} \
--build-arg SGLANG_IMAGE_REPO=${{ matrix.sglang_image_repo || 'lmsysorg/sglang' }} \
--build-arg SGLANG_IMAGE_TAG=${{ matrix.sglang_image_tag }} \
--build-arg MILES_COMMIT=${{ steps.miles.outputs.sha }} \
-t rocm/sgl-dev:${dated} \
--no-cache
docker push rocm/sgl-dev:${dated}
@@ -0,0 +1,58 @@
name: Release Docker Images Nightly Miles ROCm7.2 (AMD)
on:
workflow_dispatch:
schedule:
- cron: "0 12 * * *"
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: amd-docker-scale
environment: "prod"
strategy:
fail-fast: false
matrix:
include:
- gpu_arch: gfx950
rocm_tag: rocm720-mi35x
sglang_image_tag: v0.5.10-rocm720-mi35x
steps:
- name: Checkout miles repository
uses: actions/checkout@v4
with:
repository: radixark/miles
ref: main
fetch-depth: 0
- name: Set date
run: echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
- name: Record miles commit
id: miles
run: echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Build and push miles image to rocm/sgl-dev
run: |
base="miles-${{ matrix.rocm_tag }}"
dated="${base}-${{ env.DATE }}"
echo "IMAGE_TAG=${dated}" >> $GITHUB_ENV
echo "Building rocm/sgl-dev:${dated} from miles main @ ${{ steps.miles.outputs.sha }}"
docker build . \
-f docker/Dockerfile.rocm \
--build-arg GPU_ARCH=${{ matrix.gpu_arch }} \
--build-arg SGLANG_IMAGE_TAG=${{ matrix.sglang_image_tag }} \
--build-arg MILES_COMMIT=${{ steps.miles.outputs.sha }} \
-t rocm/sgl-dev:${dated} \
--no-cache
docker push rocm/sgl-dev:${dated}
@@ -0,0 +1,252 @@
name: Release Docker Images Nightly ROCm7.0 (AMD)
on:
workflow_dispatch:
schedule:
- cron: '0 12 * * *'
concurrency:
# A PR number if a pull request and otherwise the commit hash. This cancels
# queued and in-progress runs for the same PR (presubmit) or commit
# (postsubmit). The workflow name is prepended to avoid conflicts between
# different workflows.
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: amd-docker-scale
environment: 'prod'
strategy:
fail-fast: false
matrix:
gpu_arch: ['gfx942', 'gfx950']
build_type: ['all']
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git describe to find tags
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: "Set Date"
run: |
echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
- name: Get version from latest tag
id: version
run: |
# Use the shared helper so stable/post releases sort above rc tags.
VERSION=$(python3 python/tools/get_version_tag.py --tag-only | sed 's/^v//')
if [ -z "$VERSION" ]; then
echo "::error::Could not determine version from git tags"
exit 1
fi
# Get short commit hash of current HEAD
COMMIT_HASH=$(git rev-parse --short HEAD)
# Compose pretend version for setuptools_scm: e.g., 0.5.8.dev20260129+g1a2b3c4
PRETEND_VERSION="${VERSION}.dev${{ env.DATE }}+g${COMMIT_HASH}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "pretend_version=${PRETEND_VERSION}" >> $GITHUB_OUTPUT
echo "Detected version: ${VERSION}"
echo "Pretend version for pip: ${PRETEND_VERSION}"
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Build and Push to rocm/sgl-dev
run: |
version=${{ steps.version.outputs.version }}
pretend_version=${{ steps.version.outputs.pretend_version }}
echo "Version: ${version}"
echo "Pretend version: ${pretend_version}"
if [ "${{ matrix.gpu_arch }}" = "gfx942" ]; then
rocm_tag="rocm700-mi30x"
elif [ "${{ matrix.gpu_arch }}" = "gfx950" ]; then
rocm_tag="rocm700-mi35x"
else
echo "Unsupported gfx arch"
exit 1
fi
tag=v${version}-${rocm_tag}
echo "IMAGE_TAG=${tag}-${{ env.DATE }}" >> $GITHUB_ENV
# remove --build-arg NIC_BACKEND=ainic for auto detection nic support in mori
# UBUNTU_MIRROR forces apt over HTTPS to dodge port-80 reachability flakes
# to Canonical's archive.ubuntu.com mirror IPs from the amd-docker-scale runner.
docker build . -f docker/rocm.Dockerfile --build-arg SGL_BRANCH=${{ github.ref_name }} --build-arg BUILD_TYPE=${{ matrix.build_type }} --build-arg GPU_ARCH=${{ matrix.gpu_arch }} --build-arg ENABLE_MORI=1 --build-arg ENABLE_NIXL=1 --build-arg SETUPTOOLS_SCM_PRETEND_VERSION=${pretend_version} --build-arg UBUNTU_MIRROR=https://archive.ubuntu.com -t rocm/sgl-dev:${tag}-${{ env.DATE }} --no-cache
docker push rocm/sgl-dev:${tag}-${{ env.DATE }}
# Persist the tag right after rocm/sgl-dev push succeeds so the local
# registry mirror can run even if a later step in this job (lmsys push)
# fails. By default this step only runs when the previous step succeeded,
# so the artifact only exists when an image actually landed on Docker Hub.
- name: Save published image tag
run: |
mkdir -p image-tag
echo "${{ env.IMAGE_TAG }}" > "image-tag/${{ matrix.gpu_arch }}.txt"
- name: Upload image tag artifact
uses: actions/upload-artifact@v4
with:
name: image-tag-${{ matrix.gpu_arch }}
path: image-tag/${{ matrix.gpu_arch }}.txt
retention-days: 1
- name: Login to Docker Hub (lmsys)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to lmsysorg/sglang-rocm
run: |
docker tag rocm/sgl-dev:${{ env.IMAGE_TAG }} lmsysorg/sglang-rocm:${{ env.IMAGE_TAG }}
docker push lmsysorg/sglang-rocm:${{ env.IMAGE_TAG }}
# Mirror the freshly published rocm/sgl-dev image to the in-network Docker
# registry so AMD CI runners can pull without hitting Docker Hub rate limits.
# The tag is read verbatim from the publish job's artifact so this job uses
# exactly the same tag that publish pushed (only the registry prefix differs).
# `!cancelled()` lets us still mirror successful matrix legs when other legs
# of publish failed; legs without an artifact will fail at download and be
# the only ones marked red.
push_local_registry:
if: ${{ !cancelled() && github.repository == 'sgl-project/sglang' }}
runs-on: linux-mi300-1gpu-sglang
environment: 'prod'
needs: publish
strategy:
fail-fast: false
matrix:
gpu_arch: ['gfx942', 'gfx950']
steps:
- name: Download image tag artifact
uses: actions/download-artifact@v4
with:
name: image-tag-${{ matrix.gpu_arch }}
- name: Read image tag
run: |
image_tag=$(tr -d '[:space:]' < "${{ matrix.gpu_arch }}.txt")
if [ -z "${image_tag}" ]; then
echo "::error::Image tag artifact is empty"
exit 1
fi
echo "IMAGE_TAG=${image_tag}" >> $GITHUB_ENV
echo "Resolved IMAGE_TAG=${image_tag}"
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Mirror rocm/sgl-dev to local registry
run: |
src="rocm/sgl-dev:${{ env.IMAGE_TAG }}"
dst="10.44.14.109:5000/rocm/sgl-dev:${{ env.IMAGE_TAG }}"
docker pull "${src}"
docker tag "${src}" "${dst}"
docker push "${dst}"
# Temporarily disable docker cache seeding until performant storage is in place
# cache:
# if: false
# # if: always() && github.repository == 'sgl-project/sglang'
# runs-on: linux-mi300-gpu-1
# environment: 'prod'
# needs: publish
# strategy:
# fail-fast: false
# matrix:
# gpu_arch: ['gfx942']
# build_type: ['all']
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
# with:
# fetch-depth: 0 # Required for git describe to find tags
# - name: "Set Date"
# run: |
# echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
# - name: Get version from latest tag
# id: version
# run: |
# # Use the shared helper so stable/post releases sort above rc tags.
# VERSION=$(python3 python/tools/get_version_tag.py --tag-only | sed 's/^v//')
# if [ -z "$VERSION" ]; then
# echo "::error::Could not determine version from git tags"
# exit 1
# fi
# echo "version=${VERSION}" >> $GITHUB_OUTPUT
# echo "Detected version: ${VERSION}"
# - name: Login to Docker Hub
# uses: docker/login-action@v2
# with:
# username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
# password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
# - name: Pull and Save Docker Image to Cache
# run: |
# set -euxo pipefail
# version=${{ steps.version.outputs.version }}
# echo "Version: ${version}"
# if [ "${{ matrix.gpu_arch }}" = "gfx942" ]; then
# rocm_tag="rocm700-mi30x"
# else
# echo "Unsupported gfx arch"
# exit 1
# fi
# tag=v${version}-${rocm_tag}
# if [ "${{ matrix.build_type }}" = "all" ]; then
# tag_suffix=""
# else
# echo "Unsupported build type"
# exit 1
# fi
# image="rocm/sgl-dev:${tag}-${{ env.DATE }}${tag_suffix}"
# # Determine target cache file name based on ROCm variant
# if [[ "${rocm_tag}" == rocm700* ]]; then
# final_path="/home/runner/sgl-data/docker/image-700.tar"
# else
# echo "Unexpected ROCm tag: ${rocm_tag}"
# exit 1
# fi
# tmp_path="${final_path}.tmp"
# echo "Pulling image: ${image}"
# docker pull "${image}"
# echo "Saving to temp file: ${tmp_path}"
# docker save "${image}" -o "${tmp_path}"
# echo "Moving to final path: ${final_path}"
# mv -f "${tmp_path}" "${final_path}"
# echo "Cache populated successfully at ${final_path}"
@@ -0,0 +1,173 @@
name: Release Docker Images Nightly ROCm7.2 (AMD)
on:
workflow_dispatch:
inputs:
job_select:
description: 'Select which release job to run'
required: false
type: choice
default: 'all'
options:
- 'all'
- publish
schedule:
- cron: '0 12 * * *'
concurrency:
# A PR number if a pull request and otherwise the commit hash. This cancels
# queued and in-progress runs for the same PR (presubmit) or commit
# (postsubmit). The workflow name is prepended to avoid conflicts between
# different workflows.
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: True
jobs:
publish:
if: github.repository == 'sgl-project/sglang' && (github.event_name != 'workflow_dispatch' || inputs.job_select == 'all' || inputs.job_select == 'publish')
runs-on: amd-docker-scale
environment: 'prod'
strategy:
fail-fast: false
matrix:
gpu_arch: ['gfx942-rocm720', 'gfx950-rocm720']
build_type: ['all']
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git describe to find tags
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: "Set Date"
run: |
echo "DATE=$(date +%Y%m%d)" >> $GITHUB_ENV
- name: Get version from latest tag
id: version
run: |
# Use the shared helper so stable/post releases sort above rc tags.
VERSION=$(python3 python/tools/get_version_tag.py --tag-only | sed 's/^v//')
if [ -z "$VERSION" ]; then
echo "::error::Could not determine version from git tags"
exit 1
fi
# Get short commit hash of current HEAD
COMMIT_HASH=$(git rev-parse --short HEAD)
# Compose pretend version for setuptools_scm: e.g., 0.5.8.post1.dev20260211+g1a2b3c4
PRETEND_VERSION="${VERSION}.dev${{ env.DATE }}+g${COMMIT_HASH}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "pretend_version=${PRETEND_VERSION}" >> $GITHUB_OUTPUT
echo "Detected version: ${VERSION}"
echo "Pretend version for pip: ${PRETEND_VERSION}"
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Build and Push to rocm/sgl-dev
run: |
version=${{ steps.version.outputs.version }}
pretend_version=${{ steps.version.outputs.pretend_version }}
echo "Version: ${version}"
echo "Pretend version: ${pretend_version}"
if [ "${{ matrix.gpu_arch }}" = "gfx942-rocm720" ]; then
rocm_tag="rocm720-mi30x"
elif [ "${{ matrix.gpu_arch }}" = "gfx950-rocm720" ]; then
rocm_tag="rocm720-mi35x"
else
echo "Unsupported gfx arch"
exit 1
fi
tag=v${version}-${rocm_tag}
echo "IMAGE_TAG=${tag}-${{ env.DATE }}" >> $GITHUB_ENV
# remove --build-arg NIC_BACKEND=ainic for auto detection nic support in mori
# UBUNTU_MIRROR forces apt over HTTPS to dodge port-80 reachability flakes
# to Canonical's archive.ubuntu.com mirror IPs from the amd-docker-scale runner.
docker build . -f docker/rocm.Dockerfile --build-arg SGL_BRANCH=${{ github.ref_name }} --build-arg BUILD_TYPE=${{ matrix.build_type }} --build-arg GPU_ARCH=${{ matrix.gpu_arch }} --build-arg ENABLE_MORI=1 --build-arg ENABLE_NIXL=1 --build-arg SETUPTOOLS_SCM_PRETEND_VERSION=${pretend_version} --build-arg UBUNTU_MIRROR=https://archive.ubuntu.com -t rocm/sgl-dev:${tag}-${{ env.DATE }} --no-cache
docker push rocm/sgl-dev:${tag}-${{ env.DATE }}
# Persist the tag right after rocm/sgl-dev push succeeds so the local
# registry mirror can run even if a later step in this job (lmsys push)
# fails. By default this step only runs when the previous step succeeded,
# so the artifact only exists when an image actually landed on Docker Hub.
- name: Save published image tag
run: |
mkdir -p image-tag
echo "${{ env.IMAGE_TAG }}" > "image-tag/${{ matrix.gpu_arch }}.txt"
- name: Upload image tag artifact
uses: actions/upload-artifact@v4
with:
name: image-tag-${{ matrix.gpu_arch }}
path: image-tag/${{ matrix.gpu_arch }}.txt
retention-days: 1
- name: Login to Docker Hub (lmsys)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push to lmsysorg/sglang-rocm
run: |
docker tag rocm/sgl-dev:${{ env.IMAGE_TAG }} lmsysorg/sglang-rocm:${{ env.IMAGE_TAG }}
docker push lmsysorg/sglang-rocm:${{ env.IMAGE_TAG }}
# Mirror the freshly published rocm/sgl-dev image to the in-network Docker
# registry so AMD CI runners can pull without hitting Docker Hub rate limits.
# The tag is read verbatim from the publish job's artifact so this job uses
# exactly the same tag that publish pushed (only the registry prefix differs).
# `!cancelled()` lets us still mirror successful matrix legs when other legs
# of publish failed; legs without an artifact will fail at download and be
# the only ones marked red.
push_local_registry:
if: ${{ !cancelled() && github.repository == 'sgl-project/sglang' && (github.event_name != 'workflow_dispatch' || inputs.job_select == 'all' || inputs.job_select == 'publish') }}
runs-on: linux-mi300-1gpu-sglang
environment: 'prod'
needs: publish
strategy:
fail-fast: false
matrix:
gpu_arch: ['gfx942-rocm720', 'gfx950-rocm720']
steps:
- name: Download image tag artifact
uses: actions/download-artifact@v4
with:
name: image-tag-${{ matrix.gpu_arch }}
- name: Read image tag
run: |
image_tag=$(tr -d '[:space:]' < "${{ matrix.gpu_arch }}.txt")
if [ -z "${image_tag}" ]; then
echo "::error::Image tag artifact is empty"
exit 1
fi
echo "IMAGE_TAG=${image_tag}" >> $GITHUB_ENV
echo "Resolved IMAGE_TAG=${image_tag}"
- name: Login to Docker Hub (AMD)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_AMD_USERNAME }}
password: ${{ secrets.DOCKERHUB_AMD_TOKEN }}
- name: Mirror rocm/sgl-dev to local registry
run: |
src="rocm/sgl-dev:${{ env.IMAGE_TAG }}"
dst="10.44.14.109:5000/rocm/sgl-dev:${{ env.IMAGE_TAG }}"
docker pull "${src}"
docker tag "${src}" "${dst}"
docker push "${dst}"
+91
View File
@@ -0,0 +1,91 @@
name: Release Docker Images (AMD)
on:
push:
tags:
- 'v[0-9]+.*'
workflow_dispatch:
inputs:
version:
description: 'Version to build (without v prefix, e.g., 0.5.7)'
required: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: amd-docker-scale
environment: 'prod'
strategy:
matrix:
rocm_version: ['rocm700', 'rocm720']
gpu_arch: ['gfx942', 'gfx950']
build_type: ['all']
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get version from tag
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Extract version from tag (e.g., v0.5.7 -> 0.5.7)
VERSION="${GITHUB_REF_NAME#v}"
fi
# Validate version format
if [ -z "$VERSION" ]; then
echo "::error::Version is empty"
exit 1
fi
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version format: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Build and Push
run: |
version=${{ steps.version.outputs.version }}
echo "Version: ${version}"
gpu_arch_suffix=""
if [ "${{ matrix.rocm_version }}" = "rocm700" ]; then
if [ "${{ matrix.gpu_arch }}" = "gfx942" ]; then
rocm_tag="rocm700-mi30x"
elif [ "${{ matrix.gpu_arch }}" = "gfx950" ]; then
rocm_tag="rocm700-mi35x"
else
echo "Unsupported gfx arch"
exit 1
fi
elif [ "${{ matrix.rocm_version }}" = "rocm720" ]; then
gpu_arch_suffix="-${{ matrix.rocm_version }}"
if [ "${{ matrix.gpu_arch }}" = "gfx942" ]; then
rocm_tag="rocm720-mi30x"
elif [ "${{ matrix.gpu_arch }}" = "gfx950" ]; then
rocm_tag="rocm720-mi35x"
else
echo "Unsupported gfx arch"
exit 1
fi
else
echo "Unsupported rocm version"
exit 1
fi
tag=v${version}-${rocm_tag}
# rocm.Dockerfile expects SGL_BRANCH with 'v' prefix for git tag checkout
# remove --build-arg NIC_BACKEND=ainic for auto detection nic support in mori
# UBUNTU_MIRROR forces apt over HTTPS to dodge port-80 reachability flakes
# to Canonical's archive.ubuntu.com mirror IPs from the amd-docker-scale runner.
docker build . -f docker/rocm.Dockerfile --build-arg BUILD_TYPE=${{ matrix.build_type }} --build-arg GPU_ARCH=${{ matrix.gpu_arch }}${gpu_arch_suffix} --build-arg SGL_BRANCH=v${version} --build-arg ENABLE_MORI=1 --build-arg UBUNTU_MIRROR=https://archive.ubuntu.com -t lmsysorg/sglang:${tag} --no-cache
docker push lmsysorg/sglang:${tag}
+200
View File
@@ -0,0 +1,200 @@
name: Build and Push Development Docker Images
on:
workflow_dispatch:
inputs:
pr_number:
description: "PR number to build from (leave empty to use current branch)"
required: false
default: ""
tag:
description: "Custom tag suffix (overrides pr_number in tag). E.g. 'my-test' → dev-my-test, dev-cu13-my-test, etc."
required: false
default: ""
image_repo:
description: "Docker Hub repo to push to. Use lmsysorg/sglang-staging for testing."
required: false
default: "lmsysorg/sglang"
overlay_dockerfile:
description: "Optional extra Dockerfile lines appended after FROM <base> to build a layered image (e.g. 'RUN pip install ...'). Note: this job has no repo checkout, so only FROM + RUN work — you cannot COPY files from this repo. Leave empty to skip overlay."
required: false
default: ""
overlay_cudas:
description: "Comma-separated cuda variants to overlay onto (cu12, cu13). Default 'cu13'."
required: false
default: "cu13"
overlay_tag_suffix:
description: "Overlay output suffix appended to the base tag. EMPTY = overwrite the base tag(s); non-empty = append (e.g. 'msa')."
required: false
default: ""
schedule:
- cron: "0 0 * * *"
concurrency:
group: release-docker-dev-${{ inputs.tag || inputs.pr_number || 'nightly' }}
cancel-in-progress: true
jobs:
prepare:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
checkout_ref: ${{ steps.config.outputs.checkout_ref }}
extra_build_args: ${{ steps.config.outputs.extra_build_args }}
tag_config: ${{ steps.config.outputs.tag_config }}
overlay_cudas_json: ${{ steps.config.outputs.overlay_cudas_json }}
overlay_tags_json: ${{ steps.config.outputs.overlay_tags_json }}
steps:
- name: Compute build configuration
id: config
env:
# Inject multi-line/free-text inputs via env to avoid ${{ }} injection
# breaking the bash script on quotes or $ in the Dockerfile snippet.
OVERLAY_DOCKERFILE_INPUT: ${{ inputs.overlay_dockerfile }}
OVERLAY_CUDAS_INPUT: ${{ inputs.overlay_cudas }}
OVERLAY_SUFFIX_INPUT: ${{ inputs.overlay_tag_suffix }}
run: |
# Determine checkout ref
if [ -n "${{ inputs.pr_number }}" ]; then
echo "checkout_ref=refs/pull/${{ inputs.pr_number }}/head" >> $GITHUB_OUTPUT
else
echo "checkout_ref=" >> $GITHUB_OUTPUT
fi
# Determine extra build args. INSTALL_DYNAMO=1 installs the ai-dynamo
# nightly here only; release-docker.yml / release-docker-runtime.yml
# leave it at the Dockerfile default (0), keeping it out of releases.
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "extra_build_args=--build-arg USE_LATEST_SGLANG=1 --build-arg INSTALL_DYNAMO=1 --build-arg CMAKE_BUILD_PARALLEL_LEVEL=\$(nproc)" >> $GITHUB_OUTPUT
else
echo "extra_build_args=--build-arg BRANCH_TYPE=local --build-arg INSTALL_DYNAMO=1 --build-arg CMAKE_BUILD_PARALLEL_LEVEL=\$(nproc)" >> $GITHUB_OUTPUT
fi
# Determine tag suffix
SUFFIX=""
if [ -n "${{ inputs.tag }}" ]; then
SUFFIX="-${{ inputs.tag }}"
elif [ -n "${{ inputs.pr_number }}" ]; then
SUFFIX="-pr-${{ inputs.pr_number }}"
fi
# Build tag config. dev-cu13 / nightly-dev-cu13 are published as
# aliases on the cu130 image for backwards compatibility with
# consumers pinned to the pre-flip names.
if [ -z "${SUFFIX}" ]; then
# Nightly: include dated tags
TAG_CONFIG='[{"cuda":"cu129","tags":["dev-cu12","nightly-dev-cu12-{date}-{short_sha}"]},{"cuda":"cu130","tags":["dev","dev-cu13","nightly-dev-{date}-{short_sha}","nightly-dev-cu13-{date}-{short_sha}"]}]'
else
TAG_CONFIG="[{\"cuda\":\"cu129\",\"tags\":[\"dev-cu12${SUFFIX}\"]},{\"cuda\":\"cu130\",\"tags\":[\"dev${SUFFIX}\",\"dev-cu13${SUFFIX}\"]}]"
fi
echo "tag_config=${TAG_CONFIG}" >> $GITHUB_OUTPUT
# Overlay configuration (optional). When overlay_dockerfile is set,
# build one or more layered images on top of the just-built base.
# SUFFIX was computed above from inputs.tag / inputs.pr_number.
if [ -n "${OVERLAY_DOCKERFILE_INPUT}" ]; then
# Refuse to overlay without a tag/pr_number: SUFFIX would be empty,
# so base would point at the moving 'dev'/'dev-cu12'/'dev-cu13'
# latest tags and overwrite mode would clobber them.
if [ -z "${SUFFIX}" ]; then
echo "overlay_dockerfile is set but neither 'tag' nor 'pr_number' was given." >&2
echo "Set one so the overlay targets a specific tag, not the moving 'dev' latest." >&2
exit 1
fi
CUDAS_INPUT="${OVERLAY_CUDAS_INPUT}"
[ -z "${CUDAS_INPUT}" ] && CUDAS_INPUT="cu13"
OVERLAY_TAGS='{}'
IFS=',' read -ra CUDAS <<< "${CUDAS_INPUT}"
for C in "${CUDAS[@]}"; do
C="$(echo "${C}" | xargs)"
if [ "${C}" = "cu13" ]; then
BASES='["dev'"${SUFFIX}"'","dev-cu13'"${SUFFIX}"'"]'
elif [ "${C}" = "cu12" ]; then
BASES='["dev-cu12'"${SUFFIX}"'"]'
else
echo "Unknown overlay cuda variant: ${C} (expected cu12 or cu13)" >&2
exit 1
fi
if [ -n "${OVERLAY_SUFFIX_INPUT}" ]; then
# Append mode: primary base tag + suffix.
PRIMARY="$(echo "${BASES}" | jq -r '.[0]')"
OUTS='["'"${PRIMARY}-${OVERLAY_SUFFIX_INPUT}"'"]'
else
# Overwrite mode: replace every base alias.
OUTS="${BASES}"
fi
OVERLAY_TAGS="$(echo "${OVERLAY_TAGS}" | jq -c \
--arg c "${C}" --argjson b "${BASES}" --argjson o "${OUTS}" \
'.[$c] = {bases:$b, outs:$o}')"
done
CUDAS_JSON="$(echo "${CUDAS_INPUT}" | jq -c -R 'split(",") | map(gsub("^\\s+|\\s+$";""))')"
echo "overlay_cudas_json=${CUDAS_JSON}" >> $GITHUB_OUTPUT
echo "overlay_tags_json=${OVERLAY_TAGS}" >> $GITHUB_OUTPUT
else
echo "overlay_cudas_json=[]" >> $GITHUB_OUTPUT
echo "overlay_tags_json={}" >> $GITHUB_OUTPUT
fi
build-and-publish:
needs: prepare
uses: ./.github/workflows/_docker-build-and-publish.yml
with:
docker_target: framework_final
checkout_ref: ${{ needs.prepare.outputs.checkout_ref }}
extra_build_args: ${{ needs.prepare.outputs.extra_build_args }}
tag_config: ${{ needs.prepare.outputs.tag_config }}
image_repo: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
secrets: inherit
cleanup-nightly:
needs: build-and-publish
if: ${{ !inputs.tag && !inputs.pr_number }}
uses: ./.github/workflows/_docker-cleanup-nightly.yml
with:
tag_prefixes: '["nightly-dev", "nightly-dev-cu12", "nightly-dev-cu13"]'
image_repo: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
secrets: inherit
build-overlay:
needs: [prepare, build-and-publish]
if: ${{ inputs.overlay_dockerfile != '' && github.repository == 'sgl-project/sglang' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
cuda: ${{ fromJson(needs.prepare.outputs.overlay_cudas_json) }}
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push overlay image (amd64 + arm64)
env:
REPO: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
TAGS: ${{ needs.prepare.outputs.overlay_tags_json }}
CUDA: ${{ matrix.cuda }}
# Injected via env to keep Dockerfile quotes/$ verbatim.
SNIPPET: ${{ inputs.overlay_dockerfile }}
run: |
set -euo pipefail
BASE="$(printf '%s' "$TAGS" | jq -r --arg c "$CUDA" '.[$c].bases[0]')"
mapfile -t OUTS < <(printf '%s' "$TAGS" | jq -r --arg c "$CUDA" '.[$c].outs[]')
{
printf 'FROM %s:%s\n' "$REPO" "$BASE"
printf '%s\n' "$SNIPPET"
} > Dockerfile.overlay
echo "::group::overlay Dockerfile ($CUDA)"
cat Dockerfile.overlay
echo "::endgroup::"
OUT_ARGS=""
for o in "${OUTS[@]}"; do OUT_ARGS="$OUT_ARGS -t $REPO:$o"; done
docker buildx build \
--platform linux/amd64,linux/arm64 \
-f Dockerfile.overlay \
$OUT_ARGS \
--push .
@@ -0,0 +1,39 @@
name: Release SGLang Model Gateway Docker Image
on:
push:
branches:
- main
paths:
- sgl-model-gateway/bindings/python/pyproject.toml
workflow_dispatch:
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push
run: |
version=$(cat sgl-model-gateway/bindings/python/src/sglang_router/version.py | cut -d'"' -f2)
tag=v${version}
docker buildx build . -f docker/gateway.Dockerfile \
--platform linux/amd64,linux/arm64 \
-t lmsysorg/sgl-model-gateway:${tag} \
-t lmsysorg/sgl-model-gateway:latest \
--push
@@ -0,0 +1,59 @@
name: Release Docker Images Nightly (Intel XPU)
on:
workflow_dispatch:
schedule:
- cron: '0 12 * * *'
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: intel-bmg
environment: 'prod'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
path: sglang-nightly-build
fetch-depth: 0
- name: Set date and commit hash
id: image_meta
run: |
cd sglang-nightly-build
date_tag=$(date +%Y%m%d)
commit_hash=$(git rev-parse --short=7 HEAD)
if [ -z "${commit_hash}" ]; then
echo "::error::Could not resolve commit hash"
exit 1
fi
image_tag="nightly-dev-xpu-bmg-${date_tag}-${commit_hash}"
echo "DATE=${date_tag}" >> $GITHUB_ENV
echo "commit_hash=${commit_hash}" >> $GITHUB_OUTPUT
echo "IMAGE_TAG=${image_tag}" >> $GITHUB_ENV
echo "Image tag: ${image_tag}"
- name: Login to Docker Hub (Intel)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_INTEL_USERNAME }}
password: ${{ secrets.DOCKERHUB_INTEL_TOKEN }}
- name: Build intel/sglang-dev
run: |
echo "Building intel/sglang-dev:${{ env.IMAGE_TAG }}"
docker build sglang-nightly-build \
-f sglang-nightly-build/docker/xpu.Dockerfile \
--build-arg SG_LANG_REPO=https://github.com/sgl-project/sglang.git \
--build-arg SG_LANG_BRANCH=main \
--no-cache --progress=plain \
-t "intel/sglang-dev:${{ env.IMAGE_TAG }}" \
-t "intel/sglang-dev:latest"
- name: Push intel/sglang-dev
run: |
docker push "intel/sglang-dev:${{ env.IMAGE_TAG }}"
docker push "intel/sglang-dev:latest"
@@ -0,0 +1,85 @@
name: Release Docker Images Nightly (NPU)
on:
pull_request:
branches:
- 'main'
paths:
- '.github/workflows/release-docker-npu-nightly.yml'
- 'docker/npu.Dockerfile'
workflow_dispatch:
schedule:
- cron: "0 16 * * *" # Execute at 0:00 a.m. Beijing Time every day
concurrency:
group: ${{ github.workflow }}-${{ github.sha }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-22.04-arm
strategy:
matrix:
cann_version: ["9.0.0"]
device_type: ["910b", "a3"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Free up disk space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: true
docker-images: false
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
lmsysorg/sglang
# push with schedule event
# push with workflow_dispatch event
tags: |
type=ref,event=pr
type=ref,event=branch
type=schedule,pattern=main
flavor: |
latest=false
suffix=-cann${{ matrix.cann_version }}-${{ matrix.device_type }},onlatest=true
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into docker hub
uses: docker/login-action@v3
if: ${{ github.repository == 'sgl-project/sglang' && github.event_name != 'pull_request' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Enable Docker multi-architecture build environment
# Emulate non-native architectures
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Required for building and pushing multi-arch Docker images
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
context: docker
file: docker/npu.Dockerfile
platforms: linux/arm64,linux/amd64
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }}
push: ${{ github.repository == 'sgl-project/sglang' && github.event_name != 'pull_request' }}
provenance: false
build-args: |
SGLANG_KERNEL_NPU_TAG=2026.6.2
CANN_VERSION=${{ matrix.cann_version }}
DEVICE_TYPE=${{ matrix.device_type }}
+93
View File
@@ -0,0 +1,93 @@
name: Release Docker Images (NPU)
on:
push:
tags:
- 'v[0-9]+.*'
workflow_dispatch:
inputs:
version:
description: 'Version to build (without v prefix, e.g., 0.5.7)'
required: true
jobs:
build:
runs-on: ubuntu-22.04-arm
strategy:
matrix:
cann_version: ["9.0.0"]
device_type: ["910b", "a3"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Free up disk space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: true
docker-images: false
# push with tag
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
lmsysorg/sglang
tags: |
type=ref,event=pr
flavor: |
latest=false
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Login to Docker Hub
uses: docker/login-action@v2
if: ${{ github.repository == 'sgl-project/sglang' && github.event_name != 'pull_request' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get version from tag
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Extract version from tag (e.g., v0.5.7 -> 0.5.7)
VERSION="${GITHUB_REF_NAME#v}"
fi
# Validate version format
if [ -z "$VERSION" ]; then
echo "::error::Version is empty"
exit 1
fi
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version format: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=v${VERSION}" >> $GITHUB_OUTPUT
echo "TAG=lmsysorg/sglang:v${VERSION}-cann${{ matrix.cann_version }}-${{ matrix.device_type }}" >> $GITHUB_OUTPUT
# Enable Docker multi-architecture build environment
# Emulate non-native architectures
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Required for building and pushing multi-arch Docker images
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
context: docker
file: docker/npu.Dockerfile
platforms: linux/arm64,linux/amd64
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags || steps.version.outputs.TAG }}
push: ${{ github.repository == 'sgl-project/sglang' && github.event_name != 'pull_request' }}
provenance: false
build-args: |
SGLANG_KERNEL_NPU_TAG=2026.6.2
CANN_VERSION=${{ matrix.cann_version }}
DEVICE_TYPE=${{ matrix.device_type }}
SGLANG_TAG=${{ steps.version.outputs.version }}
@@ -0,0 +1,55 @@
name: Release Docker Runtime Images
#
# Builds and publishes runtime Docker images (production-optimized, ~50% smaller):
# - lmsysorg/sglang:v{version}-runtime, lmsysorg/sglang:latest-runtime
# - lmsysorg/sglang:v{version}-cu129-runtime, lmsysorg/sglang:latest-cu129-runtime
#
on:
push:
tags:
- "v[0-9]+.*"
workflow_dispatch:
inputs:
version:
description: "Version to build (without v prefix, e.g., 0.5.7)"
required: true
image_repo:
description: "Docker Hub repo to push to. Use lmsysorg/sglang-staging for testing."
required: false
default: "lmsysorg/sglang"
jobs:
resolve-version:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#v}"
fi
if [ -z "$VERSION" ] || ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
build-and-publish:
needs: resolve-version
uses: ./.github/workflows/_docker-build-and-publish.yml
with:
docker_target: runtime
sgl_version: ${{ needs.resolve-version.outputs.version }}
use_environment: prod
image_repo: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
tag_config: |
[
{"cuda": "cu130", "tags": ["v${{ needs.resolve-version.outputs.version }}-runtime", "latest-runtime", "v${{ needs.resolve-version.outputs.version }}-cu130-runtime", "latest-cu130-runtime"]},
{"cuda": "cu129", "tags": ["v${{ needs.resolve-version.outputs.version }}-cu129-runtime", "latest-cu129-runtime"]}
]
secrets: inherit
+62
View File
@@ -0,0 +1,62 @@
name: Release Docker Xeon Images
on:
push:
tags:
- 'v[0-9]+.*'
workflow_dispatch:
inputs:
version:
description: 'Version to build (without v prefix, e.g., 0.5.7)'
required: true
jobs:
publish:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-24.04
environment: 'prod'
strategy:
matrix:
build_type: ['all']
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get version from tag
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
# Extract version from tag (e.g., v0.5.7 -> 0.5.7)
VERSION="${GITHUB_REF_NAME#v}"
fi
# Validate version format
if [ -z "$VERSION" ]; then
echo "::error::Version is empty"
exit 1
fi
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version format: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Build and Push
run: |
version=${{ steps.version.outputs.version }}
tag=v${version}-xeon
docker build . -f docker/xeon.Dockerfile \
--build-arg VER_SGLANG=v${version} \
-t lmsysorg/sglang:${tag} \
--no-cache
docker push lmsysorg/sglang:${tag}
+55
View File
@@ -0,0 +1,55 @@
name: Release Docker Images
#
# Builds and publishes framework Docker images (full development environment):
# - lmsysorg/sglang:v{version}, lmsysorg/sglang:latest (cuda 13)
# - lmsysorg/sglang:v{version}-cu129, lmsysorg/sglang:latest-cu129
#
on:
push:
tags:
- "v[0-9]+.*"
workflow_dispatch:
inputs:
version:
description: "Version to build (without v prefix, e.g., 0.5.7)"
required: true
image_repo:
description: "Docker Hub repo to push to. Use lmsysorg/sglang-staging for testing."
required: false
default: "lmsysorg/sglang"
jobs:
resolve-version:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Get version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#v}"
fi
if [ -z "$VERSION" ] || ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version: $VERSION (expected: X.Y.Z)"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
build-and-publish:
needs: resolve-version
uses: ./.github/workflows/_docker-build-and-publish.yml
with:
docker_target: framework_final
sgl_version: ${{ needs.resolve-version.outputs.version }}
use_environment: prod
image_repo: ${{ inputs.image_repo || 'lmsysorg/sglang' }}
tag_config: |
[
{"cuda": "cu130", "tags": ["v${{ needs.resolve-version.outputs.version }}", "latest", "v${{ needs.resolve-version.outputs.version }}-cu130", "latest-cu130"]},
{"cuda": "cu129", "tags": ["v${{ needs.resolve-version.outputs.version }}-cu129", "latest-cu129"]}
]
secrets: inherit
+167
View File
@@ -0,0 +1,167 @@
name: Release SGLang Model Gateway to PyPI
on:
push:
branches:
- main
paths:
- sgl-model-gateway/bindings/python/pyproject.toml
workflow_dispatch:
jobs:
build:
name: build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }})
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu, macos, windows]
target: [x86_64, aarch64]
manylinux: [auto]
include:
- os: ubuntu
platform: linux
- os: windows
ls: dir
target: x86_64
python-architecture: x64
interpreter: 3.9 3.10 3.11 3.12 3.13
- os: macos
target: aarch64
interpreter: 3.9 3.10 3.11 3.12 3.13
- os: ubuntu
platform: linux
target: aarch64
# musllinux
- os: ubuntu
platform: linux
target: x86_64
manylinux: musllinux_1_1
- os: ubuntu
platform: linux
target: aarch64
manylinux: musllinux_1_1
exclude:
- os: windows
target: aarch64
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root and delete sglang-repo
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
ls -alt
shell: bash
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
architecture: ${{ matrix.python-architecture || 'x64' }}
- name: Install twine
run: pip install -U twine
- name: Install protoc (macOS)
if: matrix.os == 'macos'
run: brew install protobuf
- name: Install protoc (Windows)
if: matrix.os == 'windows'
run: choco install protoc -y
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
working-directory: bindings/python
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux || 'auto' }}
args: --release --out dist --features vendored-openssl --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 3.14' }}
rust-toolchain: stable
docker-options: -e CI -e CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc -e CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
before-script-linux: |
# Install build dependencies (perl/make for vendored OpenSSL, protoc for gRPC)
if command -v yum &> /dev/null; then
yum update -y && yum install -y wget unzip gcc gcc-c++ perl-core make
# Install cross-compilation toolchain for aarch64 if needed
if [ "${{ matrix.target }}" = "aarch64" ]; then
yum install -y gcc-aarch64-linux-gnu gcc-c++-aarch64-linux-gnu || true
fi
elif command -v apt-get &> /dev/null; then
apt-get update && apt-get install -y wget unzip gcc g++ perl make
# Install cross-compilation toolchain for aarch64 if needed
if [ "${{ matrix.target }}" = "aarch64" ]; then
apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu || true
fi
fi
(cd /tmp && \
wget https://github.com/protocolbuffers/protobuf/releases/download/v32.0/protoc-32.0-linux-x86_64.zip && \
unzip protoc-32.0-linux-x86_64.zip -d /usr/local && \
rm protoc-32.0-linux-x86_64.zip)
protoc --version
- name: List built packages
run: ${{ matrix.ls || 'ls -lh' }} bindings/python/dist/
- name: Check packages
run: twine check --strict bindings/python/dist/*
- uses: actions/upload-artifact@v4
with:
name: packages-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux || 'auto' }}
path: bindings/python/dist/
build-sdist:
name: Build SDist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: sglang-repo
- name: Move sgl-model-gateway folder to root and delete sglang-repo
run: |
mv sglang-repo/sgl-model-gateway/* .
rm -rf sglang-repo
ls -alt
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Build SDist
uses: PyO3/maturin-action@v1
with:
working-directory: bindings/python
command: sdist
args: --out dist
rust-toolchain: stable
- uses: actions/upload-artifact@v4
with:
name: sdist
path: bindings/python/dist/*.tar.gz
upload:
name: Upload to PyPI
if: github.repository == 'sgl-project/sglang' # Ensure this job only runs for the sgl-project/sglang repository
needs: [build, build-sdist]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Upload to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN_ROUTER }}
run: |
pip install twine
twine upload dist/* --verbose
+175
View File
@@ -0,0 +1,175 @@
name: Release PyPI Nightly Wheels
on:
# Run daily at 2 AM UTC
schedule:
- cron: '0 2 * * *'
# Triggered by nightly Docker workflow to use same commit
repository_dispatch:
types: [nightly-release]
# Manual trigger for testing
workflow_dispatch:
inputs:
commit_sha:
description: 'Specific commit SHA to build (leave empty for latest)'
required: false
type: string
concurrency:
group: release-pypi-nightly-${{ github.ref }}
cancel-in-progress: true
jobs:
build-nightly-wheel:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
nightly_version: ${{ steps.build.outputs.nightly_version }}
commit_hash: ${{ steps.build.outputs.commit_hash }}
build_date: ${{ steps.build.outputs.build_date }}
steps:
- uses: actions/checkout@v4
with:
# Use commit from: 1) Docker workflow, 2) manual input, 3) latest main
ref: ${{ github.event.client_payload.commit_sha || inputs.commit_sha || github.sha }}
fetch-depth: 0 # Need full history for setuptools-scm
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install build dependencies
run: |
pip install build wheel setuptools setuptools-scm
# Needed by setuptools-rust to build the bundled native gRPC extension
# (rust/sglang-grpc) when `python -m build` builds the sglang wheel.
- name: Install protoc
run: sudo bash scripts/ci/utils/install_protoc.sh
- name: Build wheel
id: build
run: |
cd python
cp ../README.md ../LICENSE .
TAG=$(python3 ../python/tools/get_version_tag.py)
HASH="g$(git rev-parse --short HEAD)"
BUILD_DATE=$(date -u +%Y%m%d)
# Increment patch version for nightlies (e.g., v0.5.9 -> 0.5.10)
# Must always increment so nightly > latest tag per PEP 440 ordering:
# X.Y.Z.devN < X.Y.Z.rcN < X.Y.Z < X.Y.(Z+1).devN
VERSION=${TAG#v} # Remove 'v' prefix
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
PATCH_RAW=$(echo "$VERSION" | cut -d. -f3)
# Strip pre-release suffixes (rc0, post1, etc.) to get numeric patch
PATCH=$(echo "$PATCH_RAW" | sed 's/[^0-9].*//')
NEXT_PATCH=$((PATCH + 1))
NEXT_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}"
# Use date-based dev number for correct chronological sorting
# e.g., 0.5.9.dev20260215+g4cf4f0859 > 0.5.9.dev20260214+g45a4697d4
FORCE_VERSION="${NEXT_VERSION}.dev${BUILD_DATE}+${HASH}"
echo "Forcing nightly version to: $FORCE_VERSION"
export SETUPTOOLS_SCM_PRETEND_VERSION="$FORCE_VERSION"
# Build wheel
python3 -m build --wheel
# Extract version from built wheel filename
WHEEL_FILE=$(ls dist/*.whl)
NIGHTLY_VERSION=$(echo "$WHEEL_FILE" | sed 's/.*sglang-\(.*\)-py3.*/\1/')
# Get commit info
COMMIT_HASH=$(git rev-parse --short HEAD)
BUILD_DATE=$(date -u +%Y-%m-%d)
echo "Built wheel: $WHEEL_FILE"
echo "Nightly version: ${NIGHTLY_VERSION}"
echo "Commit: ${COMMIT_HASH}"
echo "Build date: ${BUILD_DATE}"
echo "nightly_version=${NIGHTLY_VERSION}" >> $GITHUB_OUTPUT
echo "commit_hash=${COMMIT_HASH}" >> $GITHUB_OUTPUT
echo "build_date=${BUILD_DATE}" >> $GITHUB_OUTPUT
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: nightly-wheel
path: python/dist/*.whl
retention-days: 7
release-nightly:
needs: build-nightly-wheel
runs-on: ubuntu-latest
environment: 'prod'
strategy:
fail-fast: false
# The wheel is CUDA-agnostic and built once — we just register the same
# artifact under cu129/sglang/ and cu130/sglang/ wheel indexes so users
# can install via either --extra-index-url. Serialize because both matrix
# runs clone and push to the same sgl-whl branch.
max-parallel: 1
matrix:
cuda_version: ['129', '130']
steps:
- uses: actions/checkout@v4
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: nightly-wheel
path: dist/
- name: List downloaded wheels
run: |
echo "Downloaded wheel:"
ls -lh dist/
- name: Create GitHub Release for nightly wheel
uses: softprops/action-gh-release@v2
with:
tag_name: nightly-${{ needs.build-nightly-wheel.outputs.build_date }}-${{ needs.build-nightly-wheel.outputs.commit_hash }}
name: Nightly Build ${{ needs.build-nightly-wheel.outputs.build_date }} (${{ needs.build-nightly-wheel.outputs.commit_hash }})
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
prerelease: true
body: |
Nightly build from commit ${{ github.sha }}
Build date: ${{ needs.build-nightly-wheel.outputs.build_date }}
Version: ${{ needs.build-nightly-wheel.outputs.nightly_version }}
files: |
dist/*.whl
- name: Clone wheel index repository
run: |
git clone https://oauth2:${WHL_TOKEN}@github.com/sgl-project/whl.git sgl-whl
cd sgl-whl
git config --local user.name "sglang-bot"
git config --local user.email "sglangbot@gmail.com"
env:
WHL_TOKEN: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Update wheel index
run: |
python3 scripts/update_nightly_whl_index.py \
--commit-hash ${{ needs.build-nightly-wheel.outputs.commit_hash }} \
--nightly-version ${{ needs.build-nightly-wheel.outputs.nightly_version }} \
--cuda-version ${{ matrix.cuda_version }} \
--build-date ${{ needs.build-nightly-wheel.outputs.build_date }}
- name: Push wheel index
run: |
cd sgl-whl
git add -A
git diff --staged --quiet || git commit -m "Update cu${{ matrix.cuda_version }} nightly wheel index for commit ${{ needs.build-nightly-wheel.outputs.commit_hash }}"
git push
+178
View File
@@ -0,0 +1,178 @@
name: Release PyPI PR Wheels
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to build wheel for (works with both internal and fork PRs)'
required: true
type: string
concurrency:
group: build-pr-wheel-${{ github.event.inputs.pr_number }}
cancel-in-progress: true
jobs:
build-pr-wheel:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
outputs:
wheel_version: ${{ steps.gen_version.outputs.wheel_version }}
commit_hash: ${{ steps.gen_version.outputs.commit_hash }}
build_date: ${{ steps.gen_version.outputs.build_date }}
steps:
- uses: actions/checkout@v4
with:
ref: refs/pull/${{ inputs.pr_number }}/head
fetch-depth: 0 # Need full history for version generation
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Generate PR wheel version
id: gen_version
run: |
LATEST_TAG=$(python3 python/tools/get_version_tag.py)
BASE_VERSION=${LATEST_TAG#v}
echo "Latest release tag: ${LATEST_TAG}"
# Get commit info
COMMIT_HASH=$(git rev-parse --short HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
# Get current date in YYYY-MM-DD format
BUILD_DATE=$(date -u +%Y-%m-%d)
# Always use pr-{number} format for suffix
SUFFIX="pr-${{ inputs.pr_number }}"
# Generate PR wheel version following PEP 440
# Format: {base_version}.dev{commit_count}+pr-{number}.g{commit_hash}
WHEEL_VERSION="${BASE_VERSION}.dev${COMMIT_COUNT}+${SUFFIX}.g${COMMIT_HASH}"
echo "Base version: ${BASE_VERSION}"
echo "PR wheel version: ${WHEEL_VERSION}"
echo "Commit: ${COMMIT_HASH}"
echo "Build date: ${BUILD_DATE}"
echo "wheel_version=${WHEEL_VERSION}" >> $GITHUB_OUTPUT
echo "commit_hash=${COMMIT_HASH}" >> $GITHUB_OUTPUT
echo "base_version=${BASE_VERSION}" >> $GITHUB_OUTPUT
echo "build_date=${BUILD_DATE}" >> $GITHUB_OUTPUT
- name: Update pyproject.toml with PR wheel version
run: |
cd python
WHEEL_VERSION="${{ steps.gen_version.outputs.wheel_version }}"
# Update pyproject.toml to use static version instead of dynamic
# Remove 'version' from dynamic list and add static version
sed -i 's/dynamic = \["version"\]/dynamic = []/' pyproject.toml
sed -i "/^name = \"sglang\"/a version = \"${WHEEL_VERSION}\"" pyproject.toml
# Verify update
echo "Updated version in pyproject.toml:"
grep "^version" pyproject.toml
grep "^dynamic" pyproject.toml
- name: Install build dependencies
run: |
cd python
pip install build wheel setuptools
- name: Build wheel
run: |
cd python
cp ../README.md ../LICENSE .
python3 -m build --wheel
# List built wheels
echo "Built wheel:"
ls -lh dist/
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: pr-wheel-${{ inputs.pr_number }}
path: python/dist/*.whl
retention-days: 30
release-pr-wheel:
needs: build-pr-wheel
runs-on: ubuntu-latest
environment: 'prod'
steps:
- uses: actions/checkout@v4
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: pr-wheel-${{ inputs.pr_number }}
path: dist/
- name: List downloaded wheels
run: |
echo "Downloaded wheel:"
ls -lh dist/
- name: Create GitHub Release for PR wheel
uses: softprops/action-gh-release@v2
with:
tag_name: pr-${{ inputs.pr_number }}-${{ needs.build-pr-wheel.outputs.build_date }}-${{ needs.build-pr-wheel.outputs.commit_hash }}
name: "PR #${{ inputs.pr_number }} Build (${{ needs.build-pr-wheel.outputs.commit_hash }})"
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
prerelease: true
body: |
PR wheel build from PR #${{ inputs.pr_number }}
Commit: ${{ github.sha }}
Build date: ${{ needs.build-pr-wheel.outputs.build_date }}
Version: ${{ needs.build-pr-wheel.outputs.wheel_version }}
**Installation via index (pip):**
```bash
pip install sglang==${{ needs.build-pr-wheel.outputs.wheel_version }} --index-url https://sgl-project.github.io/whl/pr/
```
**Installation via index (uv):**
```bash
uv pip install sglang==${{ needs.build-pr-wheel.outputs.wheel_version }} --index-url https://sgl-project.github.io/whl/pr/ --extra-index-url https://pypi.org/simple --index-strategy unsafe-best-match
```
**Direct installation:**
```bash
pip install https://github.com/sgl-project/whl/releases/download/pr-${{ inputs.pr_number }}-${{ needs.build-pr-wheel.outputs.build_date }}-${{ needs.build-pr-wheel.outputs.commit_hash }}/sglang-${{ needs.build-pr-wheel.outputs.wheel_version }}-py3-none-any.whl
```
files: |
dist/*.whl
- name: Clone wheel index repository
run: |
git clone https://oauth2:${WHL_TOKEN}@github.com/sgl-project/whl.git sgl-whl
cd sgl-whl
git config --local user.name "sglang-bot"
git config --local user.email "sglangbot@gmail.com"
env:
WHL_TOKEN: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Update wheel index
run: |
python3 scripts/update_pr_whl_index.py \
--pr-number ${{ inputs.pr_number }} \
--commit-hash ${{ needs.build-pr-wheel.outputs.commit_hash }} \
--wheel-version ${{ needs.build-pr-wheel.outputs.wheel_version }} \
--build-date ${{ needs.build-pr-wheel.outputs.build_date }}
- name: Push wheel index
run: |
cd sgl-whl
git add -A
git diff --staged --quiet || git commit -m "Update PR wheel index for PR #${{ inputs.pr_number }} (commit ${{ needs.build-pr-wheel.outputs.commit_hash }})"
git push
+116
View File
@@ -0,0 +1,116 @@
name: Release PyPI
on:
push:
tags:
- 'v[0-9]+.*'
workflow_dispatch:
inputs:
version:
description: 'Release version to publish (e.g. v0.5.11). Overrides setuptools-scm.'
required: true
type: string
jobs:
build:
if: github.repository == 'sgl-project/sglang'
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
arch: [x86_64, aarch64]
include:
- arch: x86_64
runner: x64-docker-build-node
- arch: aarch64
runner: arm-docker-build-node
runs-on: ${{ matrix.runner }}
steps:
# Self-hosted build nodes retain the workspace across jobs. Prior builds
# leave root-owned artifacts behind that actions/checkout cannot remove,
# causing EACCES on rmdir. Wipe them via a throwaway root container.
- name: Clean workspace (remove root-owned files from prior runs)
run: |
docker run --rm -v "${{ github.workspace }}:/workspace" alpine:3 \
sh -c 'rm -rf /workspace/..?* /workspace/.[!.]* /workspace/*' || true
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for setuptools-scm to determine version from tags
submodules: "recursive"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
# Needed by setuptools-rust to build the bundled native gRPC extension
# (rust/sglang-grpc) when `python -m build` builds the sglang wheel.
- name: Install protoc
run: sudo bash scripts/ci/utils/install_protoc.sh
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build wheel
env:
# When triggered manually, pin the wheel version to the supplied
# input (strip leading `v`) so setuptools-scm doesn't derive a
# `.devN+gHASH` version from the branch HEAD.
RELEASE_VERSION: ${{ inputs.version }}
run: |
cd python
cp ../README.md ../LICENSE .
pip install build wheel setuptools setuptools-scm setuptools-rust
if [ -n "$RELEASE_VERSION" ]; then
export SETUPTOOLS_SCM_PRETEND_VERSION="${RELEASE_VERSION#v}"
echo "Pinning wheel version to $SETUPTOOLS_SCM_PRETEND_VERSION"
fi
python3 -m build --wheel
# PyPI rejects plain `linux_x86_64` / `linux_aarch64` platform tags;
# auditwheel rewrites the wheel's platform tag to a `manylinux_*` tag
# and bundles any external native deps. The runner's glibc determines
# the lowest acceptable manylinux policy.
- name: Repair wheel for manylinux
run: |
cd python
pip install auditwheel patchelf
mkdir -p dist-repaired
python3 -m auditwheel repair dist/*.whl -w dist-repaired/
rm dist/*.whl
mv dist-repaired/*.whl dist/
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: wheel-py${{ matrix.python-version }}-${{ matrix.arch }}
path: python/dist/*.whl
publish:
needs: build
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
environment: "prod"
steps:
- name: Download all wheels
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: wheel-*
- name: List wheels
run: ls -lh dist/
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Upload to pypi
run: |
pip install twine
# --skip-existing makes partial reruns safe (e.g. one arch failed,
# rerun only the failed shard without re-uploading already-published wheels).
python3 -m twine upload --skip-existing dist/*.whl -u __token__ -p ${{ secrets.PYPI_TOKEN }}
+68
View File
@@ -0,0 +1,68 @@
name: Release Tag
# Creates a git tag to trigger release workflows (PyPI, Docker)
# Use this after testing on a release branch is complete
on:
workflow_dispatch:
inputs:
version:
description: 'Version to tag (without v prefix, e.g., 0.5.7)'
required: true
type: string
ref:
description: 'Branch or commit to tag (e.g., release/v0.5.7, main, or commit SHA)'
required: false
default: 'main'
type: string
permissions:
contents: write
jobs:
create-tag:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-latest
environment: 'prod'
steps:
- name: Validate version format
run: |
VERSION="${{ github.event.inputs.version }}"
if [ -z "$VERSION" ]; then
echo "::error::Version is required"
exit 1
fi
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Invalid version format: $VERSION (expected: X.Y.Z or X.Y.Z.postN)"
exit 1
fi
echo "Version validated: v$VERSION"
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check if tag already exists
run: |
TAG="v${{ github.event.inputs.version }}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG already exists"
exit 1
fi
echo "Tag $TAG does not exist, proceeding..."
- name: Create and push tag
run: |
TAG="v${{ github.event.inputs.version }}"
REF="${{ github.event.inputs.ref }}"
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
echo "Creating tag $TAG on ref $REF (commit: $(git rev-parse HEAD))"
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
echo "::notice::Successfully created and pushed tag $TAG"
echo "This will trigger the release workflows (PyPI, Docker)"
+330
View File
@@ -0,0 +1,330 @@
name: Release sgl-deep-gemm
on:
workflow_dispatch:
inputs:
version:
description: "Wheel version (e.g. 0.1.0, 0.1.1rc0)"
type: string
required: true
target:
type: choice
description: "Build target (default: all)"
required: false
default: 'all'
options:
- 'all'
- 'cu129'
- 'cu130'
branch:
description: "DeepGEMM branch to build from (default: dev)"
type: string
required: false
default: 'dev'
concurrency:
group: release-sgl-deepgemm-${{ github.ref }}
cancel-in-progress: true
env:
# Must match TORCH_VER in docker/sgl-deep-gemm.Dockerfile — the wheel's
# pre-compiled _C.so links against this torch ABI.
TORCH_VER: "2.11.0"
jobs:
build-cu129-matrix:
if: |
github.repository == 'sgl-project/sglang' &&
(github.event.inputs.target == 'all' || github.event.inputs.target == 'cu129')
strategy:
matrix:
python-version: ["3.12"]
cuda-version: ["12.9"]
arch: [x86_64, aarch64]
include:
- arch: x86_64
runner: x64-kernel-build-node
- arch: aarch64
runner: arm-kernel-build-node
runs-on: ${{ matrix.runner }}
steps:
- name: Clean workspace (remove root-owned files from prior runs)
run: |
docker run --rm -v "${{ github.workspace }}:/workspace" alpine:3 \
sh -c 'rm -rf /workspace/..?* /workspace/.[!.]* /workspace/*' || true
- uses: actions/checkout@v4
- name: Checkout DeepGEMM
uses: actions/checkout@v4
with:
repository: sgl-project/DeepGEMM
ref: ${{ inputs.branch || 'dev' }}
path: DeepGEMM
submodules: recursive
- name: Set wheel version
run: |
echo -n "${{ inputs.version }}" > DeepGEMM/sgl_deep_gemm/VERSION
cat DeepGEMM/sgl_deep_gemm/VERSION
- name: Build wheel
run: |
chmod +x ./scripts/build_sgl_deep_gemm.sh ./scripts/rename_sgl_deep_gemm_whl.sh
./scripts/build_sgl_deep_gemm.sh "${{ matrix.python-version }}" "${{ matrix.cuda-version }}" "${{ github.workspace }}/DeepGEMM" "${{ matrix.arch }}"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: deepgemm-wheel-cuda${{ matrix.cuda-version }}-${{ matrix.arch }}
path: DeepGEMM/dist/*.whl
test-cu129:
needs: build-cu129-matrix
strategy:
fail-fast: false
matrix:
include:
- arch_label: sm90
runner: 8-gpu-h200
wheel_arch: x86_64
- arch_label: sm100
runner: 8-gpu-b200
wheel_arch: x86_64
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Checkout DeepGEMM
uses: actions/checkout@v4
with:
repository: sgl-project/DeepGEMM
ref: ${{ inputs.branch || 'dev' }}
path: DeepGEMM
submodules: recursive
- name: Download wheel
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: deepgemm-wheel-cuda12.9-${{ matrix.wheel_arch }}
- name: Install wheel and deps
env:
CU_TAG: cu129
run: |
python3 -m pip install --upgrade pip
# deep_gemm imports torch before loading _C.so, so torch must preload the
# libcudart the cu129 wheel links. Pin the CUDA-matched torch (see sgl-deep-gemm.Dockerfile).
python3 -m pip install "torch==${TORCH_VER}" --index-url "https://download.pytorch.org/whl/${CU_TAG}" --force-reinstall
python3 -m pip install numpy
python3 -m pip install dist/*.whl
python3 -c "import deep_gemm; print('deep_gemm:', deep_gemm.__file__)"
- name: Run DeepGEMM test suite
run: |
chmod +x "${{ github.workspace }}/DeepGEMM/sgl_deep_gemm/run_tests.sh"
"${{ github.workspace }}/DeepGEMM/sgl_deep_gemm/run_tests.sh"
release-cu129:
needs: [build-cu129-matrix, test-cu129]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: deepgemm-wheel-cuda12.9-*
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ inputs.version }}
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
files: |
dist/*
- name: Clone wheel index
run: git clone https://oauth2:${WHL_TOKEN}@github.com/sgl-project/whl.git sgl-whl
env:
WHL_TOKEN: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
- name: Update wheel index
run: python3 scripts/update_deepgemm_whl_index.py --cuda 129
- name: Push wheel index
run: |
cd sgl-whl
git config --local user.name "sglang-bot"
git config --local user.email "sglangbot@gmail.com"
git add -A
git commit -m "update sgl-deep-gemm whl index for v${{ inputs.version }}"
git push
build-cu130-matrix:
if: |
github.repository == 'sgl-project/sglang' &&
(github.event.inputs.target == 'all' || github.event.inputs.target == 'cu130')
strategy:
matrix:
python-version: ["3.12"]
cuda-version: ["13.0"]
arch: [x86_64, aarch64]
include:
- arch: x86_64
runner: x64-kernel-build-node
- arch: aarch64
runner: arm-kernel-build-node
runs-on: ${{ matrix.runner }}
steps:
- name: Clean workspace (remove root-owned files from prior runs)
run: |
docker run --rm -v "${{ github.workspace }}:/workspace" alpine:3 \
sh -c 'rm -rf /workspace/..?* /workspace/.[!.]* /workspace/*' || true
- uses: actions/checkout@v4
- name: Checkout DeepGEMM
uses: actions/checkout@v4
with:
repository: sgl-project/DeepGEMM
ref: ${{ inputs.branch || 'dev' }}
path: DeepGEMM
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set wheel version
run: |
echo -n "${{ inputs.version }}" > DeepGEMM/sgl_deep_gemm/VERSION
cat DeepGEMM/sgl_deep_gemm/VERSION
- name: Build wheel
run: |
chmod +x ./scripts/build_sgl_deep_gemm.sh ./scripts/rename_sgl_deep_gemm_whl.sh
./scripts/build_sgl_deep_gemm.sh "${{ matrix.python-version }}" "${{ matrix.cuda-version }}" "${{ github.workspace }}/DeepGEMM" "${{ matrix.arch }}"
# PyPI upload moved to release-cu130 so it is gated on test-cu130.
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: deepgemm-wheel-cuda${{ matrix.cuda-version }}-${{ matrix.arch }}
path: DeepGEMM/dist/*.whl
- name: Upload PyPI artifacts
uses: actions/upload-artifact@v4
with:
name: deepgemm-pypi-cuda${{ matrix.cuda-version }}-${{ matrix.arch }}
path: DeepGEMM/dist-pypi/*.whl
test-cu130:
needs: build-cu130-matrix
strategy:
fail-fast: false
matrix:
include:
- arch_label: sm90
runner: 8-gpu-h200
wheel_arch: x86_64
- arch_label: sm100
runner: 8-gpu-b200
wheel_arch: x86_64
- arch_label: sm100-aarch64
runner: 4-gpu-gb300
wheel_arch: aarch64
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Checkout DeepGEMM
uses: actions/checkout@v4
with:
repository: sgl-project/DeepGEMM
ref: ${{ inputs.branch || 'dev' }}
path: DeepGEMM
submodules: recursive
- name: Download wheel
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: deepgemm-wheel-cuda13.0-${{ matrix.wheel_arch }}
- name: Install wheel and deps
env:
CU_TAG: cu130
run: |
python3 -m pip install --upgrade pip
# deep_gemm imports torch before loading _C.so, so torch must preload the
# libcudart the cu130 wheel links. Pin the CUDA-matched torch (see sgl-deep-gemm.Dockerfile).
python3 -m pip install "torch==${TORCH_VER}" --index-url "https://download.pytorch.org/whl/${CU_TAG}" --force-reinstall
python3 -m pip install numpy
python3 -m pip install dist/*.whl
python3 -c "import deep_gemm; print('deep_gemm:', deep_gemm.__file__)"
- name: Run DeepGEMM test suite
run: |
chmod +x "${{ github.workspace }}/DeepGEMM/sgl_deep_gemm/run_tests.sh"
"${{ github.workspace }}/DeepGEMM/sgl_deep_gemm/run_tests.sh"
release-cu130:
needs: [build-cu130-matrix, test-cu130]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
pattern: deepgemm-wheel-cuda13.0-*
- name: Download PyPI artifacts
uses: actions/download-artifact@v4
with:
path: dist-pypi/
merge-multiple: true
pattern: deepgemm-pypi-cuda13.0-*
- name: Upload to PyPI
run: |
pip install --upgrade twine "packaging>=24.2"
python3 -m twine upload --skip-existing dist-pypi/* -u __token__ -p ${{ secrets.SGL_DEEP_GEMM_PYPI_TOKEN }}
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ inputs.version }}
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
files: |
dist/*
- name: Clone wheel index
run: git clone https://oauth2:${WHL_TOKEN}@github.com/sgl-project/whl.git sgl-whl
env:
WHL_TOKEN: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
- name: Update wheel index
run: python3 scripts/update_deepgemm_whl_index.py --cuda 130
- name: Push wheel index
run: |
cd sgl-whl
git config --local user.name "sglang-bot"
git config --local user.email "sglangbot@gmail.com"
git add -A
git commit -m "update sgl-deep-gemm whl index for v${{ inputs.version }}"
git push
+515
View File
@@ -0,0 +1,515 @@
name: Release SGLang Kernels
on:
push:
branches:
- main
paths:
- sgl-kernel/python/sgl_kernel/version.py
workflow_dispatch:
inputs:
target:
type: choice
description: 'Build target'
required: false
default: 'all'
options:
- 'all'
- 'cu129'
- 'cu130'
- 'rocm700'
- 'rocm720'
- 'musa43'
tag_name:
description: "Version number, must be in the form of vX.Y.Z (e.g. v0.4.0)"
type: string
required: false
pr_number:
description: "PR number to build from (e.g. 12345)"
type: string
required: false
concurrency:
group: release-sglang-kernels-${{ github.ref }}
cancel-in-progress: true
jobs:
# cu130 is the PyPI-released variant; cu129 wheels are published only to the
# sgl-project/whl index (consumed via `pip install ...+cu129` for the legacy
# cuda 12.9 path), not to PyPI.
build-cu129-matrix:
if: |
github.repository == 'sgl-project/sglang' &&
(github.event_name == 'push' || github.event.inputs.target == 'all' || github.event.inputs.target == 'cu129')
strategy:
matrix:
python-version: ["3.10"]
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:
# Self-hosted build nodes retain the workspace across jobs. Prior builds
# leave root-owned artifacts under 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
- uses: actions/checkout@v4
with:
submodules: "recursive"
ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || '' }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Build wheels
run: |
cd sgl-kernel
chmod +x ./build.sh
./build.sh "${{ matrix.python-version }}" "${{ matrix.cuda-version }}" ${{ matrix.arch == 'aarch64' && 'aarch64' || '' }}
env:
BUILD_JOBS: 64
NVCC_THREADS: 8
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: wheel-python${{ matrix.python-version }}-cuda${{ matrix.cuda-version }}${{ matrix.arch == 'aarch64' && '-aarch64' || '' }}
path: sgl-kernel/dist/*
release-cu129:
needs: build-cu129-matrix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || '' }}
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-*
- name: Set tag name
id: set_tag_name
run: |
if [ -z "${{ inputs.tag_name }}" ]; then
TAG_NAME="v$(cat sgl-kernel/python/sgl_kernel/version.py | cut -d'"' -f2)"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
else
echo "tag_name=${{ inputs.tag_name }}" >> $GITHUB_OUTPUT
fi
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.set_tag_name.outputs.tag_name }}
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
files: |
sgl-kernel/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_kernel_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 whl index"
git push
build-cu130-matrix:
if: |
github.repository == 'sgl-project/sglang' &&
(github.event_name == 'push' || github.event.inputs.target == 'all' || github.event.inputs.target == 'cu130')
strategy:
matrix:
python-version: ["3.10"]
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:
# Self-hosted build nodes retain the workspace across jobs. Prior builds
# leave root-owned artifacts under 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
- uses: actions/checkout@v4
with:
submodules: "recursive"
ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || '' }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Build wheels
run: |
cd sgl-kernel
chmod +x ./build.sh
./build.sh "${{ matrix.python-version }}" "${{ matrix.cuda-version }}" ${{ matrix.arch == 'aarch64' && 'aarch64' || '' }}
env:
BUILD_JOBS: 64
NVCC_THREADS: 8
- name: Strip +cu130 local version for PyPI upload
working-directory: sgl-kernel
run: |
set -eux
pip install wheel
mkdir -p dist-pypi
for w in dist/*.whl; do
tmp=$(mktemp -d)
python3 -m wheel unpack "$w" --dest "$tmp"
unpacked=$(find "$tmp" -mindepth 1 -maxdepth 1 -type d | head -1)
info=$(find "$unpacked" -maxdepth 1 -type d -name "*.dist-info" | head -1)
meta="$info/METADATA"
orig=$(grep '^Version:' "$meta" | head -1 | sed 's/^Version:[[:space:]]*//')
new=$(echo "$orig" | sed 's/+cu[0-9]\+$//')
if [ "$orig" != "$new" ]; then
sed -i "s/^Version:.*/Version: ${new}/" "$meta"
old_base=$(basename "$info")
new_base="${old_base/${orig}/${new}}"
mv "$info" "$(dirname "$info")/${new_base}"
fi
python3 -m wheel pack "$unpacked" --dest-dir dist-pypi
rm -rf "$tmp"
done
ls -lh dist-pypi/
- name: Upload to PyPI
working-directory: sgl-kernel
run: |
pip install twine
python3 -m twine upload --skip-existing dist-pypi/* -u __token__ -p ${{ secrets.PYPI_TOKEN_SGLANG_KERNEL }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: wheel-python${{ matrix.python-version }}-cuda${{ matrix.cuda-version }}${{ matrix.arch == 'aarch64' && '-aarch64' || '' }}
path: sgl-kernel/dist/*
release-cu130:
needs: build-cu130-matrix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || '' }}
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-*
- name: Set tag name
id: set_tag_name
run: |
if [ -z "${{ inputs.tag_name }}" ]; then
TAG_NAME="v$(cat sgl-kernel/python/sgl_kernel/version.py | cut -d'"' -f2)"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
else
echo "tag_name=${{ inputs.tag_name }}" >> $GITHUB_OUTPUT
fi
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.set_tag_name.outputs.tag_name }}
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
files: |
sgl-kernel/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_kernel_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 whl index"
git push
build-rocm-matrix:
if: |
github.repository == 'sgl-project/sglang' &&
(github.event_name == 'push' || github.event.inputs.target == 'all' || github.event.inputs.target == 'rocm700' || github.event.inputs.target == 'rocm720')
runs-on: amd-docker-scale
strategy:
matrix:
python-version: ["3.10"]
rocm-version: ["700", "720"]
steps:
# Self-hosted build nodes retain the workspace across jobs. Prior builds
# leave root-owned artifacts under 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
- uses: actions/checkout@v4
with:
submodules: "recursive"
ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || '' }}
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Build wheels
run: |
cp 3rdparty/amd/wheel/sgl-kernel/* sgl-kernel/
cd sgl-kernel
chmod +x ./build_rocm.sh
./build_rocm.sh "${{ matrix.rocm-version }}"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: wheel-python${{ matrix.python-version }}-rocm${{ matrix.rocm-version }}
path: sgl-kernel/dist/*
release-rocm700:
needs: build-rocm-matrix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || '' }}
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-*-rocm700
- name: Set tag name
id: set_tag_name
run: |
if [ -z "${{ inputs.tag_name }}" ]; then
TAG_NAME="v$(cat sgl-kernel/python/sgl_kernel/version.py | cut -d'"' -f2)"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
else
echo "tag_name=${{ inputs.tag_name }}" >> $GITHUB_OUTPUT
fi
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.set_tag_name.outputs.tag_name }}
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
files: |
sgl-kernel/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_kernel_whl_index.py --rocm 700
- 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 whl index"
git push
release-rocm720:
needs: build-rocm-matrix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || '' }}
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-*-rocm720
- name: Set tag name
id: set_tag_name
run: |
if [ -z "${{ inputs.tag_name }}" ]; then
TAG_NAME="v$(cat sgl-kernel/python/sgl_kernel/version.py | cut -d'"' -f2)"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
else
echo "tag_name=${{ inputs.tag_name }}" >> $GITHUB_OUTPUT
fi
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.set_tag_name.outputs.tag_name }}
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
files: |
sgl-kernel/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_kernel_whl_index.py --rocm 720
- 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 whl index"
git push
build-musa43:
if: |
github.repository == 'sgl-project/sglang' &&
(github.event_name == 'push' || github.event.inputs.target == 'all' || github.event.inputs.target == 'musa43')
runs-on: kernel-build-node-musa
strategy:
matrix:
python-version: ["3.10"]
musa-version: ["43"]
steps:
# Self-hosted build nodes retain the workspace across jobs. Prior builds
# leave root-owned artifacts under 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
- uses: actions/checkout@v4
with:
submodules: "recursive"
- name: Clean stale torchada extension locks
run: |
if ps -eo comm=,args= | grep -E '(^|[[:space:]])(mcc|ninja)([[:space:]]|$)|torchada_cpp_ops' | grep -v grep; then
echo "::error::Active torchada extension build detected; refusing to remove lock files"
exit 1
fi
find "${HOME}/.cache/torch_extensions" \
-path '*/torchada_cpp_ops/lock' \
-type f \
-print \
-delete 2>/dev/null || true
- name: Build wheels
run: |
cd sgl-kernel
mv pyproject_musa.toml pyproject.toml
python setup_musa.py sdist bdist_wheel
- name: Rename MUSA wheels
run: |
bash scripts/ci/musa/rename_wheels_musa.sh ${{ matrix.musa-version }} sgl-kernel/dist
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: wheel-python${{ matrix.python-version }}-musa${{ matrix.musa-version }}
path: sgl-kernel/dist/*
release-musa43:
needs: build-musa43
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: sgl-kernel/dist/
merge-multiple: true
pattern: wheel-*
- name: Set tag name
id: set_tag_name
run: |
if [ -z "${{ inputs.tag_name }}" ]; then
TAG_NAME="v$(cat sgl-kernel/python/sgl_kernel/version.py | cut -d'"' -f2)"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
else
echo "tag_name=${{ inputs.tag_name }}" >> $GITHUB_OUTPUT
fi
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.set_tag_name.outputs.tag_name }}
repository: sgl-project/whl
token: ${{ secrets.GH_PAT_FOR_WHL_RELEASE }}
files: |
sgl-kernel/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_kernel_whl_index.py --musa 43
- 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 whl index"
git push
+349
View File
@@ -0,0 +1,349 @@
name: Rerun Test
run-name: ${{ inputs.pr_head_sha && format('[rerun-test] {0} {1}', inputs.test_command, inputs.pr_head_sha) || format('[rerun-test] {0}', inputs.test_command) }}
on:
workflow_dispatch:
inputs:
mode:
description: "Dispatch mode: cuda | multimodal_gen | cpu"
required: true
type: choice
options:
- cuda
- multimodal_gen
- cpu
test_command:
description: "Test command(s) to run, one per line (e.g. 'registered/core/test_srt_endpoint.py TestSRTEndpoint.test_simple_decode')"
required: true
type: string
runs_on:
description: "GHA runner label (cuda/multimodal_gen only; ignored for cpu)"
required: false
type: string
default: ""
install_script:
description: "Install script path (cuda only). E.g. scripts/ci/cuda/ci_install_dependency.sh"
required: false
type: string
default: ""
install_timeout:
description: "Install-step timeout minutes (cuda only)"
required: false
type: string
default: "20"
grace_blackwell:
description: "Set GRACE_BLACKWELL for the install step (cuda only)"
required: false
type: string
default: "0"
rdma_devices:
description: "SGLANG_CI_RDMA_ALL_DEVICES csv (cuda only; empty = unset)"
required: false
type: string
default: ""
pr_head_sha:
description: "PR head SHA to checkout (for /rerun-test on fork PRs)"
required: false
type: string
default: ""
reply_comment_id:
description: "Reply comment ID to write back result to"
required: false
type: string
default: ""
reply_marker:
description: "Per-batch marker for locating the line in reply comment"
required: false
type: string
default: ""
env:
SGLANG_IS_IN_CI: true
SGLANG_ENABLE_ASYNC_ASSERT: true
SGLANG_CUDA_COREDUMP: "1"
SGLANG_JIT_DEEPGEMM_FAST_WARMUP: true
# TEMP: rebuild deepep against the new torch for torch-211-merge PR only — revert before merging to main.
FORCE_REBUILD_DEEPEP: '1'
permissions:
actions: write
contents: read
issues: read
jobs:
rerun-test-cuda:
if: inputs.mode == 'cuda'
runs-on: ${{ inputs.runs_on }}
timeout-minutes: 120
permissions:
contents: read
issues: write
env:
RUNNER_LABELS: ${{ inputs.runs_on }}
SGLANG_CI_RDMA_ALL_DEVICES: ${{ inputs.rdma_devices }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.pr_head_sha || github.sha }}
- name: Mark runner picked up
if: inputs.reply_comment_id != '' && inputs.reply_marker != ''
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "${{ inputs.runs_on }}" == "1-gpu-5090" ]]; then
source /etc/profile.d/sglang-ci.sh
fi
python3 scripts/ci/utils/update_rerun_test_status.py \
--comment-id "${{ inputs.reply_comment_id }}" \
--marker "${{ inputs.reply_marker }}" \
--status running \
--repo "${{ github.repository }}"
- uses: ./.github/actions/check-maintenance
- name: Install dependencies
timeout-minutes: ${{ fromJson(inputs.install_timeout) }}
env:
GRACE_BLACKWELL: ${{ inputs.grace_blackwell }}
run: |
if [[ "${{ inputs.runs_on }}" == "1-gpu-5090" ]]; then
source /etc/profile.d/sglang-ci.sh
fi
# Fail fast on an empty install_script. `bash <empty>` is a silent
# no-op that "succeeds" without installing sglang, so the test step
# then dies with `ModuleNotFoundError: No module named 'sglang'`.
# /rerun-test resolves install_script from runner_configs.yml; a manual
# workflow_dispatch must pass it explicitly (it can't be derived from
# runs_on, which is shared across configs, e.g. default vs deepep).
if [ -z "${{ inputs.install_script }}" ]; then
echo "::error::install_script is required for cuda mode (empty would silently skip installing sglang). Pass e.g. scripts/ci/cuda/ci_install_dependency.sh"
exit 1
fi
bash ${{ inputs.install_script }}
- name: Run test
timeout-minutes: 60
run: |
if [[ "${{ inputs.runs_on }}" == "1-gpu-5090" ]]; then
source /etc/profile.d/sglang-ci.sh
fi
cmds=()
while IFS= read -r cmd; do
[ -z "$cmd" ] && continue
cmds+=("$cmd")
done <<< "${{ inputs.test_command }}"
total=${#cmds[@]}
suite_start=$SECONDS
for idx in "${!cmds[@]}"; do
i=$((idx + 1))
cmd="${cmds[$idx]}"
echo ""
echo "."
echo "Begin ($i/$total): python3 $cmd"
echo "."
file_start=$SECONDS
# base-a perf tests (fwd_occupancy) need async assert off; match
# _pr-test-stage.yml's base-a carve-out.
test_file="${cmd%% *}"
test_file="${test_file%%::*}"
async_assert=true
if grep -qE 'register_cuda_ci\(.*stage[[:space:]]*=[[:space:]]*"base-a"' "test/$test_file" 2>/dev/null; then
async_assert=false
fi
(cd test/ && SGLANG_ENABLE_ASYNC_ASSERT=$async_assert python3 $cmd -f) || exit 1
elapsed=$(( SECONDS - file_start ))
echo "."
echo "End ($i/$total): elapsed=${elapsed}s"
echo "."
echo ""
done
total_elapsed=$(( SECONDS - suite_start ))
echo "All $total test(s) passed in ${total_elapsed}s"
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
rerun-test-multimodal-gen:
if: inputs.mode == 'multimodal_gen'
runs-on: ${{ inputs.runs_on }}
timeout-minutes: 120
permissions:
contents: read
issues: write
env:
RUNNER_LABELS: ${{ inputs.runs_on }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.pr_head_sha || github.sha }}
- name: Mark runner picked up
if: inputs.reply_comment_id != '' && inputs.reply_marker != ''
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python3 scripts/ci/utils/update_rerun_test_status.py \
--comment-id "${{ inputs.reply_comment_id }}" \
--marker "${{ inputs.reply_marker }}" \
--status running \
--repo "${{ github.repository }}"
- uses: ./.github/actions/check-maintenance
- name: Install dependencies (diffusion)
timeout-minutes: 20
run: bash scripts/ci/cuda/ci_install_dependency.sh diffusion
- name: Run test
timeout-minutes: 60
run: |
cmds=()
while IFS= read -r cmd; do
[ -z "$cmd" ] && continue
cmds+=("$cmd")
done <<< "${{ inputs.test_command }}"
total=${#cmds[@]}
suite_start=$SECONDS
for idx in "${!cmds[@]}"; do
i=$((idx + 1))
cmd="${cmds[$idx]}"
echo ""
echo "."
echo "Begin ($i/$total): python3 -m pytest $cmd -x"
echo "."
file_start=$SECONDS
python3 -m pytest $cmd -x || exit 1
elapsed=$(( SECONDS - file_start ))
echo "."
echo "End ($i/$total): elapsed=${elapsed}s"
echo "."
echo ""
done
total_elapsed=$(( SECONDS - suite_start ))
echo "All $total test(s) passed in ${total_elapsed}s"
- uses: ./.github/actions/upload-cuda-coredumps
if: failure()
rerun-test-cpu:
if: inputs.mode == 'cpu'
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
contents: read
issues: write
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.pr_head_sha || github.sha }}
- name: Mark runner picked up
if: inputs.reply_comment_id != '' && inputs.reply_marker != ''
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python3 scripts/ci/utils/update_rerun_test_status.py \
--comment-id "${{ inputs.reply_comment_id }}" \
--marker "${{ inputs.reply_marker }}" \
--status running \
--repo "${{ github.repository }}"
- 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: Install dependencies
timeout-minutes: 20
env:
UV_SYSTEM_PYTHON: "1"
run: |
uv pip install -e "python[dev]" --index-strategy unsafe-best-match --prerelease allow
- name: Run test
timeout-minutes: 60
run: |
cd test/
cmds=()
while IFS= read -r cmd; do
[ -z "$cmd" ] && continue
cmds+=("$cmd")
done <<< "${{ inputs.test_command }}"
total=${#cmds[@]}
suite_start=$SECONDS
for idx in "${!cmds[@]}"; do
i=$((idx + 1))
cmd="${cmds[$idx]}"
echo ""
echo "."
echo "Begin ($i/$total): python3 $cmd"
echo "."
file_start=$SECONDS
python3 $cmd -f || exit 1
elapsed=$(( SECONDS - file_start ))
echo "."
echo "End ($i/$total): elapsed=${elapsed}s"
echo "."
echo ""
done
total_elapsed=$(( SECONDS - suite_start ))
echo "All $total test(s) passed in ${total_elapsed}s"
write-back-result:
needs: [rerun-test-cuda, rerun-test-multimodal-gen, rerun-test-cpu]
if: always() && inputs.reply_comment_id != '' && inputs.reply_marker != ''
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
concurrency:
group: rerun-test-writeback-${{ inputs.reply_comment_id }}
cancel-in-progress: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Write back result to reply comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "${{ needs.rerun-test-cuda.result }}" == "success" \
|| "${{ needs.rerun-test-multimodal-gen.result }}" == "success" \
|| "${{ needs.rerun-test-cpu.result }}" == "success" ]]; then
STATUS=success
else
STATUS=failure
fi
python3 scripts/ci/utils/update_rerun_test_status.py \
--comment-id "${{ inputs.reply_comment_id }}" \
--marker "${{ inputs.reply_marker }}" \
--status "$STATUS" \
--repo "${{ github.repository }}"
+30
View File
@@ -0,0 +1,30 @@
name: Retag Docker Image
on:
workflow_dispatch:
inputs:
source_tag:
description: "Existing image tag (e.g., v0.4.7-cu129-amd64)"
required: true
target_tag:
description: "New tag to apply (e.g., latest)"
required: true
jobs:
retag:
if: github.repository == 'sgl-project/sglang'
runs-on: ubuntu-22.04
environment: "prod"
steps:
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Retag image
run: |
echo "Retagging lmsysorg/sglang:${{ inputs.source_tag }} -> lmsysorg/sglang:${{ inputs.target_tag }}"
docker buildx imagetools create \
-t lmsysorg/sglang:${{ inputs.target_tag }} \
lmsysorg/sglang:${{ inputs.source_tag }}
+68
View File
@@ -0,0 +1,68 @@
name: Runner Utilization Report
on:
schedule:
- cron: '0 8 * * *' # Daily at 8 AM UTC
pull_request:
paths:
- '.github/workflows/runner-utilization.yml'
- 'scripts/ci/utils/runner_utilization_report.py'
workflow_dispatch:
inputs:
hours:
description: 'Time window in hours'
required: false
default: '24'
type: string
filter:
description: 'Filter runner labels (e.g., 5090, h200)'
required: false
type: string
# Sandbox PRs (e.g. tom/kv_canary) push dozens of times in a row. Without this
# group, every push spawns another 24h API scan that races the previous ones
# and drains the shared 15k/hr installation token quota. Cancel-in-progress
# keeps at most one Runner Utilization run per ref alive at a time.
concurrency:
group: runner-utilization-${{ github.ref }}
cancel-in-progress: true
jobs:
report:
name: Generate Report
# Skip fork PRs entirely, and require the `run-ci` label on same-repo PRs
# so unlabeled iteration pushes (e.g. long-lived sandbox branches) don't
# fire the 24h API scan. 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
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Unit-test report logic
# Pure-logic tests (stdlib only, no API calls) — guards the
# queue-time accounting and status/link rendering before the
# (API-heavy) report runs.
run: |
python -m unittest discover -s scripts/ci/utils \
-p 'test_runner_utilization_report.py' -v
- name: Generate Utilization Report
timeout-minutes: 30
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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/runner_utilization_report.py \
--repo ${{ github.repository }} \
--hours ${{ (github.event_name == 'pull_request' && '0.34') || inputs.hours || '24' }} \
${{ inputs.filter && format('--filter {0}', inputs.filter) || '' }}
+100
View File
@@ -0,0 +1,100 @@
name: Slash Command Handler
on:
issue_comment:
types: [created, edited]
permissions:
contents: read
pull-requests: write # Required to add labels and reactions
actions: write # Required to rerun workflows
issues: write # Required for comment reactions in some contexts
jobs:
slash_command:
# Only run if it is a PR and the comment contains a recognized command
# Use contains() since startsWith() can't handle leading whitespace/newlines
if: >
github.event.issue.pull_request &&
(contains(github.event.comment.body, '/tag-run-ci-label') ||
contains(github.event.comment.body, '/rerun-failed-ci') ||
contains(github.event.comment.body, '/tag-and-rerun-ci') ||
contains(github.event.comment.body, '/rerun-stage') ||
contains(github.event.comment.body, '/rerun-group') ||
contains(github.event.comment.body, '/rerun-test'))
runs-on: ubuntu-latest
steps:
# SECURITY: This workflow runs on issue_comment trigger with elevated permissions
# (pull-requests: write, actions: write). For non-fork PRs, we can safely checkout
# the PR branch to allow testing changes to this handler. For fork PRs, we MUST
# stay on main to prevent untrusted code execution with these elevated permissions.
- name: Get PR details
id: pr
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_DATA=$(gh pr view ${{ github.event.issue.number }} --repo ${{ github.repository }} --json headRefName,headRepositoryOwner) || {
echo "::error::Failed to fetch PR data"
exit 1
}
# Use 'empty' filter to handle null/missing values (e.g., deleted forks)
HEAD_OWNER=$(echo "$PR_DATA" | jq -r '.headRepositoryOwner.login // empty')
REPO_OWNER="${{ github.repository_owner }}"
# Treat missing/null owner as fork for security (fail-safe)
if [[ -z "$HEAD_OWNER" || "$HEAD_OWNER" != "$REPO_OWNER" ]]; then
IS_FORK="true"
else
IS_FORK="false"
fi
echo "is_fork=$IS_FORK" >> $GITHUB_OUTPUT
echo "ref=$(echo "$PR_DATA" | jq -r '.headRefName')" >> $GITHUB_OUTPUT
echo "pr_ref=refs/pull/${{ github.event.issue.number }}/head" >> $GITHUB_OUTPUT
echo "PR owner: $HEAD_OWNER, Repo owner: $REPO_OWNER, Is fork: $IS_FORK"
- name: Check commenter permission for fork PRs
id: perm
if: steps.pr.outputs.is_fork == 'true'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PERM=$(gh api repos/${{ github.repository }}/collaborators/${{ github.event.comment.user.login }}/permission --jq '.permission') || {
PERM="none"
echo "::warning::Failed to check commenter permission, defaulting to none"
}
if [[ "$PERM" == "admin" || "$PERM" == "maintain" || "$PERM" == "write" ]]; then
echo "safe_to_checkout_pr=true" >> $GITHUB_OUTPUT
else
echo "safe_to_checkout_pr=false" >> $GITHUB_OUTPUT
fi
echo "Commenter ${{ github.event.comment.user.login }} permission: $PERM"
- name: Checkout code
uses: actions/checkout@v4
with:
# For non-fork PRs: checkout PR branch by name
# For fork PRs with trusted commenter: checkout via refs/pull/N/head
# For fork PRs with untrusted commenter: stay on main for security
ref: ${{ steps.pr.outputs.is_fork == 'false' && steps.pr.outputs.ref || (steps.perm.outputs.safe_to_checkout_pr == 'true' && steps.pr.outputs.pr_ref || '') }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install PyGithub PyYAML
- name: Handle Slash Command
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO_FULL_NAME: ${{ github.repository }}
PR_NUMBER: ${{ github.event.issue.number }}
COMMENT_ID: ${{ github.event.comment.id }}
COMMENT_BODY: ${{ github.event.comment.body }}
USER_LOGIN: ${{ github.event.comment.user.login }}
run: |
python scripts/ci/utils/slash_command_handler.py
+44
View File
@@ -0,0 +1,44 @@
name: Stress Test
on:
workflow_dispatch:
inputs:
num_prompts:
description: 'Number of prompts per model'
required: true
default: '50000'
type: string
duration_minutes:
description: 'Timeout per model in minutes'
required: true
default: '45'
type: string
jobs:
stress-test:
if: github.repository == 'sgl-project/sglang'
runs-on: 8-gpu-h200
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run stress tests
timeout-minutes: 210
env:
NUM_PROMPTS: ${{ inputs.num_prompts }}
DURATION_MINUTES: ${{ inputs.duration_minutes }}
run: |
cd test
python3 run_suite.py --hw cuda --suite stress
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: stress-test-results
path: |
stress_test_*.jsonl
@@ -0,0 +1,60 @@
name: Sync LMSYS SGLang blogs
on:
workflow_dispatch:
schedule:
- cron: "0 */12 * * *"
permissions:
contents: write
pull-requests: write
jobs:
sync:
if: github.repository == 'sgl-project/sglang' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Sync blog cards
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python docs_new/scripts/update_lmsys_sglang_blogs.py
- name: Open or update pull request
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
set -euo pipefail
if git diff --quiet -- docs_new/index.mdx; then
echo "No blog card changes; nothing to do."
exit 0
fi
BRANCH="bot/lmsys-blog-cards"
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
# Recreate the bot branch as "current main + card diff" and overwrite
# any previous state, so the PR always shows a clean single-commit diff.
git checkout -B "$BRANCH"
git add docs_new/index.mdx
git commit -m "docs: sync LMSYS SGLang blog cards"
git push --force origin "$BRANCH"
# Open a PR only if one is not already open for this branch; otherwise
# the force-push above has already updated the existing PR in place.
if [ -z "$(gh pr list --head "$BRANCH" --base main --state open --json number --jq '.[].number')" ]; then
gh pr create \
--base main \
--head "$BRANCH" \
--title "docs: sync LMSYS SGLang blog cards" \
--body $'Automated sync of the LMSYS SGLang blog cards in `docs_new/index.mdx`.\n\n🤖 Generated with GitHub Actions'
fi
+88
View File
@@ -0,0 +1,88 @@
name: Trivy Scan Dev Docker Images
on:
# Run daily after nightly dev builds (which run at midnight UTC)
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
inputs:
tag:
description: "Image tag to scan (e.g., dev, dev-cu13, latest)"
required: false
default: ""
jobs:
scan:
if: github.repository == 'sgl-project/sglang'
runs-on: x64-docker-build-node
timeout-minutes: 45
permissions:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
tag: ${{ inputs.tag && fromJSON(format('["{0}"]', inputs.tag)) || fromJSON('["dev", "dev-cu12"]') }}
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
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@v0.35.0
with:
image-ref: 'docker.io/lmsysorg/sglang:${{ matrix.tag }}'
scanners: 'vuln'
format: 'sarif'
output: 'trivy-results-${{ matrix.tag }}.sarif'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
skip-dirs: 'usr/local/go,opt/nvidia'
- name: Upload Trivy scan results to GitHub Security
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles(format('trivy-results-{0}.sarif', matrix.tag)) != ''
with:
sarif_file: 'trivy-results-${{ matrix.tag }}.sarif'
category: 'trivy-${{ matrix.tag }}'
- name: Run Trivy (table output for logs)
if: success()
uses: aquasecurity/trivy-action@v0.35.0
with:
image-ref: 'docker.io/lmsysorg/sglang:${{ matrix.tag }}'
scanners: 'vuln'
format: 'table'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
skip-dirs: 'usr/local/go,opt/nvidia'
- name: Scan summary
if: always()
run: |
IMAGE="docker.io/lmsysorg/sglang:${{ matrix.tag }}"
SARIF="trivy-results-${{ matrix.tag }}.sarif"
echo "## Trivy Scan: \`${{ matrix.tag }}\`" >> "$GITHUB_STEP_SUMMARY"
if [ ! -f "${SARIF}" ]; then
echo "**Status:** Scan failed — no SARIF output produced" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
VULN_COUNT=$(python3 -c "
import json
data = json.load(open('${SARIF}'))
print(sum(len(run.get('results', [])) for run in data.get('runs', [])))
")
echo "- **Image**: \`${IMAGE}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- **Findings**: ${VULN_COUNT}" >> "$GITHUB_STEP_SUMMARY"
if [ "${VULN_COUNT}" = "0" ]; then
echo "- **Result**: No CRITICAL/HIGH unfixed vulnerabilities found" >> "$GITHUB_STEP_SUMMARY"
else
echo "- **Result**: Found ${VULN_COUNT} finding(s) — check the Security tab for details" >> "$GITHUB_STEP_SUMMARY"
fi
+50
View File
@@ -0,0 +1,50 @@
name: Weekly Test (Nvidia)
on:
schedule:
- cron: '0 0 * * 0' # Run every Sunday at midnight UTC
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'
- 'weekly-test-8-gpu-h200'
concurrency:
group: weekly-test-nvidia-${{ github.ref }}
cancel-in-progress: true
env:
SGLANG_IS_IN_CI: true
SGLANG_ENABLE_ASYNC_ASSERT: true
HF_HUB_DOWNLOAD_TIMEOUT: 300
HF_HUB_ETAG_TIMEOUT: 300
jobs:
# Weekly tests - 8 GPU H200
weekly-test-8-gpu-h200:
if: github.repository == 'sgl-project/sglang' && (inputs.job_filter == '' || inputs.job_filter == 'all' || inputs.job_filter == 'weekly-test-8-gpu-h200')
runs-on: 8-gpu-h200
timeout-minutes: 120
env:
RUNNER_LABELS: 8-gpu-h200
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
bash scripts/ci/cuda/ci_install_dependency.sh
- name: Run weekly 8-GPU H200 tests
timeout-minutes: 120
env:
GPU_CONFIG: "8-gpu-h200"
IS_H200: "1"
run: |
cd test
python3 run_suite.py --hw cuda --suite weekly-8-gpu-h200 --nightly --continue-on-error --timeout-per-file 7200
@@ -0,0 +1,80 @@
name: Weekly Update Est Time
on:
schedule:
- cron: '0 0 * * 1' # Monday 00:00 UTC
workflow_dispatch: {}
permissions:
contents: write
pull-requests: write
jobs:
update-est-time:
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: Update est_time values
env:
MODEL_URL: "https://raw.githubusercontent.com/sgl-project/sglang-ci-stats/main/model.json"
run: |
python scripts/ci/update_est_time.py \
--model-url "$MODEL_URL" \
--summary-file /tmp/est_time_summary.md
- name: Check for changes
id: changes
run: |
if git diff --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No est_time changes detected"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "Est_time changes detected:"
git diff --stat
fi
- name: Create PR
if: steps.changes.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GH_PAT_FOR_PULL_REQUEST }}
run: |
git config user.name "sglang-bot"
git config user.email "sglang-bot@users.noreply.github.com"
BRANCH_NAME="bot/update-est-time-$(date +%Y%m%d)"
git checkout -b "$BRANCH_NAME"
git add -A
git commit -m "chore: update CI test est_time from recent run data"
git push origin "$BRANCH_NAME"
{
echo "## Summary"
echo
echo "Refreshes \`est_time\` literals from [\`sgl-project/sglang-ci-stats\`](https://github.com/sgl-project/sglang-ci-stats)'s \`model.json\` (per-(suite, file) p90 over recent successful CI runs on \`main\`)."
echo
echo "This keeps the LPT load-balancing algorithm accurate for partitioning tests across parallel CI jobs, and serves as the static fallback when \`compute_partitions\` cannot fetch the live model at PR time."
echo
if [ -f /tmp/est_time_summary.md ]; then
cat /tmp/est_time_summary.md
echo
fi
echo "🤖 Generated with GitHub Actions"
} > /tmp/pr_body.md
gh pr create \
--title "chore: update CI test est_time values" \
--body-file /tmp/pr_body.md \
--base main \
--head "$BRANCH_NAME"
+256
View File
@@ -0,0 +1,256 @@
name: XPU CI Job Monitor
on:
schedule:
- cron: '0 0 * * *' # Daily at midnight UTC
pull_request:
paths:
- '.github/workflows/xpu-ci-job-monitor.yml'
- 'scripts/ci/utils/xpu_job_monitor.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 XPU 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: xpu-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-xpu.yml" >> "$GITHUB_OUTPUT"
else
echo "workflows=pr-test-xpu.yml,nightly-test-intel.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/xpu_job_monitor.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/xpu_job_monitor.py \
--repo ${{ github.repository }} \
--job "${{ inputs.job_filter }}" \
--workflow "pr-test-xpu.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 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Parse workflow files
id: parse
run: |
# Parse pr-test-xpu.yml and extract job names (exclude utility jobs)
# Excluded: check-changes, pr-gate, wait-for-stage-a, finish
pr_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/pr-test-xpu.yml | \
grep -v -E '^(check-changes|pr-gate|wait-for-stage-a|finish)$' | \
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-intel.yml and extract job names (exclude utility jobs)
# Excluded: check-all-jobs
nightly_jobs=$(yq -r '.jobs | keys | .[]' .github/workflows/nightly-test-intel.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"
# 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/xpu_job_monitor.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "pr-test-xpu.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary
# Nightly XPU 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/xpu_job_monitor.py \
--repo ${{ github.repository }} \
--job "${{ matrix.job_name }}" \
--workflow "nightly-test-intel.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/xpu_job_monitor.py \
--repo ${{ github.repository }} \
--runner-report \
--workflow "pr-test-xpu.yml,nightly-test-intel.yml" \
--hours ${{ inputs.hours || '24' }} \
--input-data-file ci-data/actions-job-snapshot.json \
--summary