fix(gui): allow deleting and closing custom models #3196
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Validate New vLLM Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| lite: | |
| description: 'Run in lite mode (smallest model only)' | |
| type: boolean | |
| default: false | |
| pull_request: | |
| types: [opened, synchronize, reopened, labeled] | |
| merge_group: | |
| schedule: | |
| # Sunday at 6:00 PM UTC | |
| - cron: "0 18 * * 0" | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'pull_request' && 'pr' || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| LEMONADE_CI_MODE: "True" | |
| PYTHONIOENCODING: utf-8 | |
| HF_TOKEN: ${{ secrets.HUGGINGFACE_ACCESS_TOKEN }} | |
| LITE_MODE: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' || (github.event_name == 'workflow_dispatch' && inputs.lite == true) }} | |
| jobs: | |
| # ======================================================================== | |
| # Discover the latest vllm-rocm release | |
| # ======================================================================== | |
| get-latest-releases: | |
| name: Get latest releases | |
| # Only the auto-update runs resolve upstream releases. Everywhere else the | |
| # build validates the pins committed in backend_versions.json, so a bad | |
| # upstream release can't fail a merge or hide a broken pin. | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| vllm_rocm_release: ${{ steps.vllm_rocm.outputs.release }} | |
| steps: | |
| - name: Get latest vllm-rocm release | |
| id: vllm_rocm | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| RELEASE=$(gh api repos/lemonade-sdk/vllm-rocm/releases/latest --jq '.tag_name') | |
| echo "Latest vllm-rocm release: $RELEASE" | |
| echo "release=$RELEASE" >> "$GITHUB_OUTPUT" | |
| # ======================================================================== | |
| # Build binaries with updated backend version | |
| # ======================================================================== | |
| build: | |
| name: Build (Linux) | |
| needs: get-latest-releases | |
| # Label a PR `ci:upgrades` to opt it back in. | |
| if: (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci:upgrades')) && always() && !failure() && !cancelled() | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| clean: true | |
| fetch-depth: 0 | |
| - name: Update backend_versions.json | |
| if: needs.get-latest-releases.result == 'success' | |
| env: | |
| VLLM_ROCM_RELEASE: ${{ needs.get-latest-releases.outputs.vllm_rocm_release }} | |
| run: | | |
| set -e | |
| python3 - << 'PY' | |
| import json, os | |
| path = "src/cpp/resources/backend_versions.json" | |
| with open(path) as f: | |
| data = json.load(f) | |
| release = os.environ["VLLM_ROCM_RELEASE"] | |
| old = data["vllm"].get("rocm", "") | |
| data["vllm"]["rocm"] = release | |
| with open(path, "w") as f: | |
| json.dump(data, f, indent=2) | |
| f.write("\n") | |
| print(f"vllm.rocm: {old} -> {release}") | |
| PY | |
| - name: Build Lemonade server | |
| run: | | |
| set -e | |
| ./setup.sh | |
| # Skip the web app; CI only exercises the backend. | |
| cmake -DBUILD_WEB_APP=OFF --preset default | |
| cmake --build --preset default | |
| test -f build/lemond || { echo "lemond not found!"; exit 1; } | |
| echo "Build successful!" | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: vllm-build | |
| path: | | |
| build/lemond | |
| build/lemonade | |
| build/resources/ | |
| retention-days: 7 | |
| # ======================================================================== | |
| # Validate backend on self-hosted runner | |
| # ======================================================================== | |
| validate: | |
| name: Validate vllm (rocm) | |
| needs: [get-latest-releases, build] | |
| if: always() && needs.build.result == 'success' | |
| runs-on: [self-hosted, stx-halo, Linux, lemon-prod] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| clean: true | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: vllm-build | |
| path: build | |
| - name: Verify binaries | |
| run: | | |
| set -e | |
| chmod +x build/lemond build/lemonade | |
| ./build/lemond --version | |
| echo "Binaries verified!" | |
| - name: Setup Python and virtual environment | |
| uses: ./.github/actions/setup-venv | |
| with: | |
| venv-name: '.venv' | |
| python-version: '3.10' | |
| requirements-file: 'test/requirements.txt' | |
| - name: Run validation with lemond | |
| env: | |
| HOME: ${{ github.workspace }}/.home | |
| run: | | |
| set -e | |
| mkdir -p "$HOME" | |
| echo "HOME=$HOME" >> "$GITHUB_ENV" | |
| CACHE_DIR="$(pwd)/ci-cache-vllm" | |
| LOGS_DIR="$(pwd)/server-logs-rocm" | |
| mkdir -p "$CACHE_DIR" "$LOGS_DIR" | |
| mkdir -p "$RUNNER_TEMP/xdg-runtime" | |
| chmod 700 "$RUNNER_TEMP/xdg-runtime" | |
| echo "XDG_RUNTIME_DIR=$RUNNER_TEMP/xdg-runtime" >> "$GITHUB_ENV" | |
| export XDG_RUNTIME_DIR="$RUNNER_TEMP/xdg-runtime" | |
| # This validate job runs on a shared self-hosted runner, so the default | |
| # port 13305 may already be held by another job. lemond now refuses to | |
| # bind an in-use port, so start on a job-unique free port and pass it to | |
| # validate_vllm.py and the shutdown call. | |
| pick_free_port() { | |
| if command -v python3 >/dev/null 2>&1; then | |
| python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()' && return 0 | |
| fi | |
| for _ in $(seq 1 50); do | |
| local p=$(( (RANDOM % 20000) + 20000 )) | |
| if ! ss -ltn 2>/dev/null | grep -q ":$p "; then echo "$p"; return 0; fi | |
| done | |
| return 1 | |
| } | |
| LEMOND_PID="" | |
| started=0 | |
| for start_attempt in $(seq 1 5); do | |
| PORT=$(pick_free_port) || { echo "ERROR: could not find a free port"; exit 1; } | |
| echo "Attempt $start_attempt: starting lemond on 127.0.0.1:$PORT..." | |
| ./build/lemond "$CACHE_DIR" --port "$PORT" --host 127.0.0.1 \ | |
| > "$LOGS_DIR/lemond.stdout.log" 2> "$LOGS_DIR/lemond.stderr.log" & | |
| LEMOND_PID=$! | |
| for i in $(seq 1 30); do | |
| if curl -sf "http://127.0.0.1:$PORT/api/v1/health" > /dev/null 2>&1; then | |
| echo "lemond is healthy on port $PORT (PID $LEMOND_PID)" | |
| started=1 | |
| break | |
| fi | |
| if ! kill -0 "$LEMOND_PID" 2>/dev/null; then | |
| echo "lemond (PID $LEMOND_PID) exited prematurely on port $PORT" | |
| cat "$LOGS_DIR/lemond.stderr.log" 2>/dev/null || true | |
| break | |
| fi | |
| sleep 2 | |
| done | |
| [ "$started" = "1" ] && break | |
| echo "Start attempt $start_attempt failed; retrying on a new port..." | |
| kill -9 "$LEMOND_PID" 2>/dev/null || true | |
| sleep 1 | |
| done | |
| if [ "$started" != "1" ]; then | |
| echo "ERROR: lemond failed to start after multiple attempts" | |
| exit 1 | |
| fi | |
| EXIT_CODE=0 | |
| VALIDATION_ARGS=( | |
| test/validate_vllm.py | |
| --backend rocm | |
| --port "$PORT" | |
| --output vllm_validation_rocm.json | |
| --logs-dir "$LOGS_DIR" | |
| ) | |
| if [ "${LITE_MODE}" = "true" ]; then | |
| VALIDATION_ARGS+=(--lite) | |
| fi | |
| .venv/bin/python "${VALIDATION_ARGS[@]}" || EXIT_CODE=$? | |
| # Shut the server down cleanly so logs flush. | |
| curl -sf -X POST "http://127.0.0.1:$PORT/internal/shutdown" || true | |
| sleep 2 | |
| kill $LEMOND_PID 2>/dev/null || true | |
| exit $EXIT_CODE | |
| - name: Upload results | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: validation-results-rocm | |
| path: vllm_validation_rocm.json | |
| retention-days: 30 | |
| - name: Upload server logs | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: server-logs-rocm | |
| path: server-logs-rocm/ | |
| retention-days: 30 | |
| if-no-files-found: ignore | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| pkill -9 -f lemond 2>/dev/null || true | |
| pkill -9 -f "vllm" 2>/dev/null || true | |
| pkill -9 -f "resource_tracker" 2>/dev/null || true | |
| # ======================================================================== | |
| # Create PR if validation passed (schedule or workflow_dispatch only) | |
| # ======================================================================== | |
| create-pr: | |
| name: Create update PR | |
| needs: [get-latest-releases, validate] | |
| runs-on: ubuntu-latest | |
| if: >- | |
| (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && | |
| needs.validate.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Download validation results | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: validation-results-* | |
| merge-multiple: true | |
| - name: Update backend_versions.json | |
| env: | |
| VLLM_ROCM_RELEASE: ${{ needs.get-latest-releases.outputs.vllm_rocm_release }} | |
| run: | | |
| python3 - << 'PY' | |
| import json, os | |
| path = 'src/cpp/resources/backend_versions.json' | |
| with open(path) as f: | |
| data = json.load(f) | |
| rocm = os.environ['VLLM_ROCM_RELEASE'] | |
| data['vllm']['rocm'] = rocm | |
| with open(path, 'w') as f: | |
| json.dump(data, f, indent=2) | |
| f.write('\n') | |
| print(f"Updated: vllm.rocm = {rocm}") | |
| PY | |
| - name: Generate PR body | |
| env: | |
| VLLM_ROCM_RELEASE: ${{ needs.get-latest-releases.outputs.vllm_rocm_release }} | |
| run: | | |
| python3 << 'PYEOF' | |
| import json, os | |
| lines = [] | |
| rocm = os.environ.get("VLLM_ROCM_RELEASE", "unknown") | |
| lines.append("## Auto-update vLLM backend") | |
| lines.append("") | |
| lines.append(f"- **vllm-rocm**: `{rocm}`") | |
| lines.append("") | |
| lines.append("## Validation Results") | |
| lines.append("") | |
| lines.append("| Model | Result | Response | input_tokens | output_tokens | time_to_first_token | tokens_per_second |") | |
| lines.append("|-------|--------|----------|--------------|---------------|---------------------|-------------------|") | |
| results_file = "vllm_validation_rocm.json" | |
| if not os.path.isfile(results_file): | |
| lines.append("| _rocm: no results_ | | | | | | |") | |
| else: | |
| with open(results_file) as f: | |
| results = json.load(f) | |
| for r in results: | |
| status = "PASS" if r["pass"] else "FAIL" | |
| resp = str(r.get("response", ""))[:80].replace("|", "\\|").replace("\n", " ") | |
| ttft = r.get("time_to_first_token", "N/A") | |
| if isinstance(ttft, float): | |
| ttft = f"{ttft:.3f}s" | |
| tps = r.get("tokens_per_second", "N/A") | |
| if isinstance(tps, float): | |
| tps = f"{tps:.1f}" | |
| lines.append( | |
| f"| {r['model']} (rocm) | {status} | {resp} | " | |
| f"{r.get('input_tokens', 'N/A')} | {r.get('output_tokens', 'N/A')} | " | |
| f"{ttft} | {tps} |" | |
| ) | |
| lines.append("") | |
| lines.append("---") | |
| lines.append("*Auto-generated by validate_vllm workflow*") | |
| with open("pr_body.md", "w") as f: | |
| f.write("\n".join(lines)) | |
| PYEOF | |
| - name: Create Pull Request | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VLLM_ROCM_RELEASE: ${{ needs.get-latest-releases.outputs.vllm_rocm_release }} | |
| run: | | |
| BRANCH="auto/vllm-update-${VLLM_ROCM_RELEASE}" | |
| EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "") | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "PR #$EXISTING_PR already exists for branch $BRANCH. Skipping." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| if git diff --quiet src/cpp/resources/backend_versions.json; then | |
| echo "backend_versions.json is unchanged — nothing to update." | |
| exit 0 | |
| fi | |
| git checkout -b "$BRANCH" | |
| git add src/cpp/resources/backend_versions.json | |
| git commit -m "Update vllm-rocm to ${VLLM_ROCM_RELEASE}" | |
| git push origin "$BRANCH" | |
| gh pr create \ | |
| --title "Update vllm-rocm to ${VLLM_ROCM_RELEASE}" \ | |
| --body-file pr_body.md \ | |
| --base main \ | |
| --head "$BRANCH" | |
| # Gate job that ensures: 1. in the merge queue, all jobs in `needs:` ran | |
| # successfully (otherwise the merge is blocked), and 2. these jobs do not need | |
| # to run on ordinary pull request pushes. | |
| validation-gate: | |
| name: vLLM validation | |
| needs: [build, validate] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check gated jobs | |
| env: | |
| NEEDS: ${{ toJSON(needs) }} | |
| run: | | |
| # $NEEDS: {"job-id": {"result": "success|failure|skipped|cancelled"}, ...} | |
| # Fail if any job broke. In the merge queue, also fail if any never ran. | |
| echo "$NEEDS" | |
| broke=$(jq -r 'to_entries[]|select(.value.result=="failure" or .value.result=="cancelled")|.key' <<<"$NEEDS") | |
| if [ -n "$broke" ]; then | |
| echo "FAILED: $broke" | |
| exit 1 | |
| fi | |
| if [ "${{ github.event_name }}" = "merge_group" ]; then | |
| absent=$(jq -r 'to_entries[]|select(.value.result!="success")|.key' <<<"$NEEDS") | |
| if [ -n "$absent" ]; then | |
| echo "DID NOT RUN IN MERGE QUEUE: $absent" | |
| exit 1 | |
| fi | |
| fi |