Repository navigation
ATOM SGLang Benchmark #12
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: ATOM SGLang Benchmark | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.repository }}-${{ github.ref_name }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| on: | |
| schedule: | |
| # Nightly at 23:00 Beijing time (15:00 UTC). MI308 runs Mon-Sat; MI355 tiers run Tue/Thu/Sat. | |
| - cron: '0 15 * * 1,3,5' # Mon/Wed/Fri: MI308 only | |
| - cron: '0 15 * * 2,4' # Tue/Thu: SGLang-OOB P0 + P1 on MI355, plus MI308 | |
| - cron: '0 15 * * 6' # Sat only: SGLang-OOB P0 + P1 + P2 on MI355, plus MI308 | |
| workflow_dispatch: | |
| inputs: | |
| benchmark_suite: | |
| description: "Benchmark suite" | |
| type: choice | |
| options: | |
| - SGLang-OOB | |
| default: SGLang-OOB | |
| model_mi355: | |
| description: "Manual MI355 benchmark model selection" | |
| required: false | |
| type: choice | |
| default: none | |
| options: | |
| - none | |
| - all | |
| - OOB_P0 | |
| - OOB_P1 | |
| - OOB_P2 | |
| - OOB_P0+OOB_P1 | |
| - DeepSeek-V4-Pro TP8 | |
| - DeepSeek-V4-Pro MTP1 TP8 | |
| - DeepSeek-V4-Pro MTP3 TP8 | |
| - DeepSeek-V4-Pro Prefix Cache TP8 | |
| - DeepSeek-V3.2 FP8 TP4 | |
| - DeepSeek-V3.2 FP8 TP4 DP4 EP4 | |
| - DeepSeek-V3.2 FP8 TP8 | |
| - DeepSeek-V3.2 FP8 TP8 DP8 EP8 | |
| - DeepSeek-R1 FP8 TP4 | |
| - DeepSeek-R1 FP8 TP8 | |
| - DeepSeek-R1 FP4 TP4 | |
| - DeepSeek-R1 FP4 TP4 DP4 EP4 | |
| - DeepSeek-R1 FP4 TP8 | |
| - DeepSeek-R1 FP4 TP8 EP8 | |
| - DeepSeek-R1 FP4 TP8 DP8 EP8 | |
| - DeepSeek-R1 FP4 TP8 MTP1 | |
| - DeepSeek-R1 FP4 TP8 MTP3 | |
| - DeepSeek-R1 FP4 MTP3 TP4 DP4 EP4 | |
| - DeepSeek-R1 FP4 MTP3 TP8 DP8 EP8 | |
| - Qwen3.5-397B-A17B-FP8 TP4 | |
| - Qwen3.5-397B-A17B-FP8 TP8 | |
| - Qwen3.8-2.4T-A95B-Quark-MXFP4 TP8 | |
| - Qwen3.8-Flash-Next-FP8 TP2 | |
| - GLM-5.1 FP8 TP8 | |
| - GLM-5.2 FP8 TP4 | |
| - GLM-5.2 FP8 TP4 PCP4 | |
| - GLM-5.2 FP4 TP4 | |
| - Kimi-K3 TP8 | |
| - GLM-5.2 FP4 TP4 MTP3 | |
| - MiniMax-M3-MXFP4 TP4 | |
| - MiniMax-M3-MXFP8 EAGLE3 TP4 MTP3 | |
| model_mi308: | |
| description: "Manual MI308 benchmark model selection" | |
| required: false | |
| type: choice | |
| default: none | |
| options: | |
| - none | |
| - all | |
| - MI308 Qwen3.5-397B-A17B-FP8 TP4 | |
| - MI308 Qwen3.5-397B-A17B-FP8 TP8 | |
| - MI308 Qwen3.5-35B-A3B-FP8 TP1 | |
| - MI308 Qwen3.5-35B-A3B-PTPC-FP8 TP1 | |
| - MI308 Qwen3.8-Flash-Next-FP8 TP2 | |
| - MI308 DeepSeek-V4-Flash | |
| docker_image: | |
| description: "Optional Docker image override." | |
| type: string | |
| default: "" | |
| publish_to_dashboard: | |
| description: "Upload SGLang-OOB results to the ATOM dashboard (off by default for manual runs; schedule always publishes)" | |
| type: boolean | |
| default: false | |
| upload_to_custom_dashboard: | |
| description: "Upload benchmark results to the LLM-Booster dashboard (off by default for manual runs)" | |
| type: boolean | |
| default: false | |
| param_lists: | |
| description: | | |
| "Optional benchmark parameter lists. | |
| Input as a single or multiple sets (comma-separated, semicolon between sets), | |
| format: input_length,output_length,concurrency,random_range_ratio. | |
| Leave empty to use workload_label defaults. | |
| Example (single set): 1024,1024,64,0.8 | |
| Example (multiple sets): 1024,1024,64,0.8;2048,1024,32,0.7" | |
| type: string | |
| default: "" | |
| jobs: | |
| wait-for-scheduled-sglang-image: | |
| name: Wait for scheduled SGLang Docker image | |
| runs-on: ubuntu-latest | |
| outputs: | |
| scheduled_sglang_image: ${{ steps.wait.outputs.scheduled_sglang_image }} | |
| scheduled_sglang_tag: ${{ steps.wait.outputs.scheduled_sglang_tag }} | |
| scheduled_sglang_digest: ${{ steps.wait.outputs.scheduled_sglang_digest }} | |
| scheduled_sglang_resolution: ${{ steps.wait.outputs.scheduled_sglang_resolution }} | |
| steps: | |
| - name: Checkout selected branch | |
| uses: actions/checkout@v6 | |
| - name: Wait for today's SGLang nightly tag | |
| id: wait | |
| env: | |
| DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | |
| DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | |
| WAIT_FOR_SGLANG_IMAGE: ${{ github.event_name == 'schedule' && github.ref_name == 'main' && 'true' || 'false' }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "${WAIT_FOR_SGLANG_IMAGE}" != "true" ]]; then | |
| echo "Not a scheduled main run; skipping SGLang nightly image wait." | |
| { | |
| echo "scheduled_sglang_image=" | |
| echo "scheduled_sglang_tag=" | |
| echo "scheduled_sglang_digest=" | |
| echo "scheduled_sglang_resolution=" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| python3 - <<'PY' | |
| import base64 | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import time | |
| from datetime import datetime, timedelta | |
| from pathlib import Path | |
| from urllib.error import HTTPError, URLError | |
| from urllib.parse import quote, urlencode | |
| from urllib.parse import urljoin | |
| from urllib.request import Request, urlopen | |
| from zoneinfo import ZoneInfo | |
| repository = "rocm/atom-dev" | |
| auth_url = "https://auth.docker.io/token" | |
| registry_url = "https://registry-1.docker.io" | |
| manifest_accept = ", ".join( | |
| ( | |
| "application/vnd.oci.image.index.v1+json", | |
| "application/vnd.docker.distribution.manifest.list.v2+json", | |
| "application/vnd.oci.image.manifest.v1+json", | |
| "application/vnd.docker.distribution.manifest.v2+json", | |
| ) | |
| ) | |
| text = Path(".github/workflows/docker-release.yaml").read_text(encoding="utf-8") | |
| version_match = re.search(r'^\s*SGLANG_VERSION:\s*"([^"]+)"', text, re.MULTILINE) | |
| if not version_match: | |
| raise SystemExit("Failed to read SGLANG_VERSION from .github/workflows/docker-release.yaml") | |
| today_date = datetime.now(ZoneInfo("Asia/Shanghai")).date() | |
| today = today_date.strftime("%Y%m%d") | |
| version = version_match.group(1) | |
| tag = f"sglang-v{version}-nightly_{today}" | |
| nightly_pattern = re.compile(r"^sglang-v(?P<version>.+)-nightly_(?P<date>\d{8})$") | |
| def request(url, *, headers=None): | |
| req = Request(url, headers=headers or {}) | |
| with urlopen(req, timeout=30) as response: | |
| return response.read(), response.headers | |
| def get_token(): | |
| query = urlencode( | |
| { | |
| "service": "registry.docker.io", | |
| "scope": f"repository:{repository}:pull", | |
| } | |
| ) | |
| headers = {"User-Agent": "ATOM SGLang Benchmark Image Gate/1.0"} | |
| username = os.environ.get("DOCKER_USERNAME") | |
| password = os.environ.get("DOCKER_PASSWORD") | |
| if username and password: | |
| creds = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii") | |
| headers["Authorization"] = f"Basic {creds}" | |
| body, _ = request(f"{auth_url}?{query}", headers=headers) | |
| token = json.loads(body.decode("utf-8")).get("token") | |
| if not token: | |
| raise RuntimeError(f"Registry token response did not include a token for {repository}") | |
| return token | |
| def get_digest(token, image_tag): | |
| manifest_url = f"{registry_url}/v2/{repository}/manifests/{quote(image_tag, safe='')}" | |
| _, headers = request( | |
| manifest_url, | |
| headers={ | |
| "Authorization": f"Bearer {token}", | |
| "Accept": manifest_accept, | |
| "User-Agent": "ATOM SGLang Benchmark Image Gate/1.0", | |
| }, | |
| ) | |
| digest = headers.get("Docker-Content-Digest") or headers.get("docker-content-digest") | |
| if not digest: | |
| raise RuntimeError(f"Registry did not return Docker-Content-Digest for {repository}:{image_tag}") | |
| return str(digest).strip() | |
| def next_page_url(base_url, link_header): | |
| if not link_header: | |
| return None | |
| for part in link_header.split(","): | |
| match = re.match(r'\s*<([^>]+)>\s*;\s*rel="next"\s*', part) | |
| if match: | |
| return urljoin(base_url, match.group(1)) | |
| return None | |
| def list_tags(token): | |
| tags = [] | |
| url = f"{registry_url}/v2/{repository}/tags/list?n=1000" | |
| while url: | |
| body, headers = request( | |
| url, | |
| headers={ | |
| "Authorization": f"Bearer {token}", | |
| "User-Agent": "ATOM SGLang Benchmark Image Gate/1.0", | |
| }, | |
| ) | |
| payload = json.loads(body.decode("utf-8")) | |
| tags.extend(payload.get("tags") or []) | |
| url = next_page_url(url, headers.get("Link")) | |
| return tags | |
| def latest_recent_nightly_tag(token, max_age_days=1): | |
| allowed_dates = { | |
| (today_date - timedelta(days=offset)).strftime("%Y%m%d") | |
| for offset in range(max_age_days + 1) | |
| } | |
| candidates = [] | |
| for candidate in list_tags(token): | |
| match = nightly_pattern.match(candidate) | |
| if not match: | |
| continue | |
| if match.group("version") != version: | |
| continue | |
| if match.group("date") not in allowed_dates: | |
| continue | |
| candidates.append( | |
| ( | |
| match.group("date"), | |
| candidate, | |
| ) | |
| ) | |
| if not candidates: | |
| allowed = ", ".join(sorted(allowed_dates)) | |
| raise RuntimeError( | |
| f"No SGLang nightly tags for version {version} were found in {repository} " | |
| f"within allowed dates: {allowed}" | |
| ) | |
| return max(candidates)[1] | |
| def write_selected_image(image_tag, digest, resolution): | |
| image = f"{repository}:{image_tag}@{digest}" | |
| output_path = os.environ["GITHUB_OUTPUT"] | |
| with open(output_path, "a", encoding="utf-8") as fh: | |
| fh.write(f"scheduled_sglang_image={image}\n") | |
| fh.write(f"scheduled_sglang_tag={image_tag}\n") | |
| fh.write(f"scheduled_sglang_digest={digest}\n") | |
| fh.write(f"scheduled_sglang_resolution={resolution}\n") | |
| with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as fh: | |
| fh.write("### Scheduled SGLang image\n") | |
| fh.write(f"- Image: `{image}`\n") | |
| fh.write(f"- Tag: `{image_tag}`\n") | |
| fh.write(f"- Digest: `{digest}`\n") | |
| fh.write(f"- Resolution: `{resolution}`\n") | |
| return image | |
| token = get_token() | |
| timeout_seconds = 2 * 60 * 60 | |
| interval_seconds = 5 * 60 | |
| deadline = time.monotonic() + timeout_seconds | |
| last_error = "" | |
| print(f"Waiting up to {timeout_seconds // 60} minutes for {repository}:{tag}") | |
| while True: | |
| try: | |
| digest = get_digest(token, tag) | |
| image = write_selected_image(tag, digest, "scheduled-nightly-tag") | |
| print(f"Found scheduled SGLang image: {image}") | |
| break | |
| except HTTPError as exc: | |
| last_error = f"HTTP {exc.code}" | |
| if exc.code in (401, 403): | |
| token = get_token() | |
| elif exc.code != 404: | |
| print(f"Warning: failed to check {repository}:{tag}: {last_error}", file=sys.stderr) | |
| except (RuntimeError, URLError) as exc: | |
| last_error = str(exc) | |
| print(f"Warning: failed to check {repository}:{tag}: {last_error}", file=sys.stderr) | |
| try: | |
| fallback_tag = latest_recent_nightly_tag(token, max_age_days=1) | |
| if fallback_tag != tag: | |
| digest = get_digest(token, fallback_tag) | |
| image = write_selected_image(fallback_tag, digest, "recent-nightly-tag") | |
| print( | |
| f"{repository}:{tag} is not available yet; " | |
| f"using recent SGLang nightly image: {image}" | |
| ) | |
| break | |
| except (HTTPError, RuntimeError, URLError) as exc: | |
| last_error = str(exc) | |
| remaining = deadline - time.monotonic() | |
| if remaining <= 0: | |
| print( | |
| f"Timed out waiting for scheduled SGLang image {repository}:{tag}. " | |
| f"Falling back to a same-version nightly tag from today/yesterday. " | |
| f"Last error: {last_error or 'not found'}" | |
| ) | |
| fallback_tag = latest_recent_nightly_tag(token, max_age_days=1) | |
| digest = get_digest(token, fallback_tag) | |
| image = write_selected_image(fallback_tag, digest, "recent-nightly-tag") | |
| print(f"Using latest recent SGLang image: {image}") | |
| break | |
| sleep_for = min(interval_seconds, remaining) | |
| print(f"{repository}:{tag} is not available yet; retrying in {int(sleep_for)} seconds.") | |
| time.sleep(sleep_for) | |
| PY | |
| resolve-atom-source: | |
| name: Resolve ATOM benchmark source | |
| needs: [wait-for-scheduled-sglang-image] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| atom_repository: ${{ steps.resolve.outputs.atom_repository }} | |
| atom_ref: ${{ steps.resolve.outputs.atom_ref }} | |
| normalized_ref: ${{ steps.resolve.outputs.normalized_ref }} | |
| rebuild_sglang_image: ${{ steps.resolve.outputs.rebuild_sglang_image }} | |
| prebuilt_sglang_image: ${{ steps.resolve.outputs.prebuilt_sglang_image }} | |
| selected_sglang_ref: ${{ steps.resolve.outputs.selected_sglang_ref }} | |
| selected_sglang_version: ${{ steps.resolve.outputs.selected_sglang_version }} | |
| sglang_base_image: ${{ steps.resolve.outputs.sglang_base_image }} | |
| sglang_image_source: ${{ steps.resolve.outputs.sglang_image_source }} | |
| publish_to_dashboard: ${{ steps.resolve.outputs.publish_to_dashboard }} | |
| upload_to_custom_dashboard: ${{ steps.resolve.outputs.upload_to_custom_dashboard }} | |
| workload_label: ${{ steps.resolve.outputs.workload_label }} | |
| use_workspace_atom: ${{ steps.resolve.outputs.use_workspace_atom }} | |
| steps: | |
| - name: Checkout selected branch | |
| uses: actions/checkout@v6 | |
| - name: Resolve benchmark source | |
| id: resolve | |
| env: | |
| DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | |
| DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | |
| INPUT_DOCKER_IMAGE: ${{ inputs.docker_image || '' }} | |
| INPUT_PUBLISH_TO_DASHBOARD: ${{ inputs.publish_to_dashboard }} | |
| INPUT_UPLOAD_TO_CUSTOM_DASHBOARD: ${{ inputs.upload_to_custom_dashboard }} | |
| SCHEDULE_CRON: ${{ github.event.schedule || '' }} | |
| SCHEDULED_SGLANG_IMAGE: ${{ needs.wait-for-scheduled-sglang-image.outputs.scheduled_sglang_image }} | |
| SCHEDULED_SGLANG_TAG: ${{ needs.wait-for-scheduled-sglang-image.outputs.scheduled_sglang_tag }} | |
| SCHEDULED_SGLANG_DIGEST: ${{ needs.wait-for-scheduled-sglang-image.outputs.scheduled_sglang_digest }} | |
| SCHEDULED_SGLANG_RESOLUTION: ${{ needs.wait-for-scheduled-sglang-image.outputs.scheduled_sglang_resolution }} | |
| run: | | |
| set -euo pipefail | |
| ATOM_REPOSITORY="${GITHUB_REPOSITORY}" | |
| ATOM_REF="${GITHUB_REF_NAME}" | |
| SGLANG_BASE_IMAGE="rocm/atom-dev:latest" | |
| NORMALIZED_REF="${ATOM_REF#refs/heads/}" | |
| NORMALIZED_REF="${NORMALIZED_REF#refs/tags/}" | |
| PREBUILT_SGLANG_IMAGE="" | |
| REBUILD_SGLANG_IMAGE=false | |
| SGLANG_IMAGE_SOURCE="prebuilt" | |
| USER_PROVIDED_SGLANG_IMAGE=false | |
| USE_WORKSPACE_ATOM=true | |
| RESOLVED_PREBUILT_ALIAS="" | |
| RESOLVED_PREBUILT_DIGEST="" | |
| PREBUILT_RESOLUTION_TYPE="" | |
| RESOLVED_NIGHTLY_TAG="" | |
| if [[ -n "${INPUT_DOCKER_IMAGE}" ]]; then | |
| PREBUILT_SGLANG_IMAGE="${INPUT_DOCKER_IMAGE}" | |
| SGLANG_IMAGE_SOURCE="user-provided" | |
| USER_PROVIDED_SGLANG_IMAGE=true | |
| elif [[ "${GITHUB_EVENT_NAME}" == "schedule" && "${NORMALIZED_REF}" == "main" && -n "${SCHEDULED_SGLANG_IMAGE}" ]]; then | |
| PREBUILT_SGLANG_IMAGE="${SCHEDULED_SGLANG_IMAGE}" | |
| SGLANG_IMAGE_SOURCE="prebuilt-scheduled-nightly" | |
| USE_WORKSPACE_ATOM=false | |
| RESOLVED_PREBUILT_ALIAS="rocm/atom-dev:${SCHEDULED_SGLANG_TAG}" | |
| RESOLVED_PREBUILT_DIGEST="${SCHEDULED_SGLANG_DIGEST}" | |
| PREBUILT_RESOLUTION_TYPE="${SCHEDULED_SGLANG_RESOLUTION:-scheduled-nightly-tag}" | |
| RESOLVED_NIGHTLY_TAG="${SCHEDULED_SGLANG_TAG}" | |
| elif [[ "${NORMALIZED_REF}" == "main" ]]; then | |
| PREBUILT_SGLANG_IMAGE="rocm/atom-dev:sglang-latest" | |
| SGLANG_IMAGE_SOURCE="prebuilt" | |
| else | |
| REBUILD_SGLANG_IMAGE=true | |
| SGLANG_IMAGE_SOURCE="rebuild" | |
| fi | |
| WORKLOAD_LABEL="SGLang-OOB" | |
| if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then | |
| case "${SCHEDULE_CRON}" in | |
| "0 15 * * 1,3,5"|"0 15 * * 2,4"|"0 15 * * 6") WORKLOAD_LABEL="SGLang-OOB" ;; | |
| *) WORKLOAD_LABEL="SGLang-OOB" ;; | |
| esac | |
| fi | |
| case "${WORKLOAD_LABEL}" in | |
| SGLang-OOB) ;; | |
| *) | |
| echo "Unsupported workload_label: ${WORKLOAD_LABEL}. Expected SGLang-OOB." | |
| exit 1 | |
| ;; | |
| esac | |
| if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then | |
| PUBLISH_TO_DASHBOARD=true | |
| UPLOAD_TO_CUSTOM_DASHBOARD=true | |
| else | |
| PUBLISH_TO_DASHBOARD="${INPUT_PUBLISH_TO_DASHBOARD:-true}" | |
| UPLOAD_TO_CUSTOM_DASHBOARD="${INPUT_UPLOAD_TO_CUSTOM_DASHBOARD:-true}" | |
| fi | |
| mapfile -t VLLM_META < <(python3 - <<'PY' | |
| import re | |
| from pathlib import Path | |
| text = Path(".github/workflows/docker-release.yaml").read_text(encoding="utf-8") | |
| commit_match = re.search(r'^\s*SGLANG_REF:\s*"([^"]+)"', text, re.MULTILINE) | |
| version_match = re.search(r'^\s*SGLANG_VERSION:\s*"([^"]+)"', text, re.MULTILINE) | |
| if not commit_match or not version_match: | |
| raise SystemExit("Failed to read SGLANG_REF/SGLANG_VERSION from .github/workflows/docker-release.yaml") | |
| print(commit_match.group(1)) | |
| print(version_match.group(1)) | |
| PY | |
| ) | |
| SELECTED_SGLANG_REF="${VLLM_META[0]}" | |
| SELECTED_SGLANG_VERSION="${VLLM_META[1]}" | |
| if [[ "${SGLANG_IMAGE_SOURCE}" == "prebuilt" ]]; then | |
| if RESOLUTION_JSON="$( | |
| python3 .github/scripts/resolve_atom_image.py \ | |
| --repository rocm/atom-dev \ | |
| --reference-tag sglang-latest \ | |
| --preferred-version "${SELECTED_SGLANG_VERSION}" | |
| )"; then | |
| mapfile -t RESOLVED_IMAGE_META < <( | |
| RESOLUTION_JSON="${RESOLUTION_JSON}" python3 - <<'PY' | |
| import json | |
| import os | |
| resolution = json.loads(os.environ["RESOLUTION_JSON"]) | |
| print(resolution["resolved_image"]) | |
| print(resolution["reference_image"]) | |
| print(resolution["reference_digest"]) | |
| print(resolution["match_type"]) | |
| print(resolution.get("matched_tag", "")) | |
| PY | |
| ) | |
| PREBUILT_SGLANG_IMAGE="${RESOLVED_IMAGE_META[0]}" | |
| RESOLVED_PREBUILT_ALIAS="${RESOLVED_IMAGE_META[1]}" | |
| RESOLVED_PREBUILT_DIGEST="${RESOLVED_IMAGE_META[2]}" | |
| PREBUILT_RESOLUTION_TYPE="${RESOLVED_IMAGE_META[3]}" | |
| RESOLVED_NIGHTLY_TAG="${RESOLVED_IMAGE_META[4]}" | |
| else | |
| echo "::warning::Failed to resolve rocm/atom-dev:sglang-latest to an immutable digest; falling back to the floating tag." | |
| PREBUILT_SGLANG_IMAGE="rocm/atom-dev:sglang-latest" | |
| RESOLVED_PREBUILT_ALIAS="rocm/atom-dev:sglang-latest" | |
| PREBUILT_RESOLUTION_TYPE="resolution-failed" | |
| fi | |
| fi | |
| { | |
| echo "atom_repository=${ATOM_REPOSITORY}" | |
| echo "atom_ref=${ATOM_REF}" | |
| echo "normalized_ref=${NORMALIZED_REF}" | |
| echo "rebuild_sglang_image=${REBUILD_SGLANG_IMAGE}" | |
| echo "prebuilt_sglang_image=${PREBUILT_SGLANG_IMAGE}" | |
| echo "selected_sglang_ref=${SELECTED_SGLANG_REF}" | |
| echo "selected_sglang_version=${SELECTED_SGLANG_VERSION}" | |
| echo "sglang_base_image=${SGLANG_BASE_IMAGE}" | |
| echo "sglang_image_source=${SGLANG_IMAGE_SOURCE}" | |
| echo "publish_to_dashboard=${PUBLISH_TO_DASHBOARD}" | |
| echo "upload_to_custom_dashboard=${UPLOAD_TO_CUSTOM_DASHBOARD}" | |
| echo "workload_label=${WORKLOAD_LABEL}" | |
| echo "use_workspace_atom=${USE_WORKSPACE_ATOM}" | |
| } >> "$GITHUB_OUTPUT" | |
| printf '### SGLang benchmark source\n- Repository: `%s`\n- Ref: `%s`\n- Image mode: `%s`\n' \ | |
| "${ATOM_REPOSITORY}" \ | |
| "${ATOM_REF}" \ | |
| "${SGLANG_IMAGE_SOURCE}" >> "$GITHUB_STEP_SUMMARY" | |
| if [[ "${USER_PROVIDED_SGLANG_IMAGE}" == "true" ]]; then | |
| printf -- '- User-provided image: `%s`\n- Expected SGLang version: `%s`\n- Expected SGLang commit: `%s`\n' \ | |
| "${PREBUILT_SGLANG_IMAGE}" \ | |
| "${SELECTED_SGLANG_VERSION}" \ | |
| "${SELECTED_SGLANG_REF}" >> "$GITHUB_STEP_SUMMARY" | |
| printf -- '\nBecause an explicit `docker_image` input was provided, this run uses that image directly and skips branch-based rebuild logic.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| elif [[ "${REBUILD_SGLANG_IMAGE}" == "true" ]]; then | |
| printf -- '- Base image: `%s`\n- SGLang version: `%s`\n- SGLang commit: `%s`\n- SGLang source: `.github/workflows/docker-release.yaml`\n' \ | |
| "${SGLANG_BASE_IMAGE}" \ | |
| "${SELECTED_SGLANG_VERSION}" \ | |
| "${SELECTED_SGLANG_REF}" >> "$GITHUB_STEP_SUMMARY" | |
| printf -- '\nBecause branch `%s` is not `main` and no `docker_image` override was provided, a custom SGLang docker is rebuilt from the selected branch.\n' \ | |
| "${ATOM_REF}" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| printf -- '- Prebuilt image: `%s`\n- Expected SGLang version: `%s`\n- Expected SGLang commit: `%s`\n' \ | |
| "${PREBUILT_SGLANG_IMAGE}" \ | |
| "${SELECTED_SGLANG_VERSION}" \ | |
| "${SELECTED_SGLANG_REF}" >> "$GITHUB_STEP_SUMMARY" | |
| if [[ -n "${RESOLVED_PREBUILT_ALIAS}" ]]; then | |
| printf -- '- Floating alias: `%s`\n' \ | |
| "${RESOLVED_PREBUILT_ALIAS}" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| if [[ -n "${RESOLVED_PREBUILT_DIGEST}" ]]; then | |
| printf -- '- Resolved digest: `%s`\n' \ | |
| "${RESOLVED_PREBUILT_DIGEST}" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| case "${PREBUILT_RESOLUTION_TYPE}" in | |
| matched-nightly-tag) | |
| printf -- '- Resolution: reverse-matched same digest to nightly tag `%s`\n' \ | |
| "${RESOLVED_NIGHTLY_TAG}" >> "$GITHUB_STEP_SUMMARY" | |
| ;; | |
| digest-pinned-latest) | |
| printf -- '- Resolution: no nightly tag with the same digest was found, so this run pins `sglang-latest` to its current digest instead.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| ;; | |
| reverse-lookup-failed-digest-pinned-latest) | |
| printf -- '- Resolution: reverse lookup could not complete, so this run pins `sglang-latest` to its current digest instead of falling back to the floating tag.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| ;; | |
| scheduled-nightly-tag) | |
| printf -- '- Resolution: scheduled run waited for today'\''s nightly tag `%s` and pinned it to digest `%s`.\n' \ | |
| "${RESOLVED_NIGHTLY_TAG}" \ | |
| "${RESOLVED_PREBUILT_DIGEST}" >> "$GITHUB_STEP_SUMMARY" | |
| ;; | |
| recent-nightly-tag) | |
| { | |
| printf -- '- Resolution: today'\''s nightly tag was not available before the wait timeout, ' | |
| printf -- 'so this run uses the latest recent SGLang nightly tag `%s` pinned to digest `%s`.\n' \ | |
| "${RESOLVED_NIGHTLY_TAG}" \ | |
| "${RESOLVED_PREBUILT_DIGEST}" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| ;; | |
| resolution-failed) | |
| printf -- '- Resolution: digest pinning itself failed, so this run falls back to floating `sglang-latest`.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| ;; | |
| esac | |
| fi | |
| printf -- '- Workload label: `%s`\n- Upload to dashboard: `%s`\n- Upload to LLM-Booster: `%s`\n' \ | |
| "${WORKLOAD_LABEL}" \ | |
| "${PUBLISH_TO_DASHBOARD}" \ | |
| "${UPLOAD_TO_CUSTOM_DASHBOARD}" >> "$GITHUB_STEP_SUMMARY" | |
| if [[ "${PUBLISH_TO_DASHBOARD}" != "true" ]]; then | |
| printf '\nDashboard upload is disabled by workflow input, so results are kept in artifacts and run summaries only.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| parse-param-lists: | |
| name: Parse parameter lists | |
| needs: [resolve-atom-source] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix_json: ${{ steps.parse-param-lists.outputs.matrix_json }} | |
| param_lists: ${{ steps.parse-param-lists.outputs.param_lists }} | |
| steps: | |
| - name: Parse parameter lists | |
| id: parse-param-lists | |
| env: | |
| INPUT_PARAM_LISTS: ${{ inputs.param_lists || '' }} | |
| WORKLOAD_LABEL: ${{ needs.resolve-atom-source.outputs.workload_label }} | |
| run: | | |
| DEFAULT_OOB_PARAMS="1024,1024,4,0.8;1024,1024,8,0.8;1024,1024,16,0.8;1024,1024,32,0.8;1024,1024,64,0.8;8192,1024,4,0.8;8192,1024,8,0.8;8192,1024,16,0.8;8192,1024,32,0.8;8192,1024,64,0.8;61440,600,64,1.0" | |
| if [[ -n "${INPUT_PARAM_LISTS}" ]]; then | |
| PARAM_LISTS="${INPUT_PARAM_LISTS}" | |
| else | |
| PARAM_LISTS="${DEFAULT_OOB_PARAMS}" | |
| fi | |
| echo "Using param_lists: ${PARAM_LISTS}" | |
| printf 'param_lists=%s\n' "${PARAM_LISTS}" >> "$GITHUB_OUTPUT" | |
| IFS=';' read -ra SETS <<< "${PARAM_LISTS}" | |
| MATRIX_JSON="[" | |
| SEP="" | |
| for SET in "${SETS[@]}"; do | |
| IFS=',' read -ra PARAMS <<< "$SET" | |
| INPUT_LEN="${PARAMS[0]}" | |
| OUTPUT_LEN="${PARAMS[1]}" | |
| CONCURRENCY="${PARAMS[2]}" | |
| RANDOM_RANGE_RATIO="${PARAMS[3]}" | |
| if ! [[ "${CONCURRENCY}" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "Unsupported concurrency: ${CONCURRENCY}. Expected a positive integer." | |
| exit 1 | |
| fi | |
| MATRIX_JSON="${MATRIX_JSON}${SEP}{\"input_length\":${INPUT_LEN},\"output_length\":${OUTPUT_LEN},\"concurrency\":${CONCURRENCY},\"random_range_ratio\":${RANDOM_RANGE_RATIO}}" | |
| SEP="," | |
| done | |
| MATRIX_JSON="${MATRIX_JSON}]" | |
| echo "matrix_json=${MATRIX_JSON}" >> "$GITHUB_OUTPUT" | |
| - name: Print matrix JSON | |
| run: | | |
| echo "Matrix JSON: ${{ steps.parse-param-lists.outputs.matrix_json }}" | |
| load-models: | |
| name: Load SGLang model configs | |
| needs: [resolve-atom-source] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| models_json: ${{ steps.load.outputs.models_json }} | |
| has_enabled_models: ${{ steps.load.outputs.has_enabled_models }} | |
| selected_group: ${{ steps.load.outputs.selected_group }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - id: load | |
| env: | |
| MODEL_MI355: ${{ inputs.model_mi355 || 'none' }} | |
| MODEL_MI308: ${{ inputs.model_mi308 || 'none' }} | |
| WORKLOAD_LABEL: ${{ needs.resolve-atom-source.outputs.workload_label }} | |
| SCHEDULE_CRON: ${{ github.event.schedule || '' }} | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' >> "$GITHUB_OUTPUT" | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| def expand_template_parts(value, templates, separator): | |
| if isinstance(value, str): | |
| return templates.get(value, value) | |
| if isinstance(value, list): | |
| return separator.join( | |
| templates.get(str(part), str(part)) | |
| for part in value | |
| if str(part) | |
| ) | |
| return value | |
| def load_model_catalog(path): | |
| catalog = json.loads(path.read_text(encoding="utf-8")) | |
| if isinstance(catalog, list): | |
| return catalog | |
| templates = catalog.get("templates", {}) | |
| extra_arg_templates = templates.get("extra_args", {}) | |
| env_var_templates = templates.get("env_vars", {}) | |
| models = [] | |
| for model in catalog.get("models", []): | |
| expanded = dict(model) | |
| expanded["extra_args"] = expand_template_parts( | |
| expanded.get("extra_args", ""), extra_arg_templates, " " | |
| ) | |
| expanded["env_vars"] = expand_template_parts( | |
| expanded.get("env_vars", ""), env_var_templates, "\n" | |
| ) | |
| models.append(expanded) | |
| return models | |
| models = load_model_catalog(Path(".github/benchmark/sglang_benchmark_models.json")) | |
| event = os.environ["GITHUB_EVENT_NAME"] | |
| workload_label = os.environ.get("WORKLOAD_LABEL") or "SGLang-OOB" | |
| selected_group = "" | |
| def model_workload(model): | |
| return str(model.get("workload_label") or "SGLang-OOB") | |
| def matches_workload(model): | |
| return model_workload(model) == workload_label | |
| MODEL_SELECTION_ALIASES = { | |
| "DeepSeek-V4-Pro TP8": "deepseek-v4-pro-tp8", | |
| "DeepSeek-V4-Pro MTP1 TP8": "deepseek-v4-pro-mtp1-tp8", | |
| "DeepSeek-V4-Pro MTP3 TP8": "deepseek-v4-pro-mtp3-tp8", | |
| "DeepSeek-V4-Pro Prefix Cache TP8": "deepseek-v4-pro-prefix-cache-tp8", | |
| "DeepSeek-V3.2 FP8 TP4": "deepseek-v3-2-fp8-tp4", | |
| "DeepSeek-V3.2 FP8 TP4 DP4 EP4": "deepseek-v3-2-fp8-tp4-dp4-ep4", | |
| "DeepSeek-V3.2 FP8 TP8": "deepseek-v3-2-fp8-tp8", | |
| "DeepSeek-V3.2 FP8 TP8 DP8 EP8": "deepseek-v3-2-fp8-tp8-dp8-ep8", | |
| "DeepSeek-R1 FP8 TP4": "deepseek-r1-fp8-tp4", | |
| "DeepSeek-R1 FP8 TP8": "deepseek-r1-fp8-tp8", | |
| "DeepSeek-R1 FP4 TP4": "deepseek-r1-fp4-tp4", | |
| "DeepSeek-R1 FP4 TP4 DP4 EP4": "deepseek-r1-fp4-tp4-dp4-ep4", | |
| "DeepSeek-R1 FP4 TP8": "deepseek-r1-fp4-tp8", | |
| "DeepSeek-R1 FP4 TP8 EP8": "deepseek-r1-fp4-tp8-ep8", | |
| "DeepSeek-R1 FP4 TP8 DP8 EP8": "deepseek-r1-fp4-tp8-dp8-ep8", | |
| "DeepSeek-R1 FP4 TP8 MTP1": "deepseek-r1-fp4-tp8-mtp1", | |
| "DeepSeek-R1 FP4 TP8 MTP3": "deepseek-r1-fp4-tp8-mtp3", | |
| "DeepSeek-R1 FP4 MTP3 TP4 DP4 EP4": "deepseek-r1-fp4-mtp3-tp4-dp4-ep4", | |
| "DeepSeek-R1 FP4 MTP3 TP8 DP8 EP8": "deepseek-r1-fp4-mtp3-tp8-dp8-ep8", | |
| "Qwen3.5-397B-A17B-FP8 TP4": "qwen3-5-397b-a17b-fp8-tp4", | |
| "Qwen3.5-397B-A17B-FP8 TP8": "qwen3-5-397b-a17b-fp8-tp8", | |
| "Qwen3.8-2.4T-A95B-Quark-MXFP4 TP8": "qwen3-8-2-4t-a95b-quark-mxfp4-tp8", | |
| "Qwen3.8-Flash-Next-FP8 TP2": "qwen3-8-flash-next-fp8-tp2", | |
| "GLM-5.1 FP8 TP8": "glm-5-1-fp8-tp8", | |
| "GLM-5.2 FP8 TP4": "glm-5-2-fp8-tp4", | |
| "GLM-5.2 FP8 TP4 PCP4": "glm-5-2-fp8-tp4-pcp4", | |
| "GLM-5.2 FP4 TP4": "glm-5-2-fp4-tp4", | |
| "GLM-5.2 FP4 TP4 MTP3": "glm-5-2-fp4-tp4-mtp3", | |
| "MiniMax-M3-MXFP4 TP4": "minimax-m3-mxfp4-tp4", | |
| "MiniMax-M3-MXFP8 EAGLE3 TP4 MTP3": "minimax-m3-mxfp8-eagle3-tp4-mtp3", | |
| "MI308 Qwen3.5-397B-A17B-FP8 TP4": "qwen3-5-397b-a17b-fp8-tp4-mi308", | |
| "MI308 Qwen3.5-397B-A17B-FP8 TP8": "qwen3-5-397b-a17b-fp8-tp8-mi308", | |
| "MI308 Qwen3.5-35B-A3B-FP8 TP1": "qwen3-5-35b-a3b-fp8-tp1-mi308", | |
| "MI308 Qwen3.5-35B-A3B-PTPC-FP8 TP1": "qwen3-5-35b-a3b-ptpc-fp8-tp1-mi308", | |
| "MI308 Qwen3.8-Flash-Next-FP8 TP2": "qwen3-8-flash-next-fp8-tp2-mi308", | |
| "MI308 DeepSeek-V4-Flash": "deepseek-v4-flash-fp8-mi308", | |
| } | |
| def is_mi308_model(model): | |
| return "mi308" in str(model.get("runner", "")) or "mi308" in str(model.get("prefix", "")) | |
| def manually_selected(model, selection, expected_mi308): | |
| selection = (selection or "none").split(" (", 1)[0] | |
| if selection == "none" or is_mi308_model(model) != expected_mi308: | |
| return False | |
| if selection == "all": | |
| return True | |
| prefix = str(model.get("prefix", "")) | |
| display = str(model.get("display", "")) | |
| return prefix == selection or display == selection or prefix == MODEL_SELECTION_ALIASES.get(selection) | |
| OOB_P0_PREFIX_ORDER = [ | |
| "deepseek-v4-pro-tp8", | |
| "deepseek-v4-pro-mtp1-tp8", | |
| "deepseek-v4-pro-mtp3-tp8", | |
| "glm-5-2-fp8-tp4", | |
| "glm-5-2-fp8-tp4-pcp4", | |
| "glm-5-2-fp4-tp4", | |
| "glm-5-2-fp4-tp4-mtp3", | |
| "minimax-m3-mxfp4-tp4", | |
| ] | |
| OOB_P1_PREFIX_ORDER = [ | |
| "deepseek-v3-2-fp8-tp4", | |
| "deepseek-v3-2-fp8-tp4-dp4-ep4", | |
| "deepseek-v3-2-fp8-tp8", | |
| "deepseek-v3-2-fp8-tp8-dp8-ep8", | |
| "minimax-m3-mxfp8-eagle3-tp4-mtp3", | |
| ] | |
| OOB_P2_PREFIX_ORDER = [ | |
| "deepseek-r1-fp8-tp8", | |
| "deepseek-r1-fp8-tp4", | |
| "deepseek-r1-fp4-tp8", | |
| "deepseek-r1-fp4-tp8-ep8", | |
| "deepseek-r1-fp4-tp8-dp8-ep8", | |
| "deepseek-r1-fp4-tp8-mtp3", | |
| "deepseek-r1-fp4-tp8-mtp1", | |
| "deepseek-r1-fp4-tp4", | |
| "deepseek-r1-fp4-tp4-dp4-ep4", | |
| "deepseek-r1-fp4-mtp3-tp4-dp4-ep4", | |
| "deepseek-r1-fp4-mtp3-tp8-dp8-ep8", | |
| "qwen3-5-397b-a17b-fp8-tp4", | |
| "qwen3-5-397b-a17b-fp8-tp8", | |
| "qwen3-8-2-4t-a95b-quark-mxfp4-tp8", | |
| "qwen3-8-flash-next-fp8-tp2", | |
| "glm-5-1-fp8-tp8", | |
| ] | |
| def is_oob_mi308(model): | |
| return matches_workload(model) and "mi308" in str(model.get("prefix", "")) | |
| def first_model_by_prefix(candidates): | |
| """Catalog may list the same prefix twice; keep first occurrence order.""" | |
| by_prefix = {} | |
| for model in candidates: | |
| if not matches_workload(model): | |
| continue | |
| prefix = str(model.get("prefix", "")) | |
| if prefix not in by_prefix: | |
| by_prefix[prefix] = model | |
| return by_prefix | |
| def scheduled_oob_selection(schedule_cron): | |
| schedule_to_tiers = { | |
| "0 15 * * 1,3,5": ("MI308-only", []), | |
| "0 15 * * 2,4": ( | |
| "OOB-P0+P1", | |
| OOB_P0_PREFIX_ORDER + OOB_P1_PREFIX_ORDER, | |
| ), | |
| "0 15 * * 6": ( | |
| "OOB-P0+P1+P2", | |
| OOB_P0_PREFIX_ORDER | |
| + OOB_P1_PREFIX_ORDER | |
| + OOB_P2_PREFIX_ORDER, | |
| ), | |
| } | |
| if schedule_cron not in schedule_to_tiers: | |
| return "SKIP-UNKNOWN-DAY", [] | |
| selected_group, tier_order = schedule_to_tiers[schedule_cron] | |
| return selected_group, select_oob_models([tier_order], include_mi308=True) | |
| def select_oob_models(tier_orders, include_mi308): | |
| by_prefix = first_model_by_prefix(models) | |
| tier_prefixes = frozenset( | |
| prefix | |
| for tier_order in tier_orders | |
| for prefix in tier_order | |
| ) | |
| selected = [] | |
| seen_prefixes = set() | |
| for tier_order in tier_orders: | |
| for prefix in tier_order: | |
| if prefix in seen_prefixes: | |
| continue | |
| seen_prefixes.add(prefix) | |
| model = by_prefix.get(prefix) | |
| if model is not None: | |
| selected.append(model) | |
| if not include_mi308: | |
| return selected | |
| for prefix in sorted(by_prefix): | |
| if prefix in tier_prefixes: | |
| continue | |
| model = by_prefix[prefix] | |
| if is_oob_mi308(model): | |
| selected.append(model) | |
| return selected | |
| MANUAL_OOB_SELECTIONS = { | |
| "OOB_P0": ("OOB_P0", [OOB_P0_PREFIX_ORDER]), | |
| "OOB_P1": ("OOB_P1", [OOB_P1_PREFIX_ORDER]), | |
| "OOB_P2": ("OOB_P2", [OOB_P2_PREFIX_ORDER]), | |
| "OOB_P0+OOB_P1": ("OOB_P0+OOB_P1", [OOB_P0_PREFIX_ORDER, OOB_P1_PREFIX_ORDER]), | |
| } | |
| if event == "schedule": | |
| schedule_cron = os.environ.get("SCHEDULE_CRON", "") | |
| selected_group, selected = scheduled_oob_selection(schedule_cron) | |
| else: | |
| model_mi355 = os.environ.get("MODEL_MI355") or "none" | |
| model_mi308 = os.environ.get("MODEL_MI308") or "none" | |
| selected = [] | |
| manual_oob_selection = MANUAL_OOB_SELECTIONS.get(model_mi355) | |
| if manual_oob_selection: | |
| selected_group, tier_orders = manual_oob_selection | |
| selected.extend(select_oob_models(tier_orders, include_mi308=False)) | |
| else: | |
| selected.extend( | |
| model | |
| for model in models | |
| if matches_workload(model) | |
| and manually_selected(model, model_mi355, expected_mi308=False) | |
| ) | |
| selected.extend( | |
| model | |
| for model in models | |
| if matches_workload(model) | |
| and manually_selected(model, model_mi308, expected_mi308=True) | |
| ) | |
| print(f"SGLang workload label: {workload_label}", file=sys.stderr) | |
| if selected_group: | |
| print(f"SGLang benchmark group: {selected_group}", file=sys.stderr) | |
| if not selected: | |
| print("No models selected for SGLang benchmark.", file=sys.stderr) | |
| else: | |
| print("Selected SGLang benchmark models:", file=sys.stderr) | |
| for model in selected: | |
| print(f" - {model['display']} ({model['prefix']})", file=sys.stderr) | |
| print(f"models_json={json.dumps(selected, separators=(',', ':'))}") | |
| print(f"selected_group={selected_group}") | |
| print(f"has_enabled_models={'true' if selected else 'false'}") | |
| PY | |
| - name: Print selected models | |
| env: | |
| SELECTED_GROUP: ${{ steps.load.outputs.selected_group }} | |
| MODELS_JSON: ${{ steps.load.outputs.models_json }} | |
| run: | | |
| if [[ -n "${SELECTED_GROUP}" ]]; then | |
| echo "SGLang benchmark group: ${SELECTED_GROUP}" | |
| fi | |
| printf 'Selected models: %s\n' "${MODELS_JSON}" | |
| build-benchmark-matrix: | |
| name: Build SGLang benchmark matrix | |
| needs: [parse-param-lists, load-models] | |
| if: >- | |
| needs.parse-param-lists.result == 'success' | |
| && needs.load-models.result == 'success' | |
| && needs.load-models.outputs.has_enabled_models == 'true' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| benchmark_matrix_mi355: ${{ steps.build.outputs.benchmark_matrix_mi355 }} | |
| benchmark_matrix_mi308: ${{ steps.build.outputs.benchmark_matrix_mi308 }} | |
| has_benchmark_cells: ${{ steps.build.outputs.has_benchmark_cells }} | |
| has_benchmark_cells_mi355: ${{ steps.build.outputs.has_benchmark_cells_mi355 }} | |
| has_benchmark_cells_mi308: ${{ steps.build.outputs.has_benchmark_cells_mi308 }} | |
| steps: | |
| - name: Combine models and params | |
| id: build | |
| env: | |
| MODELS_JSON: ${{ needs.load-models.outputs.models_json }} | |
| PARAMS_JSON: ${{ needs.parse-param-lists.outputs.matrix_json }} | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import sys | |
| models = json.loads(os.environ["MODELS_JSON"]) | |
| params = json.loads(os.environ["PARAMS_JSON"]) | |
| include = [] | |
| for model in models: | |
| supported_pairs = set(model.get("supported_input_output_pairs", [])) | |
| excluded_pairs = set(model.get("excluded_input_output_pairs", [])) | |
| supported_concurrency_values = { | |
| int(value) for value in model.get("supported_concurrency_values", []) | |
| } | |
| supported_concurrency_by_pair = { | |
| str(pair): {int(value) for value in values} | |
| for pair, values in model.get("supported_concurrency_values_by_pair", {}).items() | |
| } | |
| model_params = [] | |
| seen = set() | |
| for param in params: | |
| variants = [param] | |
| for variant in variants: | |
| key = ( | |
| int(variant["input_length"]), | |
| int(variant["output_length"]), | |
| int(variant["concurrency"]), | |
| str(variant["random_range_ratio"]), | |
| ) | |
| if key in seen: | |
| continue | |
| seen.add(key) | |
| model_params.append(variant) | |
| for param in model_params: | |
| pair = f"{param['input_length']}x{param['output_length']}" | |
| if supported_pairs and pair not in supported_pairs: | |
| continue | |
| if pair in excluded_pairs: | |
| continue | |
| pair_concurrency_values = supported_concurrency_by_pair.get(pair) | |
| if pair_concurrency_values and int(param["concurrency"]) not in pair_concurrency_values: | |
| continue | |
| if supported_concurrency_values and int(param["concurrency"]) not in supported_concurrency_values: | |
| continue | |
| include.append({"model": model, "params": param}) | |
| def is_mi308_row(row): | |
| return "mi308" in str(row["model"].get("runner", "")) | |
| mi308_rows = [row for row in include if is_mi308_row(row)] | |
| mi355_rows = [row for row in include if not is_mi308_row(row)] | |
| sep = (",", ":") | |
| j355 = json.dumps({"include": mi355_rows}, separators=sep) | |
| j308 = json.dumps({"include": mi308_rows}, separators=sep) | |
| gh_out = os.environ["GITHUB_OUTPUT"] | |
| with open(gh_out, "a", encoding="utf-8") as fh: | |
| fh.write(f"benchmark_matrix_mi355={j355}\n") | |
| fh.write(f"benchmark_matrix_mi308={j308}\n") | |
| fh.write("has_benchmark_cells=" + ("false" if not include else "true") + "\n") | |
| fh.write("has_benchmark_cells_mi355=" + ("false" if not mi355_rows else "true") + "\n") | |
| fh.write("has_benchmark_cells_mi308=" + ("false" if not mi308_rows else "true") + "\n") | |
| if not include: | |
| print("No eligible benchmark cases remain after model-specific parameter filtering.", file=sys.stderr) | |
| else: | |
| print(f"Benchmark cells: {len(include)}", file=sys.stderr) | |
| print(f"MI355 shard cells: {len(mi355_rows)}, MI308 shard cells: {len(mi308_rows)}", file=sys.stderr) | |
| PY | |
| build-sglang-image: | |
| name: Build custom SGLang benchmark image | |
| needs: [resolve-atom-source, build-benchmark-matrix] | |
| if: >- | |
| needs.resolve-atom-source.outputs.rebuild_sglang_image == 'true' | |
| && needs.build-benchmark-matrix.result == 'success' | |
| && needs.build-benchmark-matrix.outputs.has_benchmark_cells == 'true' | |
| runs-on: build-only-atom | |
| outputs: | |
| sglang_image_tag: ${{ steps.image-meta.outputs.sglang_image_tag }} | |
| atom_source_sha: ${{ steps.source-meta.outputs.atom_source_sha }} | |
| env: | |
| ATOM_SOURCE_REPOSITORY: ${{ needs.resolve-atom-source.outputs.atom_repository }} | |
| ATOM_SOURCE_REF: ${{ needs.resolve-atom-source.outputs.atom_ref }} | |
| SGLANG_BASE_IMAGE: ${{ needs.resolve-atom-source.outputs.sglang_base_image }} | |
| SGLANG_REF: ${{ needs.resolve-atom-source.outputs.selected_sglang_ref }} | |
| SGLANG_VERSION: ${{ needs.resolve-atom-source.outputs.selected_sglang_version }} | |
| SGLANG_IMAGE_REPO: rocm/atom-dev | |
| steps: | |
| - name: Checkout benchmark ATOM source | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: ${{ env.ATOM_SOURCE_REPOSITORY }} | |
| ref: ${{ env.ATOM_SOURCE_REF }} | |
| fetch-depth: 1 | |
| - name: Record source metadata | |
| id: source-meta | |
| run: | | |
| ATOM_SOURCE_SHA="$(git rev-parse HEAD)" | |
| echo "atom_source_sha=${ATOM_SOURCE_SHA}" >> "$GITHUB_OUTPUT" | |
| printf '### Building custom SGLang image\n- Repository: `%s`\n- Ref: `%s`\n- Commit: `%s`\n- Base image: `%s`\n- SGLang version: `%s`\n- SGLang commit: `%s`\n' \ | |
| "${ATOM_SOURCE_REPOSITORY}" \ | |
| "${ATOM_SOURCE_REF}" \ | |
| "${ATOM_SOURCE_SHA}" \ | |
| "${SGLANG_BASE_IMAGE}" \ | |
| "${SGLANG_VERSION}" \ | |
| "${SGLANG_REF}" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Docker Login | |
| run: | | |
| echo "${{ secrets.DOCKER_PASSWORD }}" | docker login docker.io -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin | |
| - name: Generate custom SGLang base Dockerfile | |
| run: | | |
| cat > Dockerfile.sglang-base <<EOF | |
| FROM ${SGLANG_BASE_IMAGE} | |
| RUN pip install -U lm-eval[api] | |
| RUN pip show lm-eval || true | |
| RUN pip install hf_transfer | |
| RUN pip show hf_transfer || true | |
| RUN echo "=== Aiter version BEFORE uninstall ===" && pip show amd-aiter || true | |
| RUN pip uninstall -y amd-aiter | |
| RUN pip install --upgrade "pybind11>=3.0.1" | |
| RUN pip show pybind11 | |
| RUN pip install "vcs-versioning>=1" | |
| RUN rm -rf /app/aiter-test | |
| RUN git clone --depth 1 https://github.com/ROCm/aiter.git /app/aiter-test && \\ | |
| cd /app/aiter-test && \\ | |
| git checkout HEAD && \\ | |
| git submodule sync && git submodule update --init --recursive && \\ | |
| MAX_JOBS=64 PREBUILD_KERNELS=0 GPU_ARCHS=gfx950 python3 setup.py develop | |
| RUN echo "=== Aiter version AFTER installation ===" && pip show amd-aiter || true | |
| RUN echo "=== ATOM version BEFORE uninstall ===" && pip show atom || true | |
| RUN pip uninstall -y atom || true | |
| RUN rm -rf /app/ATOM | |
| COPY . /app/ATOM | |
| RUN cd /app/ATOM && pip install -e . | |
| RUN echo "=== ATOM version AFTER installation ===" && pip show atom || true | |
| EOF | |
| - name: Build custom SGLang base image | |
| run: | | |
| docker build --pull --network=host \ | |
| --no-cache \ | |
| -t atom_sglang_base:ci \ | |
| -f Dockerfile.sglang-base . | |
| - name: Build custom SGLang image | |
| id: image-meta | |
| run: | | |
| SHORT_SHA="$(git rev-parse --short HEAD)" | |
| SHORT_SGLANG="$(printf '%s' "${SGLANG_REF}" | cut -c1-12)" | |
| SAFE_SGLANG_VERSION="$(printf '%s' "${SGLANG_VERSION}" | tr '/:' '--')" | |
| SGLANG_IMAGE_TAG="${SGLANG_IMAGE_REPO}:sglang-benchmark-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${SHORT_SHA}-${SAFE_SGLANG_VERSION}-${SHORT_SGLANG}" | |
| docker build --network=host \ | |
| --no-cache \ | |
| -t "${SGLANG_IMAGE_TAG}" \ | |
| --build-arg SGLANG_BASE_IMAGE="atom_sglang_base:ci" \ | |
| --build-arg MAX_JOBS=64 \ | |
| --build-arg SGLANG_REF="${SGLANG_REF}" \ | |
| --build-arg INSTALL_LM_EVAL=1 \ | |
| --build-arg INSTALL_FASTSAFETENSORS=1 \ | |
| -f docker/sglang_release.dockerfile . | |
| echo "sglang_image_tag=${SGLANG_IMAGE_TAG}" >> "$GITHUB_OUTPUT" | |
| - name: Push custom SGLang image | |
| env: | |
| DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | |
| DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | |
| run: | | |
| .github/scripts/docker_push_retry.sh "${{ steps.image-meta.outputs.sglang_image_tag }}" | |
| - name: Clean up build images | |
| if: always() | |
| run: | | |
| docker rmi "${{ steps.image-meta.outputs.sglang_image_tag }}" || true | |
| docker rmi atom_sglang_base:ci || true | |
| benchmark-mi355: | |
| name: SGLang benchmark (MI355 / non-MI308 runners) | |
| needs: [resolve-atom-source, build-benchmark-matrix, build-sglang-image] | |
| if: >- | |
| always() | |
| && needs.resolve-atom-source.result == 'success' | |
| && needs.build-benchmark-matrix.result == 'success' | |
| && (needs.build-sglang-image.result == 'success' || needs.build-sglang-image.result == 'skipped') | |
| && needs.build-benchmark-matrix.outputs.has_benchmark_cells_mi355 == 'true' | |
| permissions: | |
| actions: read | |
| contents: write | |
| uses: ./.github/workflows/atom-sglang-benchmark-gpu-shard.yaml | |
| secrets: inherit | |
| with: | |
| shard_suffix: mi355 | |
| benchmark_matrix_json: ${{ needs.build-benchmark-matrix.outputs.benchmark_matrix_mi355 }} | |
| atom_repository: ${{ needs.resolve-atom-source.outputs.atom_repository }} | |
| atom_ref: ${{ needs.resolve-atom-source.outputs.atom_ref }} | |
| workload_label: ${{ needs.resolve-atom-source.outputs.workload_label }} | |
| prebuilt_sglang_image: ${{ needs.resolve-atom-source.outputs.prebuilt_sglang_image }} | |
| sglang_image_source: ${{ needs.resolve-atom-source.outputs.sglang_image_source }} | |
| selected_sglang_ref: ${{ needs.resolve-atom-source.outputs.selected_sglang_ref }} | |
| selected_sglang_version: ${{ needs.resolve-atom-source.outputs.selected_sglang_version }} | |
| publish_to_dashboard: ${{ needs.resolve-atom-source.outputs.publish_to_dashboard }} | |
| upload_to_custom_dashboard: ${{ needs.resolve-atom-source.outputs.upload_to_custom_dashboard }} | |
| use_workspace_atom: ${{ needs.resolve-atom-source.outputs.use_workspace_atom }} | |
| sglang_image_tag: ${{ needs.build-sglang-image.outputs.sglang_image_tag || needs.resolve-atom-source.outputs.prebuilt_sglang_image }} | |
| atom_source_sha_for_checkout: ${{ needs.build-sglang-image.outputs.atom_source_sha || github.sha }} | |
| benchmark-mi308: | |
| name: SGLang benchmark (MI308 runners) | |
| needs: [resolve-atom-source, build-benchmark-matrix, build-sglang-image] | |
| if: >- | |
| always() | |
| && needs.resolve-atom-source.result == 'success' | |
| && needs.build-benchmark-matrix.result == 'success' | |
| && (needs.build-sglang-image.result == 'success' || needs.build-sglang-image.result == 'skipped') | |
| && needs.build-benchmark-matrix.outputs.has_benchmark_cells_mi308 == 'true' | |
| permissions: | |
| actions: read | |
| contents: write | |
| uses: ./.github/workflows/atom-sglang-benchmark-gpu-shard.yaml | |
| secrets: inherit | |
| with: | |
| shard_suffix: mi308 | |
| benchmark_matrix_json: ${{ needs.build-benchmark-matrix.outputs.benchmark_matrix_mi308 }} | |
| atom_repository: ${{ needs.resolve-atom-source.outputs.atom_repository }} | |
| atom_ref: ${{ needs.resolve-atom-source.outputs.atom_ref }} | |
| workload_label: ${{ needs.resolve-atom-source.outputs.workload_label }} | |
| prebuilt_sglang_image: ${{ needs.resolve-atom-source.outputs.prebuilt_sglang_image }} | |
| sglang_image_source: ${{ needs.resolve-atom-source.outputs.sglang_image_source }} | |
| selected_sglang_ref: ${{ needs.resolve-atom-source.outputs.selected_sglang_ref }} | |
| selected_sglang_version: ${{ needs.resolve-atom-source.outputs.selected_sglang_version }} | |
| publish_to_dashboard: ${{ needs.resolve-atom-source.outputs.publish_to_dashboard }} | |
| upload_to_custom_dashboard: ${{ needs.resolve-atom-source.outputs.upload_to_custom_dashboard }} | |
| use_workspace_atom: ${{ needs.resolve-atom-source.outputs.use_workspace_atom }} | |
| sglang_image_tag: ${{ needs.build-sglang-image.outputs.sglang_image_tag || needs.resolve-atom-source.outputs.prebuilt_sglang_image }} | |
| atom_source_sha_for_checkout: ${{ needs.build-sglang-image.outputs.atom_source_sha || github.sha }} | |
| summarize-benchmark-result: | |
| concurrency: | |
| group: gh-pages-deploy | |
| cancel-in-progress: false | |
| if: >- | |
| always() | |
| && needs.resolve-atom-source.result == 'success' | |
| && needs.build-benchmark-matrix.result == 'success' | |
| && needs.build-benchmark-matrix.outputs.has_benchmark_cells == 'true' | |
| name: Summarize SGLang benchmark result | |
| needs: [resolve-atom-source, build-benchmark-matrix, benchmark-mi355, benchmark-mi308] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout ATOM repo | |
| uses: actions/checkout@v6 | |
| - name: Download all benchmark results | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: 'sglang-benchmark-*' | |
| merge-multiple: true | |
| path: . | |
| continue-on-error: true | |
| - name: Count downloaded benchmark results | |
| id: downloaded | |
| run: | | |
| shopt -s nullglob | |
| json_files=(*.json) | |
| echo "downloaded_count=${#json_files[@]}" >> "$GITHUB_OUTPUT" | |
| printf 'Downloaded %s benchmark result files\n' "${#json_files[@]}" | |
| - name: Note summary behavior | |
| run: | | |
| if [[ "${{ needs.resolve-atom-source.outputs.publish_to_dashboard }}" == "true" ]]; then | |
| printf '### Summary mode\nResults from `%s@%s` using `%s` SGLang image mode and `%s` workload are shown below as a workflow summary table. Because dashboard upload is enabled for this run, this final Ubuntu summary job collects benchmark artifacts from all completed GPU cases and publishes the available successful results in one pass, then reconciles the completed run for regression comparison.\n' \ | |
| "${{ needs.resolve-atom-source.outputs.atom_repository }}" \ | |
| "${{ needs.resolve-atom-source.outputs.atom_ref }}" \ | |
| "${{ needs.resolve-atom-source.outputs.sglang_image_source }}" \ | |
| "${{ needs.resolve-atom-source.outputs.workload_label }}" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| printf '### Summary mode\nResults from `%s@%s` using `%s` SGLang image mode and `%s` workload are shown below as a workflow summary table only. Dashboard upload is disabled for this run.\n' \ | |
| "${{ needs.resolve-atom-source.outputs.atom_repository }}" \ | |
| "${{ needs.resolve-atom-source.outputs.atom_ref }}" \ | |
| "${{ needs.resolve-atom-source.outputs.sglang_image_source }}" \ | |
| "${{ needs.resolve-atom-source.outputs.workload_label }}" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| if [[ "${{ needs.resolve-atom-source.outputs.upload_to_custom_dashboard }}" == "true" ]]; then | |
| printf 'LLM-Booster dashboard publish is enabled for this run.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| printf 'LLM-Booster dashboard publish is disabled for this run.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Generate SGLang benchmark summary table | |
| env: | |
| BENCHMARK_MATRIX_MI355: ${{ needs.build-benchmark-matrix.outputs.benchmark_matrix_mi355 }} | |
| BENCHMARK_MATRIX_MI308: ${{ needs.build-benchmark-matrix.outputs.benchmark_matrix_mi308 }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| BENCHMARK_MATRIX="$(python3 - <<'PY' | |
| import json | |
| import os | |
| include = [] | |
| for name in ("BENCHMARK_MATRIX_MI355", "BENCHMARK_MATRIX_MI308"): | |
| raw = os.environ.get(name) or '{"include":[]}' | |
| include.extend(json.loads(raw).get("include", [])) | |
| print(json.dumps({"include": include}, separators=(",", ":"))) | |
| PY | |
| )" | |
| python3 .github/scripts/plugin_benchmark_summary.py \ | |
| --result-dir . \ | |
| --matrix-json "${BENCHMARK_MATRIX}" \ | |
| --run-url "${RUN_URL}" \ | |
| --output-json sglang_benchmark_summary.json \ | |
| --title "SGLang Benchmark Summary" \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| - name: Read summary stats | |
| id: summary-stats | |
| run: | | |
| PASSED_CASES=$(python3 -c "import json; print(json.load(open('sglang_benchmark_summary.json', encoding='utf-8'))['passed_cases'])") | |
| FAILED_CASES=$(python3 -c "import json; print(json.load(open('sglang_benchmark_summary.json', encoding='utf-8'))['failed_cases'])") | |
| echo "passed_cases=${PASSED_CASES}" >> "$GITHUB_OUTPUT" | |
| echo "failed_cases=${FAILED_CASES}" >> "$GITHUB_OUTPUT" | |
| echo "Summary stats: passed=${PASSED_CASES}, failed=${FAILED_CASES}" | |
| - name: Upload SGLang benchmark summary artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sglang-benchmark-summary | |
| path: sglang_benchmark_summary.json | |
| - name: Note skipped LLM-Booster dashboard publish | |
| if: needs.resolve-atom-source.outputs.upload_to_custom_dashboard == 'true' && steps.summary-stats.outputs.passed_cases == '0' | |
| run: | | |
| printf '\nNo successful benchmark result JSONs were produced, so LLM-Booster dashboard publish was skipped.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| - name: Check LLM-Booster deploy key | |
| id: llmbooster-key | |
| if: needs.resolve-atom-source.outputs.upload_to_custom_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' | |
| env: | |
| LLM_BOOSTER_DEPLOY_KEY: ${{ secrets.LLM_BOOSTER_DEPLOY_KEY }} | |
| run: | | |
| if [[ -n "${LLM_BOOSTER_DEPLOY_KEY}" ]]; then | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::LLM_BOOSTER_DEPLOY_KEY is not configured; skipping LLM-Booster dashboard publish." | |
| printf '\nLLM-Booster dashboard publish skipped because secret `LLM_BOOSTER_DEPLOY_KEY` is not configured.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Checkout LLM-Booster dashboard repo | |
| if: needs.resolve-atom-source.outputs.upload_to_custom_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' && steps.llmbooster-key.outputs.available == 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: ROCm/LLM-Booster | |
| ref: main | |
| path: llm-booster-repo | |
| ssh-key: ${{ secrets.LLM_BOOSTER_DEPLOY_KEY }} | |
| fetch-depth: 1 | |
| - name: Build LLM-Booster dashboard files | |
| if: needs.resolve-atom-source.outputs.upload_to_custom_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' && steps.llmbooster-key.outputs.available == 'true' | |
| env: | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| ATOM_SOURCE_REPOSITORY: ${{ needs.resolve-atom-source.outputs.atom_repository }} | |
| ATOM_SOURCE_REF: ${{ needs.resolve-atom-source.outputs.atom_ref }} | |
| SGLANG_IMAGE_SOURCE: ${{ needs.resolve-atom-source.outputs.sglang_image_source }} | |
| WORKLOAD_LABEL: ${{ needs.resolve-atom-source.outputs.workload_label }} | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import time | |
| from collections import defaultdict | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| root = Path(".") | |
| llm_booster_dashboard = root / "llm-booster-repo" / "dashboard" | |
| llm_booster_data_dir = llm_booster_dashboard / "data" / "sglang-atom" | |
| llm_booster_data_dir.mkdir(parents=True, exist_ok=True) | |
| sys.path.insert(0, str(llm_booster_dashboard)) | |
| from update_data import index_root_for, rebuild_index # noqa: E402 | |
| def expand_template_parts(value, templates, separator): | |
| if isinstance(value, str): | |
| return templates.get(value, value) | |
| if isinstance(value, list): | |
| return separator.join( | |
| templates.get(str(part), str(part)) | |
| for part in value | |
| if str(part) | |
| ) | |
| return value | |
| def load_model_catalog(path): | |
| catalog = json.loads(path.read_text(encoding="utf-8")) | |
| if isinstance(catalog, list): | |
| return catalog | |
| templates = catalog.get("templates", {}) | |
| extra_arg_templates = templates.get("extra_args", {}) | |
| env_var_templates = templates.get("env_vars", {}) | |
| models = [] | |
| for model in catalog.get("models", []): | |
| expanded = dict(model) | |
| expanded["extra_args"] = expand_template_parts( | |
| expanded.get("extra_args", ""), extra_arg_templates, " " | |
| ) | |
| expanded["env_vars"] = expand_template_parts( | |
| expanded.get("env_vars", ""), env_var_templates, "\n" | |
| ) | |
| models.append(expanded) | |
| return models | |
| catalog = load_model_catalog(root / ".github" / "benchmark" / "sglang_benchmark_models.json") | |
| def slugify(text: str) -> str: | |
| return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") or "unknown" | |
| def precision_from_model(model: str): | |
| upper = model.upper() | |
| if "MXFP4" in upper or "FP4" in upper: | |
| return "fp4" | |
| if "FP8" in upper: | |
| return "fp8" | |
| return None | |
| def model_name_for_variant(variant: dict) -> str: | |
| if variant.get("dashboard_model"): | |
| return str(variant["dashboard_model"]) | |
| display = str(variant["display"]) | |
| return display.replace(" TP", "-tp").replace(" ", "-") | |
| prefix_map = {str(item["prefix"]): item for item in catalog} | |
| def parse_result_filename(path: Path): | |
| prefix, isl, osl, concurrency, ratio = path.stem.rsplit("-", 4) | |
| return prefix, int(isl), int(osl), int(concurrency), float(ratio) if ratio else 0.0 | |
| grouped = defaultdict(list) | |
| for path in sorted(root.glob("*.json")): | |
| if path.name in {"sglang_benchmark_summary.json", "regression_report.json"}: | |
| continue | |
| try: | |
| payload = json.loads(path.read_text(encoding="utf-8")) | |
| except (OSError, UnicodeDecodeError, json.JSONDecodeError): | |
| continue | |
| if "output_throughput" not in payload: | |
| continue | |
| custom_dashboard_flag = str( | |
| payload.get("custom_dashboard_publish_allowed", True) | |
| ).strip().lower() | |
| if custom_dashboard_flag in {"0", "false", "no"}: | |
| continue | |
| prefix, isl, osl, concurrency, ratio = parse_result_filename(path) | |
| variant = prefix_map.get(prefix) | |
| if variant is None: | |
| raise SystemExit(f"Unknown benchmark prefix in result file {path.name}: {prefix}") | |
| model = model_name_for_variant(variant) | |
| precision = precision_from_model(model) | |
| workload_label = str( | |
| payload.get("workload_label") | |
| or variant.get("workload_label") | |
| or os.environ.get("WORKLOAD_LABEL") | |
| or "SGLang-OOB" | |
| ) | |
| backend = str(payload.get("benchmark_backend") or workload_label) | |
| atom_source_sha = str(payload.get("atom_source_sha", "")) or "unknown" | |
| sha7 = atom_source_sha[:7] if atom_source_sha else "unknown" | |
| day = time.strftime("%Y-%m-%d", time.gmtime()) | |
| grouped[(day, sha7, backend, model, isl, osl)].append( | |
| (path, payload, precision, concurrency, ratio) | |
| ) | |
| written_files = set() | |
| for (day, sha7, backend, model, isl, osl), entries in sorted(grouped.items()): | |
| entries.sort(key=lambda item: (item[3], item[4])) | |
| timestamp_ms = int(time.time() * 1000) | |
| precision = entries[0][2] | |
| first_payload = entries[0][1] | |
| tp = first_payload.get("tensor_parallel_size") | |
| dp = first_payload.get("data_parallel_size", 1) | |
| ep = first_payload.get("expert_parallel_size") | |
| try: | |
| tp_int = int(tp) if tp not in (None, "", 0) else None | |
| except (TypeError, ValueError): | |
| tp_int = None | |
| try: | |
| dp_int = int(dp) if dp not in (None, "", 0) else 1 | |
| except (TypeError, ValueError): | |
| dp_int = 1 | |
| try: | |
| ep_int = int(ep) if ep not in (None, "", 0) else None | |
| except (TypeError, ValueError): | |
| ep_int = None | |
| enable_dp_attention = bool(first_payload.get("enable_dp_attention")) | |
| total_gpu = (tp_int or 1) if enable_dp_attention else (tp_int or 1) * max(dp_int, 1) | |
| model_slug = slugify(model) | |
| backend_slug = slugify(backend) | |
| run_id = ( | |
| f"atom_sglang_{backend_slug}_{day.replace('-', '')}_{model_slug}_" | |
| f"isl{isl}_osl{osl}_{sha7}_{os.environ['GITHUB_RUN_ID']}" | |
| ) | |
| config_label = f"atom-sglang_{backend_slug}_{model_slug}" | |
| if precision: | |
| config_label += f"_{precision}" | |
| if tp_int: | |
| config_label += f"_tp{tp_int}" | |
| if dp_int and dp_int != 1: | |
| config_label += f"_dp{dp_int}" | |
| if ep_int: | |
| config_label += f"_ep{ep_int}" | |
| points = [] | |
| for _, payload, precision, concurrency, ratio in entries: | |
| output_tput = payload.get("output_throughput") | |
| total_tput = payload.get("total_token_throughput") | |
| points.append( | |
| { | |
| "isl": isl, | |
| "osl": osl, | |
| "concurrency": concurrency, | |
| "ratio": ratio, | |
| "client_bench": "sglang bench_serving", | |
| "ttft_ms": payload.get("mean_ttft_ms"), | |
| "ttft_p99": payload.get("p99_ttft_ms"), | |
| "tpot_ms": payload.get("mean_tpot_ms"), | |
| "tpot_p99": payload.get("p99_tpot_ms"), | |
| "itl_ms": payload.get("mean_itl_ms"), | |
| "e2el_ms": payload.get("mean_e2el_ms"), | |
| "interactivity": (output_tput / concurrency) if output_tput and concurrency else None, | |
| "tput_per_gpu": (total_tput / total_gpu) if total_tput and total_gpu else None, | |
| "output_tput_per_gpu": (output_tput / total_gpu) if output_tput and total_gpu else None, | |
| "input_tput_per_gpu": None, | |
| "output_tput": output_tput, | |
| "total_tput": total_tput, | |
| "req_tput": payload.get("request_throughput"), | |
| "completed": payload.get("completed"), | |
| "duration": payload.get("duration"), | |
| "num_prompts": payload.get("num_prompts"), | |
| "prefill_tp": None, | |
| "decode_tp": None, | |
| "tensor_parallel_size": tp_int, | |
| "data_parallel_size": dp_int, | |
| "expert_parallel_size": ep_int, | |
| "enable_dp_attention": payload.get("enable_dp_attention"), | |
| "num_prefill_gpu": None, | |
| "num_decode_gpu": None, | |
| "total_gpu": total_gpu, | |
| "gpu_name": payload.get("gpu_name"), | |
| "image": payload.get("sglang_image_tag") or payload.get("docker_image"), | |
| "rocm": payload.get("rocm_version"), | |
| "run_url": os.environ["RUN_URL"], | |
| "model_path": payload.get("model_id"), | |
| "point_date": day, | |
| "precision": precision, | |
| "hardware": "mi355x", | |
| } | |
| ) | |
| run = { | |
| "run_id": run_id, | |
| "date": day, | |
| "timestamp": timestamp_ms, | |
| "model": model, | |
| "backend": backend, | |
| "config_label": config_label, | |
| "precision": precision, | |
| "source": "ATOM SGLang Benchmark", | |
| "client_bench": "sglang bench_serving", | |
| "hardware": "mi355x", | |
| "commit": { | |
| "id": first_payload.get("atom_source_sha"), | |
| "message": f"ATOM benchmark run {os.environ['GITHUB_RUN_ID']}", | |
| "timestamp": datetime.now(timezone.utc).isoformat(), | |
| "url": f"https://github.com/{os.environ['ATOM_SOURCE_REPOSITORY']}/commit/{first_payload.get('atom_source_sha', '')}", | |
| "author": {}, | |
| "committer": {}, | |
| }, | |
| "points": points, | |
| "gsm8k": None, | |
| } | |
| run_file = llm_booster_data_dir / f"{run_id}.json" | |
| run_file.write_text(json.dumps(run, indent=2) + "\n", encoding="utf-8") | |
| written_files.add(run_file) | |
| rebuild_index(index_root_for(llm_booster_data_dir)) | |
| print(f"Wrote {len(written_files)} LLM-Booster ATOM SGLang run files.") | |
| PY | |
| - name: Upload LLM-Booster dashboard preview artifact | |
| if: needs.resolve-atom-source.outputs.upload_to_custom_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' && steps.llmbooster-key.outputs.available == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sglang-benchmark-llm-booster-preview | |
| path: | | |
| llm-booster-repo/dashboard/data/index.json | |
| llm-booster-repo/dashboard/data/sglang-atom/*.json | |
| - name: Push LLM-Booster dashboard data | |
| if: needs.resolve-atom-source.outputs.upload_to_custom_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' && steps.llmbooster-key.outputs.available == 'true' | |
| working-directory: llm-booster-repo | |
| env: | |
| GIT_AUTHOR_NAME: github-actions[bot] | |
| GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com | |
| GIT_COMMITTER_NAME: github-actions[bot] | |
| GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com | |
| run: | | |
| git add dashboard/data/index.json dashboard/data/sglang-atom | |
| if git diff --cached --quiet; then | |
| echo "No LLM-Booster dashboard data changes to push." | |
| exit 0 | |
| fi | |
| git commit -m "Update ATOM SGLang benchmark dashboard data" | |
| git push origin HEAD:main | |
| - name: Download baseline from previous successful main run | |
| if: needs.resolve-atom-source.outputs.publish_to_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' | |
| id: baseline | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| mapfile -t CANDIDATE_RUN_IDS < <( | |
| { | |
| gh run list \ | |
| --workflow="ATOM SGLang Benchmark" \ | |
| --branch=main \ | |
| --event=workflow_dispatch \ | |
| --status=success \ | |
| --limit=10 \ | |
| --json databaseId \ | |
| --jq '.[].databaseId // empty' || true | |
| } | |
| ) | |
| BASELINE_DIR="" | |
| for PREV_RUN_ID in "${CANDIDATE_RUN_IDS[@]}"; do | |
| if [ -z "$PREV_RUN_ID" ] || [ "$PREV_RUN_ID" = "${{ github.run_id }}" ]; then | |
| continue | |
| fi | |
| echo "Checking baseline candidate run #$PREV_RUN_ID" | |
| rm -rf /tmp/baseline_candidate /tmp/baseline | |
| mkdir -p /tmp/baseline_candidate | |
| if ! gh run download "$PREV_RUN_ID" --dir /tmp/baseline_candidate; then | |
| echo "::warning::Failed to download baseline artifacts from run #$PREV_RUN_ID" | |
| continue | |
| fi | |
| if python3 .github/scripts/plugin_benchmark_validate_baseline.py /tmp/baseline_candidate; then | |
| mv /tmp/baseline_candidate /tmp/baseline | |
| BASELINE_DIR="/tmp/baseline" | |
| echo "Using baseline from run #$PREV_RUN_ID" | |
| break | |
| fi | |
| echo "Skipping run #$PREV_RUN_ID because it is not a dashboard-eligible baseline." | |
| done | |
| echo "baseline_dir=${BASELINE_DIR}" >> "$GITHUB_OUTPUT" | |
| - name: Note skipped dashboard processing | |
| if: needs.resolve-atom-source.outputs.publish_to_dashboard == 'true' && steps.summary-stats.outputs.passed_cases == '0' | |
| run: | | |
| printf '\nNo successful benchmark result JSONs were produced, so regression comparison and dashboard upload were skipped.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| - name: Summarize regression against baseline | |
| if: needs.resolve-atom-source.outputs.publish_to_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' | |
| id: check-regression | |
| run: | | |
| BASELINE_ARG="" | |
| if [ -n "${{ steps.baseline.outputs.baseline_dir }}" ]; then | |
| BASELINE_ARG="--baseline-dir ${{ steps.baseline.outputs.baseline_dir }}" | |
| else | |
| printf '\nNo eligible baseline run was found. Showing current successful results without regression deltas.\n' \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| python3 .github/scripts/plugin_benchmark_regression.py \ | |
| --result-dir . \ | |
| $BASELINE_ARG \ | |
| --output-json regression_report.json \ | |
| >> "$GITHUB_STEP_SUMMARY" || true | |
| if [ -f regression_report.json ]; then | |
| REG_COUNT=$(python3 -c "import json; print(json.load(open('regression_report.json', encoding='utf-8'))['regression_count'])") | |
| if [ "$REG_COUNT" -gt 0 ]; then | |
| echo "has_regression=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_regression=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| else | |
| echo "has_regression=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Upload regression report | |
| if: needs.resolve-atom-source.outputs.publish_to_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' && steps.check-regression.outputs.has_regression == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sglang-benchmark-regression-report | |
| path: regression_report.json | |
| - name: Transform results for benchmark dashboard | |
| if: needs.resolve-atom-source.outputs.publish_to_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' | |
| run: | | |
| python3 .github/scripts/plugin_benchmark_to_dashboard.py \ | |
| . \ | |
| --output benchmark-action-input.json \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --default-backend "ATOM-SGLang" | |
| - name: Prepare existing dashboard data | |
| if: needs.resolve-atom-source.outputs.publish_to_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' | |
| id: prepare_dashboard_data | |
| uses: ./.github/actions/prepare-dashboard-data | |
| with: | |
| data-path: benchmark-dashboard/data.js | |
| - name: Store benchmark result to dashboard | |
| if: needs.resolve-atom-source.outputs.publish_to_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' | |
| uses: benchmark-action/github-action-benchmark@v1 | |
| with: | |
| tool: customBiggerIsBetter | |
| output-file-path: benchmark-action-input.json | |
| gh-pages-branch: gh-pages | |
| benchmark-data-dir-path: benchmark-dashboard | |
| auto-push: false | |
| alert-threshold: "80%" | |
| comment-on-alert: true | |
| fail-on-alert: false | |
| max-items-in-chart: 300 | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Validate dashboard data after write | |
| if: needs.resolve-atom-source.outputs.publish_to_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' | |
| uses: ./.github/actions/validate-dashboard-data | |
| with: | |
| data-path: benchmark-dashboard/data.js | |
| min-entries: ${{ steps.prepare_dashboard_data.outputs.entry_count }} | |
| - name: Push dashboard data to gh-pages | |
| if: needs.resolve-atom-source.outputs.publish_to_dashboard == 'true' && steps.summary-stats.outputs.passed_cases != '0' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git fetch origin gh-pages | |
| git checkout gh-pages | |
| if [ ! -f benchmark-dashboard/data.js ]; then | |
| echo "::error::benchmark-dashboard/data.js was not produced by github-action-benchmark" | |
| exit 1 | |
| fi | |
| cp benchmark-dashboard/data.js /tmp/sglang-benchmark-data.js | |
| # Rebuild the publish branch from origin/gh-pages so only dashboard data is updated. | |
| git checkout -B gh-pages-data-only origin/gh-pages | |
| mkdir -p benchmark-dashboard | |
| cp /tmp/sglang-benchmark-data.js benchmark-dashboard/data.js | |
| git add benchmark-dashboard/data.js | |
| git diff --cached --quiet || git commit -m "Update SGLang benchmark dashboard data" | |
| git push origin HEAD:gh-pages | |