[CI] Build the Rust extension modules once per run instead of in every CUDA job (#33384)

This commit is contained in:
Liangsheng Yin
2026-08-03 18:56:48 -07:00
committed by GitHub
parent 572924634b
commit cdff33d738
9 changed files with 567 additions and 28 deletions
+81 -19
View File
@@ -208,8 +208,31 @@ jobs:
skip_pr_test_health_check: ${{ inputs.skip_pr_test_health_check == true }}
secrets: inherit
# =============================================== Rust extensions ====================================================
# Unlike sgl-kernel, not gated on source changes: the cache key is the source
# hash, so the job always runs but only compiles on a miss.
rust-ext-build:
needs: [check-changes, call-gate]
# Ungated on the event, unlike the jobs above: a cache entry is visible to
# every PR only if it was written from the default branch's ref, so every
# main-ref run - schedule, dispatch, workflow_call - has to seed it.
#
# !cancelled() so a skipped call-gate does not take this job with it, since a
# docs-only PR still installs sglang in every stage.
if: |
!cancelled() &&
needs.check-changes.result == 'success' &&
(needs.call-gate.result == 'success' || needs.call-gate.result == 'skipped')
uses: ./.github/workflows/_pr-test-rust-ext-build.yml
with:
runs_on: x64-kernel-build-node
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]
needs: [check-changes, call-gate, sgl-kernel-build-wheels, rust-ext-build]
if: |
github.event_name != 'schedule' &&
inputs.test_parallel_dispatch != true &&
@@ -221,12 +244,13 @@ jobs:
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 }}
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
# =============================================== jit-kernel ====================================================
call-jit-kernel-tests:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, sgl-kernel-build-wheels, rust-ext-build]
# 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 kernels/**.
# check-changes already forces jit_kernel='true' on scheduled runs (run_all_tests).
@@ -246,13 +270,14 @@ jobs:
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 }}
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
# =============================================== primary ====================================================
# Runs on 5090 (32GB, SM120)
base-a-test-1-gpu-small:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -262,10 +287,11 @@ jobs:
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '10'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-a-test-cpu:
needs: [check-changes, call-gate]
needs: [check-changes, call-gate, rust-ext-build]
if: |
always() &&
needs.check-changes.result == 'success' &&
@@ -303,13 +329,29 @@ jobs:
- 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.
# This stage compiled the workspace too - 7+ minutes per partition on
# billable hosted minutes. rust-ext-build's modules need an older glibc than
# this runner has, which is the safe direction, and both pin Python 3.10.
- name: Download prebuilt Rust extensions
id: rust_ext
if: ${{ needs.rust-ext-build.outputs.artifact_name != '' }}
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: ${{ needs.rust-ext-build.outputs.artifact_name }}
path: python/sglang/srt/
# Both only serve the fallback where this stage compiles the extensions
# itself, so they follow the download's outcome, not the job output: an
# expired artifact still needs cargo and a warm target dir here. Otherwise
# rust-cache restores ~1 GB per partition for nothing, on an over-quota cache.
- name: Install protoc + Rust toolchain
if: ${{ steps.rust_ext.outcome != 'success' }}
timeout-minutes: 10
run: bash scripts/ci/utils/install_rust_protoc.sh
- name: Rust cache (rust/ workspace)
if: ${{ steps.rust_ext.outcome != 'success' }}
uses: Swatinem/rust-cache@v2
with:
workspaces: rust
@@ -321,6 +363,7 @@ jobs:
timeout-minutes: 20
env:
UV_SYSTEM_PYTHON: "1"
SGLANG_BUILD_RUST_EXTS: ${{ steps.rust_ext.outcome == 'success' && 'none' || '' }}
run: |
uv pip install -e "python[dev]" --index-strategy unsafe-best-match --prerelease allow
@@ -357,7 +400,7 @@ jobs:
# Runs on 5090 (32GB, SM120)
base-b-test-1-gpu-small:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -367,11 +410,12 @@ jobs:
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
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]
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -382,10 +426,11 @@ jobs:
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-b-test-2-gpu-large:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -395,10 +440,11 @@ jobs:
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-b-test-4-gpu-b200:
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-a, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -408,10 +454,11 @@ jobs:
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '40'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
call-multimodal-gen-tests:
needs: [check-changes, call-gate, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, sgl-kernel-build-wheels, rust-ext-build]
if: |
always() &&
!cancelled() &&
@@ -428,10 +475,11 @@ jobs:
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' }}
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-c-test-4-gpu-h100:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -441,10 +489,11 @@ jobs:
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-c-test-8-gpu-h200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -461,10 +510,11 @@ jobs:
# 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'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-c-test-8-gpu-h20:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -474,10 +524,11 @@ jobs:
caller_inputs: ${{ toJson(inputs) }}
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-c-test-deepep-4-gpu-h100:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -489,10 +540,11 @@ jobs:
run_timeout_minutes: '30'
warmup_deep_gemm_models: 'lmsys/sglang-ci-dsv3-test:4'
warmup_server_models: 'lmsys/sglang-ci-dsv3-test:4'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-c-test-deepep-4-gpu-b200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -503,10 +555,11 @@ jobs:
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-c-test-deepep-8-gpu-h200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -517,10 +570,11 @@ jobs:
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-c-test-4-gpu-b200:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -531,10 +585,11 @@ jobs:
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
rust_ext_artifact: ${{ needs.rust-ext-build.outputs.artifact_name }}
secrets: inherit
base-c-test-4-gpu-gb300:
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels]
needs: [check-changes, call-gate, wait-for-base-b, sgl-kernel-build-wheels, rust-ext-build]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_pr-test-stage.yml
with:
@@ -545,8 +600,13 @@ jobs:
partitions: ${{ needs.check-changes.outputs.partitions }}
run_timeout_minutes: '30'
timeout_per_file: '1800'
# aarch64 has no prebuild, so this stage compiles during install - which also
# makes it the only stage still covering that path. Keep it that way.
rust_ext_artifact: ''
secrets: inherit
# List every build and test job: `skipped` passes here, so an omission turns that
# job's failure into a green run with no tests.
pr-test-finish:
needs:
[
@@ -557,6 +617,8 @@ jobs:
sgl-kernel-build-wheels-arm,
call-sgl-kernel-tests,
rust-ext-build,
wait-for-base-a,
wait-for-base-b,