From a1843524a5ccba645b361d9e22ebbc833f1feaea Mon Sep 17 00:00:00 2001 From: Kangyan-Zhou Date: Wed, 20 May 2026 15:20:03 +0800 Subject: [PATCH] ci(sgl-router): add PR test workflow (pre-positioned for feature PR) (#25854) Co-authored-by: Claude Opus 4.7 (1M context) --- .github/workflows/pr-test-sgl-router.yml | 347 +++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 .github/workflows/pr-test-sgl-router.yml diff --git a/.github/workflows/pr-test-sgl-router.yml b/.github/workflows/pr-test-sgl-router.yml new file mode 100644 index 000000000..2e1ec6be6 --- /dev/null +++ b/.github/workflows/pr-test-sgl-router.yml @@ -0,0 +1,347 @@ +name: PR Test (sgl-router) + +on: + push: + branches: [main] + paths: + - "experimental/sgl-router/**" + - ".github/workflows/pr-test-sgl-router.yml" + - "scripts/ci/cuda/ci_install_dependency.sh" + pull_request: + branches: [main] + types: [opened, synchronize, reopened, labeled] + paths: + - "experimental/sgl-router/**" + - ".github/workflows/pr-test-sgl-router.yml" + - "scripts/ci/cuda/ci_install_dependency.sh" + workflow_dispatch: + +concurrency: + group: sgl-router-${{ github.ref }} + cancel-in-progress: true + +env: + SGLANG_IS_IN_CI: true + +jobs: + sgl-router-lint: + name: tier-1 — lint + 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: + # 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-lint + 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" + 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 tokenizer_parity 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. + run: cargo test --release -- --skip tokenizer_parity + + # 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-build-and-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 + timeout-minutes: 15 + steps: + - name: Placeholder + run: | + echo "docker-build-test not implemented yet." + exit 0 + + sgl-router-k8s-integration: + name: tier-3 — k8s integration + needs: sgl-router-build-and-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-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 router logs on failure + if: failure() + run: kubectl -n sgl-router-test logs deploy/sgl-router --tail=200 || true + + sgl-router-e2e: + name: tier-3 — e2e + needs: sgl-router-build-and-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: 2-gpu-h100 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - 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: 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 + + - name: Run e2e + working-directory: experimental/sgl-router + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: python3 -m pytest tests/e2e/ -v -s --tb=short + + - name: Tokenizer parity matrix (uses HF cache populated by e2e) + working-directory: experimental/sgl-router + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + # Runs after pytest so the Qwen3-0.6B tokenizer.json is cached by the + # SGLang worker. Cells without a cached snapshot are skipped; if no + # cells could be checked, the test hard-fails under SGLANG_IS_IN_CI. + run: cargo test --release --test component tokenizer::parity + + sgl-router-finish: + name: finish + needs: + - sgl-router-lint + - sgl-router-build-and-test + - sgl-router-docker-build-test + - sgl-router-k8s-integration + - sgl-router-e2e + # Gate finish on the same label as upstream jobs to avoid false-green on + # unlabeled PRs (skipped needs => success), and fail when any upstream + # job actually failed or was cancelled. + if: | + always() && + !contains(needs.*.result, 'failure') && + !contains(needs.*.result, 'cancelled') && + ( + 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 + steps: + - name: All required checks completed + run: echo "OK"