Repository navigation
Atomesh Benchmark #4
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: Atomesh Benchmark | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'atom/mesh/**' | |
| - '.github/benchmark/models_atomesh.yaml' | |
| - '.github/scripts/atomesh/**' | |
| - 'atom/entrypoints/openai/**' | |
| - '.github/scripts/slurm_submit_helpers.sh' | |
| - '.github/workflows/atomesh-benchmark.yaml' | |
| pull_request: | |
| branches: [main] | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| paths: | |
| - 'atom/mesh/**' | |
| - '.github/benchmark/models_atomesh.yaml' | |
| - '.github/scripts/atomesh/**' | |
| - 'atom/entrypoints/openai/**' | |
| - '.github/scripts/slurm_submit_helpers.sh' | |
| - '.github/workflows/atomesh-benchmark.yaml' | |
| schedule: | |
| # TW weekly: Saturday 00:00 Beijing time (Friday 16:00 UTC). | |
| - cron: '0 16 * * 5' | |
| # Spur MI350X nightly at 00:10 Beijing time (16:10 UTC). | |
| - cron: '10 16 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| suite: | |
| description: 'Benchmark suite' | |
| required: false | |
| default: nightly | |
| type: choice | |
| options: | |
| - smoke | |
| - nightly | |
| - weekly | |
| - accuracy | |
| rdma_smoke_nodes: | |
| description: 'Comma-separated Slurm candidate nodes for the Mooncake RDMA smoke test; Slurm selects two when multiple nodes are listed' | |
| required: false | |
| default: '' | |
| type: string | |
| rdma_smoke_node_count: | |
| description: 'Number of Slurm nodes for the Mooncake RDMA smoke test' | |
| required: false | |
| default: '2' | |
| type: choice | |
| options: | |
| - '1' | |
| - '2' | |
| run_all_models: | |
| description: 'Run all models from models_atomesh.yaml' | |
| required: false | |
| default: false | |
| type: boolean | |
| model_names: | |
| description: 'When run_all_models=false, run all cases in the selected suite for these model families. Comma-separated: DeepSeek-V4-Pro,DeepSeek-V4-Pro-0813,GLM-5.2-MXFP4,Kimi-K3-MXFP4,Kimi-K2.5-MXFP4,MiniMax-M3-MXFP4,MiniMax-M3-MXFP8' | |
| required: false | |
| default: '' | |
| type: string | |
| case_names: | |
| description: | | |
| Overrides model_names. Enter comma-separated case names. | |
| DeepSeek: | |
| ds-v4-1p1d-tp8-smoke,ds-v4-1p1d-tp8,ds-v4-1p1d-dpa-tp8, | |
| ds-v4-1p1d-tp8-mtp3,ds-v4-1p1d-dpa-tp8-mtp1 | |
| DeepSeek-V4-Pro-0813 (weekly): | |
| ds-v4-0813-1p1d-dpa-tp8-dspark3-agentic-no-offload-c1, | |
| ds-v4-0813-1p1d-dpa-tp8-dspark3-agentic-no-offload-c2, | |
| ds-v4-0813-1p1d-dpa-tp8-dspark3-agentic-no-offload-c16, | |
| ds-v4-0813-1p1d-dpa-tp8-dspark3-agentic-no-offload-c32, | |
| ds-v4-0813-1p1d-dpa-tp8-dspark3-agentic-no-offload-c128, | |
| ds-v4-0813-1p1d-dpa-tp8-dspark3-agentic-no-offload-c192, | |
| ds-v4-0813-1p1d-dpa-tp8-dspark3-agentic-no-offload-c256 | |
| DeepSeek-V4-Pro-0813 (accuracy): | |
| ds-v4-0813-1p1d-tp8-dspark3-gsm8k-only-c16 | |
| GLM: | |
| glm-52-mxfp4-1p1d-tp4,glm-52-mxfp4-1p1d-tp8, | |
| glm-52-mxfp4-1p1d-tp4-agentic-1m-c1,glm-52-mxfp4-1p1d-tp4-agentic-1m-c2, | |
| glm-52-mxfp4-1p1d-tp4-agentic-1m-c4,glm-52-mxfp4-1p1d-tp4-agentic-1m-c8, | |
| glm-52-mxfp4-1p1d-cpp4-dcp4-agentic-lmcache-1m-c32, | |
| glm-52-mxfp4-1p1d-cpp4-dcp4-agentic-lmcache-1m-c40, | |
| glm-52-mxfp4-1p1d-cpp4-dcp4-agentic-lmcache-1m-c48, | |
| glm-52-mxfp4-1p1d-cpp4-dcp4-agentic-lmcache-1m-c56, | |
| glm-52-mxfp4-1p1d-tp4-dpa-agentic-lmcache-1m-c24, | |
| glm-52-mxfp4-1p1d-tp4-dpa-agentic-lmcache-1m-c32, | |
| glm-52-mxfp4-1p1d-tp4-dpa-agentic-lmcache-1m-c48 | |
| Kimi-K3: | |
| kimi-k3-mxfp4-1p1d-tp8-dspark7-agentic-1m-c1, | |
| kimi-k3-mxfp4-1p1d-tp8-dspark7-agentic-1m-c2, | |
| kimi-k3-mxfp4-1p1d-tp8-dspark7-agentic-1m-c4, | |
| kimi-k3-mxfp4-1p1d-tp8-dcp8-dspark3-agentic-lmcache-1m-c8, | |
| kimi-k3-mxfp4-1p1d-tp8-dcp8-dspark3-agentic-lmcache-1m-c12, | |
| kimi-k3-mxfp4-1p1d-tp8-dcp8-dspark3-agentic-lmcache-1m-c14, | |
| kimi-k3-mxfp4-1p1d-tp8-dcp8-dspark3-agentic-lmcache-1m-c16, | |
| kimi-k3-mxfp4-1p1d-tp8-dcp8-agentic-lmcache-1m-c32, | |
| kimi-k3-mxfp4-1p1d-tp8-dcp8-agentic-lmcache-1m-c40, | |
| kimi-k3-mxfp4-1p1d-tp8-dcp8-agentic-lmcache-1m-c56, | |
| kimi-k3-mxfp4-1p1d-tp8-dcp8-agentic-lmcache-1m-c64 | |
| Kimi-K2.5: | |
| kimi-k25-1p1d-tp4,kimi-k25-1p1d-tp4-tp8, | |
| kimi-k25-2p1d-tp4,kimi-k25-1p2d-tp4 | |
| MiniMax: | |
| minimax-m3-fp4-1p1d-tp4,minimax-m3-fp4-2p1d-dpa-tp4, | |
| minimax-m3-fp4-1p1d-tp4-eagle3,minimax-m3-fp4-2p1d-dpa-tp4-eagle3, | |
| minimax-m3-fp8-1p1d-tp4,minimax-m3-fp8-2p1d-dpa-tp4, | |
| minimax-m3-fp8-1p1d-tp4-eagle3,minimax-m3-fp8-2p1d-dpa-tp4-eagle3 | |
| required: false | |
| default: '' | |
| type: string | |
| atomesh_image: | |
| description: 'Override ATOM docker image' | |
| required: false | |
| default: rocm/atom-dev:latest | |
| type: string | |
| atomesh_slurm_submit_runner: | |
| description: 'GitHub runner label used to submit Slurm jobs: atomesh-cicd = TW MI355X, atomesh-cicd-mi350 = Spur MI350X, atomesh-cicd-mi355-crusoe = Crusoe MI355X' | |
| required: false | |
| default: atomesh-cicd | |
| type: choice | |
| options: | |
| - atomesh-cicd | |
| - atomesh-cicd-mi350 | |
| - atomesh-cicd-mi355-crusoe | |
| atomesh_slurm_account: | |
| description: 'Slurm account: amd-atom uses amd-atom-qos (salloc -A amd-atom --qos=amd-atom-qos); TW: amd-frameworks[42/44/47], amd-tw[36/43]; Crusoe v2 uses amd-aifw-dev' | |
| required: false | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - amd-atom | |
| - amd-frameworks | |
| - amd-tw | |
| atomesh_slurm_partition: | |
| description: 'TW Slurm partition: amd-frameworks[42/44/47], amd-tw[36/43]; Crusoe v2 uses the default partition' | |
| required: false | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - amd-frameworks | |
| - amd-tw | |
| - amd-spur | |
| atomesh_single_node: | |
| description: 'Single-node P/D placement; atomesh-cicd auto selects one node from its configured candidate pool' | |
| required: false | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - pit2-p03-g01 | |
| - pit2-p03-g03 | |
| - pit2-p03-g07 | |
| - pit2-p03-g08 | |
| - pit2-p03-g09 | |
| - pit2-p03-g10 | |
| - pit2-p03-g12 | |
| - pit2-p03-g13 | |
| - pit2-p03-g15 | |
| - pit2-p03-g17 | |
| - pit2-p03-g19 | |
| - pit2-p03-g20 | |
| - pit2-p03-g23 | |
| - pit2-p03-g24 | |
| - pit2-p03-g25 | |
| - pit2-p03-g27 | |
| - pit2-p03-g42 | |
| atomesh_1p1d_nodes: | |
| description: 'Comma-separated candidate nodes for 1P1D; atomesh-cicd auto uses its configured pool; explicit nodes must belong to that pool' | |
| required: false | |
| default: auto | |
| type: string | |
| atomesh_2p1d_nodes: | |
| description: 'Comma-separated candidate nodes for 2P1D; atomesh-cicd auto uses its configured pool; explicit nodes must belong to that pool' | |
| required: false | |
| default: auto | |
| type: string | |
| benchmark_concurrency: | |
| description: 'Optional benchmark concurrency override, e.g. 1,2,4 (no quotes)' | |
| required: false | |
| default: '' | |
| type: string | |
| eval_concurrency: | |
| description: 'Optional accuracy result-concurrency override; SWE-bench agent workers come from models_atomesh.yaml' | |
| required: false | |
| default: '' | |
| type: string | |
| run_model_benchmark: | |
| description: 'Run model Slurm benchmark for workflow_dispatch' | |
| required: false | |
| default: false | |
| type: boolean | |
| publish_dashboard: | |
| description: 'Publish workflow_dispatch results to the benchmark dashboard' | |
| required: false | |
| default: false | |
| type: boolean | |
| env: | |
| ATOMESH_NODE_POOL: pit2-p03-g01,pit2-p03-g03,pit2-p03-g07,pit2-p03-g08,pit2-p03-g09,pit2-p03-g10,pit2-p03-g12,pit2-p03-g13,pit2-p03-g15,pit2-p03-g17,pit2-p03-g19,pit2-p03-g20,pit2-p03-g23,pit2-p03-g24,pit2-p03-g25,pit2-p03-g27,pit2-p03-g42 | |
| # GID 1 selects IPv4 on TW; GID 0 is IPv6 link-local. | |
| NCCL_IB_GID_INDEX: '1' | |
| ATOMESH_SINGLE_NODE: ${{ github.event_name == 'workflow_dispatch' && inputs.atomesh_single_node || 'auto' }} | |
| ATOMESH_SLURM_ACCOUNT: ${{ github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_account != 'auto' && inputs.atomesh_slurm_account || '' }} | |
| ATOMESH_SLURM_PARTITION: ${{ github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_partition != 'auto' && inputs.atomesh_slurm_partition || (((github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-crusoe-mi355' || (github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-mi355-crusoe') && 'amd-spur' || '') }} | |
| ATOMESH_SLURM_SUBMIT_RUNNER: ${{ github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd') }} | |
| ATOMESH_DASHBOARD_HARDWARE: ${{ (github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-mi350' && 'MI350X' || 'MI355X' }} | |
| ATOMESH_PD_RANK_MAPPING_POLICY: ${{ (github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-mi350' && 'idx2idx' || 'none' }} | |
| ATOMESH_LOG_ROOT: ${{ ((github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-mi350' && '/data/${USER}/logs/ATOMESH_LOG/' || (github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-mi355-crusoe' && '/home/junyyang/ATOMesh_ACTION_RUNNER/ATOMesh_LOG/' || (github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-crusoe-mi355' && '/shared_nfs/ATOMESH_CI/LOG/' || '/it-share/ATOMESH_LOG/') }} | |
| ATOMESH_MODEL_ROOT: ${{ (github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-mi350' && '/data/models2' || (((github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-crusoe-mi355' || (github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-mi355-crusoe') && '/shared_nfs/huggingface_models/amd' || '/mnt/models') }} | |
| ATOMESH_1P1D_NODES: ${{ github.event_name == 'workflow_dispatch' && inputs.atomesh_1p1d_nodes != 'auto' && inputs.atomesh_1p1d_nodes || ((github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-mi350' && 'ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-5,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-6,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-7,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-8,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-13,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-14,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-15,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-16' || '') }} | |
| ATOMESH_2P1D_NODES: ${{ github.event_name == 'workflow_dispatch' && inputs.atomesh_2p1d_nodes != 'auto' && inputs.atomesh_2p1d_nodes || ((github.event_name == 'workflow_dispatch' && inputs.atomesh_slurm_submit_runner || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd')) == 'atomesh-cicd-mi350' && 'ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-5,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-6,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-7,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-8,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-13,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-14,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-15,ml-ai-ubuntu-gpu-mi350x8-2304gb-fabric-16' || '') }} | |
| concurrency: | |
| # Keep manual benchmark dispatches independent so different cases on the same branch can run together. | |
| group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || (github.event_name == 'schedule' && github.event.schedule == '10 16 * * *' && 'atomesh-cicd-mi350' || 'atomesh-cicd') }} | |
| cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' && github.ref != 'refs/heads/main' }} | |
| permissions: | |
| actions: read | |
| contents: write | |
| jobs: | |
| load-config: | |
| if: ${{ !github.event.pull_request || github.event.pull_request.draft == false }} | |
| name: Load ATOMesh benchmark config | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix_json: ${{ steps.matrix.outputs.matrix_json }} | |
| has_matrix: ${{ steps.matrix.outputs.has_matrix }} | |
| image: ${{ steps.image.outputs.image }} | |
| steps: | |
| - name: Checkout ATOM repo | |
| uses: actions/checkout@v6 | |
| - name: Install YAML parser | |
| run: python3 -m pip install pyyaml | |
| - name: Test ATOMesh config and launch scripts | |
| run: python3 -m unittest discover -s tests -p "test_atomesh_*.py" | |
| - name: Find ATOMesh latest nightly image | |
| id: image | |
| env: | |
| INPUT_ATOMESH_IMAGE: ${{ github.event_name == 'workflow_dispatch' && inputs.atomesh_image || '' }} | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import urllib.parse | |
| import urllib.request | |
| from datetime import datetime, timedelta, timezone | |
| from pathlib import Path | |
| import yaml | |
| cfg = yaml.safe_load(Path(".github/benchmark/models_atomesh.yaml").read_text(encoding="utf-8")) or {} | |
| default_image = str(cfg.get("backends", {}).get("atom", {}).get("image", "")).strip() | |
| image = os.environ.get("INPUT_ATOMESH_IMAGE", "").strip() or default_image | |
| def docker_tags(prefix: str) -> list[dict]: | |
| url = ( | |
| "https://hub.docker.com/v2/repositories/rocm/atom-dev/tags?" | |
| + urllib.parse.urlencode({"page_size": 100, "name": prefix}) | |
| ) | |
| tags = [] | |
| while url: | |
| with urllib.request.urlopen(url, timeout=30) as response: | |
| data = json.load(response) | |
| tags.extend( | |
| tag for tag in data.get("results", []) | |
| if str(tag.get("name", "")).startswith(prefix) | |
| ) | |
| url = data.get("next") | |
| return tags | |
| def latest_nightly_for(day) -> str | None: | |
| prefix = f"nightly_{day:%Y%m%d}" | |
| tags = docker_tags(prefix) | |
| if not tags: | |
| return None | |
| tags.sort( | |
| key=lambda tag: (str(tag.get("last_updated", "")), str(tag.get("name", ""))), | |
| reverse=True, | |
| ) | |
| return f"rocm/atom-dev:{tags[0]['name']}" | |
| def docker_tag_digest(tag_name: str) -> str: | |
| url = f"https://hub.docker.com/v2/repositories/rocm/atom-dev/tags/{urllib.parse.quote(tag_name)}" | |
| with urllib.request.urlopen(url, timeout=30) as response: | |
| data = json.load(response) | |
| if data.get("digest"): | |
| return str(data["digest"]) | |
| for image_info in data.get("images", []): | |
| if image_info.get("digest"): | |
| return str(image_info["digest"]) | |
| return "" | |
| resolved_image = image | |
| if image == "rocm/atom-dev:latest": | |
| beijing_tz = timezone(timedelta(hours=8)) | |
| today = datetime.now(beijing_tz).date() | |
| resolved_image = latest_nightly_for(today) | |
| if resolved_image is None: | |
| resolved_image = latest_nightly_for(today - timedelta(days=1)) | |
| if resolved_image is None: | |
| print("::warning::Cannot find rocm/atom-dev nightly tag for today or yesterday; falling back to rocm/atom-dev:latest.") | |
| resolved_image = image | |
| image_digest = "" | |
| if resolved_image == "rocm/atom-dev:latest": | |
| image_digest = docker_tag_digest("latest") | |
| if image_digest: | |
| print(f"ATOMesh image digest: {image_digest}") | |
| else: | |
| print("::warning::Cannot resolve digest for rocm/atom-dev:latest.") | |
| print(f"ATOMesh image: {image} -> {resolved_image}") | |
| with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output: | |
| output.write(f"image={resolved_image}\n") | |
| output.write(f"image_digest={image_digest}\n") | |
| step_summary = os.environ.get("GITHUB_STEP_SUMMARY") | |
| if step_summary and image_digest: | |
| with Path(step_summary).open("a", encoding="utf-8") as summary: | |
| summary.write(f"ATOMesh image digest: `{image_digest}`\n") | |
| PY | |
| - name: Build benchmark matrix | |
| id: matrix | |
| env: | |
| SUITE: ${{ (github.event_name == 'schedule' || github.event_name == 'push') && 'nightly' || (github.event_name == 'workflow_dispatch' && inputs.suite || 'smoke') }} | |
| RUN_ALL_MODELS: ${{ github.event_name == 'workflow_dispatch' && inputs.run_all_models || github.event_name != 'workflow_dispatch' }} | |
| MODEL_NAMES: ${{ github.event_name == 'workflow_dispatch' && inputs.model_names || '' }} | |
| CASE_NAMES: ${{ github.event_name == 'workflow_dispatch' && inputs.case_names || '' }} | |
| EVENT_SCHEDULE: ${{ github.event_name == 'schedule' && github.event.schedule || '' }} | |
| ATOMESH_IMAGE: ${{ steps.image.outputs.image }} | |
| ATOMESH_BENCHMARK_CONCURRENCY: ${{ github.event_name == 'workflow_dispatch' && inputs.benchmark_concurrency || '' }} | |
| ATOMESH_EVAL_CONCURRENCY: ${{ github.event_name == 'workflow_dispatch' && inputs.eval_concurrency || '' }} | |
| run: | | |
| set -euo pipefail | |
| MODEL_NAME="" | |
| CASE_NAME="" | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then | |
| if [[ -n "${CASE_NAMES}" ]]; then | |
| CASE_NAME="${CASE_NAMES}" | |
| elif [[ "${RUN_ALL_MODELS}" != "true" ]]; then | |
| if [[ -z "${MODEL_NAMES}" && "${SUITE}" != "smoke" ]]; then | |
| echo "::error::Set run_all_models=true, provide model_names, or provide case_names." | |
| exit 1 | |
| fi | |
| MODEL_NAME="${MODEL_NAMES}" | |
| fi | |
| fi | |
| if [[ "${SUITE}" == "smoke" && -z "${CASE_NAME}" && -z "${MODEL_NAME}" && "${RUN_ALL_MODELS}" != "true" ]]; then | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| matrix = {"include": []} | |
| Path("atomesh-matrix.json").write_text( | |
| json.dumps(matrix, indent=2), | |
| encoding="utf-8", | |
| ) | |
| with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output: | |
| output.write('matrix_json={"include":[]}\n') | |
| output.write("has_matrix=false\n") | |
| print("RDMA smoke workflow: skipping benchmark matrix generation") | |
| PY | |
| cat atomesh-matrix.json | |
| exit 0 | |
| fi | |
| if [[ "${GITHUB_EVENT_NAME}" == "schedule" && "${EVENT_SCHEDULE}" == "0 16 * * 5" ]]; then | |
| ATOMESH_SLURM_ACCOUNT=amd-frameworks \ | |
| ATOMESH_SLURM_PARTITION=amd-frameworks \ | |
| ATOMESH_SLURM_SUBMIT_RUNNER=atomesh-cicd \ | |
| python3 .github/scripts/atomesh/pd_matrix.py \ | |
| --suite nightly \ | |
| --model DeepSeek-V4-Pro \ | |
| ${ATOMESH_IMAGE:+--image "$ATOMESH_IMAGE"} \ | |
| --output atomesh-matrix-deepseek.json | |
| ATOMESH_SLURM_ACCOUNT=amd-frameworks \ | |
| ATOMESH_SLURM_PARTITION=amd-frameworks \ | |
| ATOMESH_SLURM_SUBMIT_RUNNER=atomesh-cicd \ | |
| python3 .github/scripts/atomesh/pd_matrix.py \ | |
| --suite weekly \ | |
| --model DeepSeek-V4-Pro-0813 \ | |
| ${ATOMESH_IMAGE:+--image "$ATOMESH_IMAGE"} \ | |
| --output atomesh-matrix-v4-weekly.json | |
| ATOMESH_SLURM_ACCOUNT=amd-tw \ | |
| ATOMESH_SLURM_PARTITION=amd-tw \ | |
| ATOMESH_SLURM_SUBMIT_RUNNER=atomesh-cicd \ | |
| python3 .github/scripts/atomesh/pd_matrix.py \ | |
| --suite nightly \ | |
| --model GLM-5.2-MXFP4 \ | |
| --benchmark-kind aiperf_agentic \ | |
| ${ATOMESH_IMAGE:+--image "$ATOMESH_IMAGE"} \ | |
| --output atomesh-matrix-glm.json | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| deepseek = json.loads(Path("atomesh-matrix-deepseek.json").read_text(encoding="utf-8")).get("include", []) | |
| v4_weekly = json.loads(Path("atomesh-matrix-v4-weekly.json").read_text(encoding="utf-8")).get("include", []) | |
| glm_agentic = json.loads(Path("atomesh-matrix-glm.json").read_text(encoding="utf-8")).get("include", []) | |
| matrix = {"include": deepseek + v4_weekly + glm_agentic} | |
| Path("atomesh-matrix.json").write_text( | |
| json.dumps(matrix, indent=2), | |
| encoding="utf-8", | |
| ) | |
| with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output: | |
| output.write(f"matrix_json={json.dumps(matrix, separators=(',', ':'))}\n") | |
| output.write(f"has_matrix={'true' if matrix['include'] else 'false'}\n") | |
| print( | |
| "Scheduled TW weekly matrix: " | |
| f"{len(deepseek)} DeepSeek case(s), {len(v4_weekly)} V4-0813 weekly case(s), " | |
| f"{len(glm_agentic)} GLM agentic case(s)" | |
| ) | |
| PY | |
| else | |
| python3 .github/scripts/atomesh/pd_matrix.py \ | |
| --suite "$SUITE" \ | |
| ${MODEL_NAME:+--model "$MODEL_NAME"} \ | |
| ${CASE_NAME:+--case "$CASE_NAME"} \ | |
| ${ATOMESH_IMAGE:+--image "$ATOMESH_IMAGE"} \ | |
| ${ATOMESH_BENCHMARK_CONCURRENCY:+--benchmark-concurrency "$ATOMESH_BENCHMARK_CONCURRENCY"} \ | |
| ${ATOMESH_EVAL_CONCURRENCY:+--eval-concurrency "$ATOMESH_EVAL_CONCURRENCY"} \ | |
| --output atomesh-matrix.json \ | |
| --github-output | |
| fi | |
| cat atomesh-matrix.json | |
| - name: Validate ATOMesh P/D matrix | |
| if: ${{ steps.matrix.outputs.has_matrix == 'true' }} | |
| env: | |
| RESULT_DIR: atomesh-dry-run-results | |
| run: | | |
| set -euo pipefail | |
| chmod +x .github/scripts/atomesh/*.sh .github/scripts/atomesh/*.py | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| from pathlib import Path | |
| matrix = json.loads(Path("atomesh-matrix.json").read_text(encoding="utf-8")) | |
| for cell in matrix.get("include", []): | |
| print(f"Validating {cell['id']}", flush=True) | |
| subprocess.run( | |
| [ | |
| "bash", | |
| ".github/scripts/atomesh/pd_submit.sh", | |
| "--cell-json", | |
| json.dumps(cell, separators=(",", ":")), | |
| "--result-dir", | |
| os.environ["RESULT_DIR"], | |
| "--dry-run", | |
| ], | |
| check=True, | |
| ) | |
| PY | |
| - name: Upload expanded matrix | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: atomesh-benchmark-matrix | |
| path: atomesh-matrix.json | |
| rdma-smoke-test: | |
| name: Mooncake RDMA smoke test | |
| needs: [load-config] | |
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.suite == 'smoke' }} | |
| runs-on: ${{ inputs.atomesh_slurm_submit_runner }} | |
| timeout-minutes: 75 | |
| env: | |
| MC_GID_INDEX: '1' | |
| RDMA_SMOKE_IMAGE: ${{ needs.load-config.outputs.image }} | |
| RDMA_SMOKE_NODES: ${{ inputs.rdma_smoke_nodes }} | |
| RDMA_SMOKE_NODE_COUNT: ${{ inputs.rdma_smoke_node_count }} | |
| RDMA_SMOKE_DECODE_HCA_MODE: all | |
| SLURM_SUBMIT_RUNNER: ${{ inputs.atomesh_slurm_submit_runner }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Run Mooncake RDMA transfer test on selected nodes | |
| id: rdma-smoke | |
| run: | | |
| set -euo pipefail | |
| export PATH="/usr/local/slurm-24.05.5.1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" | |
| if [[ -z "${RDMA_SMOKE_IMAGE}" ]]; then | |
| echo "::error::No ATOM image was resolved for the RDMA smoke test." | |
| exit 1 | |
| fi | |
| if [[ ! "${MC_GID_INDEX}" =~ ^[0-9]+$ ]]; then | |
| echo "::error::MC_GID_INDEX must be a non-negative integer." | |
| exit 2 | |
| fi | |
| nodes="${RDMA_SMOKE_NODES:-${ATOMESH_1P1D_NODES:-}}" | |
| nodes="${nodes// /}" | |
| if [[ "${SLURM_SUBMIT_RUNNER}" == "atomesh-cicd" ]]; then | |
| nodes="${nodes:-${ATOMESH_NODE_POOL}}" | |
| IFS=',' read -r -a requested_nodes <<< "${nodes}" | |
| for node in "${requested_nodes[@]}"; do | |
| if [[ ",${ATOMESH_NODE_POOL}," != *",${node},"* ]]; then | |
| echo "::error::Node ${node} is outside the atomesh-cicd candidate pool." | |
| exit 2 | |
| fi | |
| done | |
| fi | |
| RDMA_SMOKE_TARGET_NODE_COUNT="${RDMA_SMOKE_NODE_COUNT:-2}" | |
| if [[ -n "${nodes}" ]]; then | |
| IFS=',' read -r -a node_list <<< "${nodes}" | |
| node_count="${#node_list[@]}" | |
| if [[ "${node_count}" -gt "${RDMA_SMOKE_TARGET_NODE_COUNT}" ]]; then | |
| node_count="${RDMA_SMOKE_TARGET_NODE_COUNT}" | |
| fi | |
| echo "=== requesting ${node_count} node(s) from ${#node_list[@]} Slurm candidates ===" | |
| else | |
| # Allocate two nodes by default so both local and cross-node GDR are validated. | |
| node_count="${RDMA_SMOKE_TARGET_NODE_COUNT}" | |
| fi | |
| if [[ "${node_count}" -lt 1 ]]; then | |
| echo "::error::GPUDirect RDMA smoke testing requires at least one Slurm node." | |
| exit 1 | |
| fi | |
| current_user="$(id -un 2>/dev/null || id -u)" | |
| log_root="${ATOMESH_LOG_ROOT//\$\{USER\}/${current_user}}" | |
| log_root="${log_root//\$USER/${current_user}}" | |
| if [[ "$(hostname -s)" == "pit2-vm-amd-xl-02" ]]; then | |
| log_root="/share_nfs/ATOMESH_RUNNER/ATOMESH_LOG" | |
| fi | |
| run_dir="${log_root%/}/rdma-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| mkdir -p "${run_dir}" | |
| chmod 0777 "${run_dir}" | |
| export RDMA_SMOKE_RUN_DIR="${run_dir}" | |
| export RDMA_SMOKE_NODE_COUNT="${node_count}" | |
| CURRENT_USER="${current_user}" | |
| ATOMESH_CELL_ID="rdma-smoke" | |
| RESULT_DIR="${run_dir}" | |
| SLURM_JOB_NAME="rdma-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| SLURM_OUTPUT="${run_dir}/slurm-%j.out" | |
| SLURM_ERROR="${run_dir}/slurm-%j.err" | |
| SLURM_CANCEL_HELPER="${run_dir}/slurm-cancel.sh" | |
| SLURM_LOG_POLL_INTERVAL="${SLURM_LOG_POLL_INTERVAL:-15}" | |
| SLURM_ACCOUNT="${ATOMESH_SLURM_ACCOUNT:-}" | |
| SLURM_PARTITION="${ATOMESH_SLURM_PARTITION:-}" | |
| SLURM_QOS="" | |
| USES_SPUR_CONTROLLER=0 | |
| SPUR_CONTROLLER_ADDR="${SPUR_CONTROLLER_ADDR:-}" | |
| SPUR_ACCOUNTING_ADDR="" | |
| if [[ "${SLURM_SUBMIT_RUNNER}" == "atomesh-cicd-mi350" ]]; then | |
| USES_SPUR_CONTROLLER=1 | |
| SPUR_CONTROLLER_ADDR="http://134.199.196.72:6817" | |
| SPUR_ACCOUNTING_ADDR="http://134.199.196.72:6819" | |
| elif [[ "${SLURM_SUBMIT_RUNNER}" == "atomesh-cicd-mi355-crusoe" ]]; then | |
| USES_SPUR_CONTROLLER=1 | |
| SPUR_CONTROLLER_ADDR="http://crs-m2m-cpu-spur-v2-001.crusoe.amd.com:6817" | |
| SPUR_ACCOUNTING_ADDR="${SPUR_V2_ACCOUNTING_ADDR:-http://crs-m2m-cpu-spur-v2-001.crusoe.amd.com:6819}" | |
| SLURM_ACCOUNT="amd-aifw-dev" | |
| SLURM_PARTITION="" | |
| SLURM_QOS="amd-aifw-dev-qos" | |
| fi | |
| if [[ "$(hostname -s)" == "pit2-vm-amd-xl-02" ]]; then | |
| SLURM_PARTITION="amd-spur" | |
| SLURM_QOS="amd-frameworks-qos" | |
| fi | |
| if [[ "${SLURM_ACCOUNT}" == "amd-atom" ]]; then | |
| SLURM_QOS="amd-atom-qos" | |
| fi | |
| source "${GITHUB_WORKSPACE}/.github/scripts/slurm_submit_helpers.sh" | |
| detect_slurm_backend | |
| install_slurm_cancel_traps | |
| rank_script="${run_dir}/run-rdma-smoke-rank.sh" | |
| cat > "${rank_script}" <<'RANK_SCRIPT' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| rank="${SLURM_PROCID:-${SPUR_TASK_OFFSET:-0}}" | |
| job_id="${SLURM_JOB_ID:-${SPUR_JOB_ID:-local}}" | |
| container="atomesh-rdma-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${job_id}-${rank}-$$" | |
| rank_host="$(hostname)" | |
| cleanup() { | |
| echo "=== Removing RDMA smoke container ${container} ===" | |
| # Do not wait on D-state RDMA leftovers: kill/rm with a bound so | |
| # the Slurm task can exit instead of sitting in COMPLETING. | |
| if command -v timeout >/dev/null 2>&1; then | |
| timeout 20 docker kill "${container}" >/dev/null 2>&1 || true | |
| timeout 20 docker rm -f "${container}" >/dev/null 2>&1 || true | |
| else | |
| docker kill "${container}" >/dev/null 2>&1 || true | |
| docker rm -f "${container}" >/dev/null 2>&1 || true | |
| fi | |
| } | |
| on_error() { | |
| local rc="$1" | |
| local line="$2" | |
| local command="$3" | |
| echo "ERROR: RDMA smoke rank ${rank} failed on ${rank_host}: rc=${rc} line=${line} command=${command}" >&2 | |
| } | |
| on_signal() { | |
| local signal="$1" | |
| local rc="$2" | |
| echo "ERROR: RDMA smoke rank ${rank} received ${signal} on ${rank_host}" >&2 | |
| exit "${rc}" | |
| } | |
| publish_rank_rc() { | |
| local rc="$1" | |
| local rc_file | |
| [[ -n "${RDMA_SMOKE_RUN_DIR:-}" ]] || return 0 | |
| rc_file="${RDMA_SMOKE_RUN_DIR}/rank-rc-${rank}" | |
| printf '%s\n' "${rc}" > "${rc_file}.tmp" 2>/dev/null || return 0 | |
| mv "${rc_file}.tmp" "${rc_file}" 2>/dev/null || true | |
| } | |
| on_exit() { | |
| local rc=$? | |
| trap - EXIT | |
| echo "=== RDMA smoke rank exit: rank=${rank} host=${rank_host} pid=$$ job=${job_id} container=${container} rc=${rc} ===" | |
| # Publish before cleanup: Slurm accounting is unreliable on Spur, so | |
| # the workflow falls back to these per-rank codes. | |
| publish_rank_rc "${rc}" | |
| cleanup | |
| exit "${rc}" | |
| } | |
| trap 'on_error "$?" "${LINENO}" "${BASH_COMMAND}"' ERR | |
| trap on_exit EXIT | |
| trap 'on_signal HUP 129' HUP | |
| trap 'on_signal INT 130' INT | |
| trap 'on_signal TERM 143' TERM | |
| cleanup | |
| echo "=== RDMA smoke rank start: rank=${rank} host=${rank_host} pid=$$ job=${job_id} container=${container} ===" | |
| echo "=== Pulling RDMA smoke image on rank ${rank}: ${RDMA_SMOKE_IMAGE} ===" | |
| docker pull "${RDMA_SMOKE_IMAGE}" | |
| echo "=== RDMA smoke image pull complete on rank ${rank} ===" | |
| docker_args=( | |
| run -i --name "${container}" | |
| --network host --ipc host | |
| --privileged | |
| --device=/dev/kfd | |
| --device=/dev/dri | |
| --device=/dev/infiniband | |
| --group-add=video | |
| --cap-add=IPC_LOCK | |
| --ulimit memlock=-1:-1 | |
| -e MC_GID_INDEX="${MC_GID_INDEX:-1}" | |
| -e MC_ENABLE_DEST_DEVICE_AFFINITY=1 | |
| -e SMOKE_RANK="${rank}" | |
| -e SMOKE_NODE_COUNT="${RDMA_SMOKE_NODE_COUNT}" | |
| -e SMOKE_DECODE_HCA_MODE="${RDMA_SMOKE_DECODE_HCA_MODE}" | |
| -e SMOKE_RUN_DIR=/rdma-smoke-shared | |
| -v "${RDMA_SMOKE_RUN_DIR}:/rdma-smoke-shared" | |
| ) | |
| host_ionic="$(readlink -f /usr/lib/x86_64-linux-gnu/libionic.so.1 2>/dev/null || true)" | |
| [[ -z "${host_ionic}" ]] || docker_args+=( | |
| -v "${host_ionic}:/usr/lib/x86_64-linux-gnu/libionic.so.1:ro" | |
| ) | |
| [[ ! -e /usr/lib/x86_64-linux-gnu/libibverbs/libionic-rdmav34.so ]] || docker_args+=( | |
| -v /usr/lib/x86_64-linux-gnu/libibverbs/libionic-rdmav34.so:/usr/lib/x86_64-linux-gnu/libibverbs/libionic-rdmav34.so:ro | |
| ) | |
| [[ ! -e /etc/libibverbs.d/ionic.driver ]] || docker_args+=( | |
| -v /etc/libibverbs.d/ionic.driver:/etc/libibverbs.d/ionic.driver:ro | |
| ) | |
| docker_args+=( | |
| "${RDMA_SMOKE_IMAGE}" | |
| bash -s | |
| ) | |
| printf '=== RDMA smoke docker command on rank %s ===' "${rank}" | |
| printf ' %q' docker "${docker_args[@]}" | |
| echo | |
| echo "=== Starting RDMA smoke container on rank ${rank} ===" | |
| set +e | |
| docker "${docker_args[@]}" <<'CONTAINER_SCRIPT' | |
| set -euo pipefail | |
| mapfile -t rdma_devices < <(python3 - <<'PY' | |
| import os | |
| import re | |
| devices = [ | |
| name | |
| for name in os.listdir("/sys/class/infiniband") | |
| if os.path.isdir(os.path.join("/sys/class/infiniband", name)) | |
| ] | |
| def sort_key(name: str) -> int: | |
| match = re.search(r"(?:ionic_|rdma)(\d+)$", name) | |
| return int(match.group(1)) if match else 10_000 | |
| for device in sorted(devices, key=sort_key): | |
| print(device) | |
| PY | |
| ) | |
| if [[ "${#rdma_devices[@]}" -eq 0 ]]; then | |
| echo "ERROR: no RDMA devices found under /sys/class/infiniband" >&2 | |
| exit 1 | |
| fi | |
| echo "RDMA devices: ${rdma_devices[*]}" | |
| echo "Mooncake RDMA GID index: ${MC_GID_INDEX}" | |
| for rdma_device in "${rdma_devices[@]}"; do | |
| gid_path="/sys/class/infiniband/${rdma_device}/ports/1/gids/${MC_GID_INDEX}" | |
| selected_gid="$(cat "${gid_path}")" | |
| echo "${rdma_device}/1 GID[${MC_GID_INDEX}]: ${selected_gid}" | |
| done | |
| ibv_devinfo | |
| rocm-smi --showproductname || true | |
| gpu_count="$(python3 - <<'PY' | |
| import torch | |
| print(torch.cuda.device_count()) | |
| PY | |
| )" | |
| if ! [[ "${gpu_count}" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "ERROR: no ROCm GPUs are visible for GPUDirect RDMA testing" >&2 | |
| exit 1 | |
| fi | |
| echo "Visible ROCm GPUs: ${gpu_count}" | |
| validator_source="/app/mooncake/mooncake-transfer-engine/example/transfer_engine_validator.cpp" | |
| python3 - "${validator_source}" <<'PY' | |
| import pathlib | |
| import sys | |
| path = pathlib.Path(sys.argv[1]) | |
| source = path.read_text() | |
| replacements = { | |
| """ fillData(thread_id, addr, seed); | |
| submitRequestSync(engine, segment_id, thread_id, addr, remote_base, | |
| TransferRequest::WRITE); | |
| fillData(thread_id, addr, 0); | |
| submitRequestSync(engine, segment_id, thread_id, addr, remote_base, | |
| TransferRequest::READ); | |
| checkData(thread_id, addr, seed);""": | |
| """ fillData(thread_id, addr, seed); | |
| submitRequestSync(engine, segment_id, thread_id, addr, remote_base, | |
| TransferRequest::WRITE);""", | |
| """ submitRequestSync(engine, segment_id, thread_id, addr, remote_base, | |
| TransferRequest::WRITE); | |
| submitRequestSync(engine, segment_id, thread_id, addr, remote_base, | |
| TransferRequest::READ);""": | |
| """ submitRequestSync(engine, segment_id, thread_id, addr, remote_base, | |
| TransferRequest::WRITE);""", | |
| 'LOG(INFO) << "Worker " << thread_id << " stopped! Data validation passed";': | |
| 'LOG(INFO) << "Worker " << thread_id << " stopped! RDMA WRITE transfers completed";', | |
| } | |
| for old, new in replacements.items(): | |
| if source.count(old) != 1: | |
| raise RuntimeError( | |
| f"Expected exactly one validator WRITE/READ sequence, " | |
| f"found {source.count(old)} in {path}" | |
| ) | |
| source = source.replace(old, new) | |
| path.write_text(source) | |
| print(f"Patched {path} to exercise production WRITE-only transfers") | |
| PY | |
| build_dir="/tmp/mooncake-rdma-smoke-build" | |
| rm -rf "${build_dir}" | |
| cmake -S /app/mooncake -B "${build_dir}" \ | |
| -DUSE_HIP=ON \ | |
| -DUSE_ETCD=OFF \ | |
| -DBUILD_UNIT_TESTS=ON | |
| cmake --build "${build_dir}" \ | |
| --target transfer_engine_validator \ | |
| --parallel "$(nproc)" | |
| validator_bin="${build_dir}/mooncake-transfer-engine/example/transfer_engine_validator" | |
| if [[ ! -x "${validator_bin}" ]]; then | |
| echo "ERROR: transfer_engine_validator was not built at ${validator_bin}" >&2 | |
| exit 1 | |
| fi | |
| rdma_device_csv="$(IFS=,; echo "${rdma_devices[*]}")" | |
| export RDMA_DEVICE_CSV="${rdma_device_csv}" | |
| export GPU_COUNT="${gpu_count}" | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| devices = os.environ["RDMA_DEVICE_CSV"].split(",") | |
| gpu_count = int(os.environ["GPU_COUNT"]) | |
| matrix = {} | |
| for gpu_id in range(gpu_count): | |
| matrix[f"cuda:{gpu_id}"] = [devices, []] | |
| Path("/tmp/rdma-smoke-nic-priority.json").write_text( | |
| json.dumps(matrix), | |
| encoding="utf-8", | |
| ) | |
| PY | |
| echo "NIC priority matrix:" | |
| cat /tmp/rdma-smoke-nic-priority.json | |
| target_pid="" | |
| log_progress() { | |
| local msg="$*" | |
| echo "${msg}" | |
| echo "$(date -Is) rank=${SMOKE_RANK:-?} ${msg}" >> "${SMOKE_RUN_DIR}/rank-${SMOKE_RANK:-unknown}.log" 2>/dev/null || true | |
| } | |
| start_target_process() { | |
| local gpu_id="$1" | |
| local log_file="$2" | |
| shift 2 | |
| if command -v setsid >/dev/null 2>&1; then | |
| HIP_VISIBLE_DEVICES="${gpu_id}" \ | |
| setsid "${validator_bin}" "$@" >"${log_file}" 2>&1 < /dev/null & | |
| else | |
| HIP_VISIBLE_DEVICES="${gpu_id}" \ | |
| "${validator_bin}" "$@" >"${log_file}" 2>&1 < /dev/null & | |
| fi | |
| target_pid=$! | |
| } | |
| stop_target_process() { | |
| local pid="${target_pid:-}" | |
| [[ -n "${pid}" ]] || return 0 | |
| target_pid="" | |
| # SIGTERM runs Mooncake unregisterLocalMemory(), which can hang in | |
| # D-state. Smoke targets are ephemeral: KILL the process group and | |
| # do not wait, so the next GPU pair can start immediately. | |
| echo "WARNING: killing RDMA target process group ${pid} without waiting" >&2 | |
| kill -KILL -- "-${pid}" 2>/dev/null || kill -KILL "${pid}" 2>/dev/null || true | |
| disown "${pid}" 2>/dev/null || true | |
| } | |
| cleanup_processes() { | |
| stop_target_process | |
| pkill -KILL -f 'transfer_engine_validator' 2>/dev/null || true | |
| } | |
| trap cleanup_processes EXIT HUP INT TERM | |
| wait_for_p2p_address() { | |
| local target_log="$1" | |
| local target_pid="$2" | |
| local timeout_seconds="$3" | |
| local deadline=$((SECONDS + timeout_seconds)) | |
| local address="" | |
| while true; do | |
| address="$(awk ' | |
| /Transfer Engine RPC using P2P handshake, listening on / { | |
| print $NF | |
| exit | |
| } | |
| ' "${target_log}" 2>/dev/null || true)" | |
| if [[ -n "${address}" ]] && kill -0 "${target_pid}" 2>/dev/null; then | |
| printf '%s\n' "${address}" | |
| return 0 | |
| fi | |
| if ! kill -0 "${target_pid}" 2>/dev/null; then | |
| echo "ERROR: P2P target exited before publishing its listening address" >&2 | |
| cat "${target_log}" >&2 || true | |
| return 1 | |
| fi | |
| if [[ "${SECONDS}" -ge "${deadline}" ]]; then | |
| echo "ERROR: timed out waiting for P2P target listening address" >&2 | |
| cat "${target_log}" >&2 || true | |
| return 1 | |
| fi | |
| sleep 1 | |
| done | |
| } | |
| if [[ "${gpu_count}" -ne 8 ]]; then | |
| echo "ERROR: node-local prefill/decode RDMA testing requires exactly 8 GPUs, found ${gpu_count}" >&2 | |
| exit 1 | |
| fi | |
| if [[ "${#rdma_devices[@]}" -lt 8 ]]; then | |
| echo "ERROR: node-local prefill/decode RDMA testing requires at least 8 RDMA devices, found ${#rdma_devices[@]}" >&2 | |
| exit 1 | |
| fi | |
| HCA_COUNT=8 | |
| select_all_ib_devices_for_gpu() { | |
| local phys_idx="$1" | |
| local primary="${rdma_devices[phys_idx]}" | |
| local -a selected=("${primary}") | |
| local idx device existing | |
| for ((idx = 0; idx < HCA_COUNT; idx++)); do | |
| device="${rdma_devices[idx]:-}" | |
| [[ -z "${device}" ]] && continue | |
| for existing in "${selected[@]}"; do | |
| [[ "${existing}" == "${device}" ]] && continue 2 | |
| done | |
| selected+=("${device}") | |
| done | |
| (IFS=,; echo "${selected[*]}") | |
| } | |
| select_decode_ib_devices() { | |
| local prefill_idx="$1" | |
| local decode_idx="$2" | |
| local prefill_device="${rdma_devices[prefill_idx]}" | |
| local decode_device="${rdma_devices[decode_idx]}" | |
| case "${SMOKE_DECODE_HCA_MODE}" in | |
| local|same-gpu) | |
| echo "${decode_device}" | |
| ;; | |
| peer) | |
| echo "${prefill_device}" | |
| ;; | |
| local-peer) | |
| if [[ "${decode_device}" == "${prefill_device}" ]]; then | |
| echo "${decode_device}" | |
| else | |
| echo "${decode_device},${prefill_device}" | |
| fi | |
| ;; | |
| all) | |
| select_all_ib_devices_for_gpu "${decode_idx}" | |
| ;; | |
| *) | |
| echo "ERROR: unsupported SMOKE_DECODE_HCA_MODE=${SMOKE_DECODE_HCA_MODE}" >&2 | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| expected_decode_peer_device() { | |
| local prefill_idx="$1" | |
| local decode_idx="$2" | |
| if [[ "${SMOKE_DECODE_HCA_MODE}" == "local" || | |
| "${SMOKE_DECODE_HCA_MODE}" == "same-gpu" ]]; then | |
| echo "${rdma_devices[decode_idx]}" | |
| else | |
| echo "${rdma_devices[prefill_idx]}" | |
| fi | |
| } | |
| write_cuda0_nic_matrix() { | |
| local matrix_path="$1" | |
| shift | |
| MATRIX_PATH="${matrix_path}" RDMA_MATRIX_DEVICES="${*}" python3 - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| devices = os.environ["RDMA_MATRIX_DEVICES"].split() | |
| Path(os.environ["MATRIX_PATH"]).write_text( | |
| json.dumps({"cuda:0": [devices, []]}), | |
| encoding="utf-8", | |
| ) | |
| PY | |
| } | |
| echo "Decode HCA diagnostic mode: ${SMOKE_DECODE_HCA_MODE}" | |
| local_prefill_gpu_ids=() | |
| local_decode_gpu_ids=() | |
| if [[ "${SMOKE_DECODE_HCA_MODE}" == "same-gpu" ]]; then | |
| for ((gpu_id = 0; gpu_id < gpu_count; gpu_id++)); do | |
| local_prefill_gpu_ids+=("${gpu_id}") | |
| local_decode_gpu_ids+=("${gpu_id}") | |
| done | |
| else | |
| for ((prefill_gpu_id = 0; prefill_gpu_id < gpu_count - 1; prefill_gpu_id++)); do | |
| for ((decode_gpu_id = prefill_gpu_id + 1; decode_gpu_id < gpu_count; decode_gpu_id++)); do | |
| local_prefill_gpu_ids+=("${prefill_gpu_id}") | |
| local_decode_gpu_ids+=("${decode_gpu_id}") | |
| done | |
| done | |
| fi | |
| for ((pair_index = 0; pair_index < ${#local_prefill_gpu_ids[@]}; pair_index++)); do | |
| prefill_gpu_id="${local_prefill_gpu_ids[pair_index]}" | |
| decode_gpu_id="${local_decode_gpu_ids[pair_index]}" | |
| prefill_rdma_device="${rdma_devices[prefill_gpu_id]}" | |
| decode_rdma_device="${rdma_devices[decode_gpu_id]}" | |
| decode_device_csv="$(select_decode_ib_devices "${prefill_gpu_id}" "${decode_gpu_id}")" | |
| prefill_matrix="/tmp/gdr-smoke-prefill-gpu-${prefill_gpu_id}-nic-priority.json" | |
| decode_matrix="/tmp/gdr-smoke-decode-gpu-${decode_gpu_id}-nic-priority.json" | |
| write_cuda0_nic_matrix "${prefill_matrix}" "${prefill_rdma_device}" | |
| IFS=',' read -r -a decode_ib_devices <<< "${decode_device_csv}" | |
| write_cuda0_nic_matrix "${decode_matrix}" "${decode_ib_devices[@]}" | |
| target_log="/tmp/gdr-smoke-decode-gpu-${decode_gpu_id}-target.log" | |
| initiator_log="/tmp/gdr-smoke-prefill-gpu-${prefill_gpu_id}-initiator.log" | |
| echo "=== GPUDirect RDMA prefill GPU ${prefill_gpu_id} (${prefill_rdma_device}) -> decode GPU ${decode_gpu_id} (${decode_device_csv}) ===" | |
| start_target_process "${decode_gpu_id}" "${target_log}" \ | |
| --mode=target \ | |
| --protocol=rdma \ | |
| --metadata_server=P2PHANDSHAKE \ | |
| --local_server_name=127.0.0.2:0 \ | |
| --device_name="${decode_device_csv}" \ | |
| --nic_priority_matrix="${decode_matrix}" \ | |
| --use_vram=true \ | |
| --gpu_id=0 \ | |
| --buffer_size=100663296 | |
| target_name="$(wait_for_p2p_address "${target_log}" "${target_pid}" 30)" | |
| echo "P2P target listening at ${target_name}" | |
| set +e | |
| MC_LOG_RDMA_SLICE_AFFINITY=1 \ | |
| GLOG_v=1 \ | |
| HIP_VISIBLE_DEVICES="${prefill_gpu_id}" \ | |
| timeout 120 "${validator_bin}" \ | |
| --mode=initiator \ | |
| --protocol=rdma \ | |
| --metadata_server=P2PHANDSHAKE \ | |
| --local_server_name=127.0.0.3:0 \ | |
| --segment_id="${target_name}" \ | |
| --device_name="${prefill_rdma_device}" \ | |
| --nic_priority_matrix="${prefill_matrix}" \ | |
| --use_vram=true \ | |
| --gpu_id=0 \ | |
| --buffer_size=100663296 \ | |
| --batch_size=1 \ | |
| --block_size=67108864 \ | |
| --threads=1 \ | |
| --duration=1 \ | |
| 2>&1 | tee "${initiator_log}" | |
| transfer_rc="${PIPESTATUS[0]}" | |
| set -e | |
| if [[ "${transfer_rc}" -ne 0 ]] || | |
| ! grep -q "RDMA WRITE transfers completed" "${initiator_log}"; then | |
| cat "${initiator_log}" >&2 | |
| cat "${target_log}" >&2 | |
| echo "ERROR: GPUDirect RDMA WRITE failed from prefill GPU ${prefill_gpu_id} (${prefill_rdma_device}) to decode GPU ${decode_gpu_id} (${decode_device_csv})" >&2 | |
| exit 1 | |
| fi | |
| expected_peer_device="$(expected_decode_peer_device "${prefill_gpu_id}" "${decode_gpu_id}")" | |
| expected_affinity="local_device_name=${prefill_rdma_device}, peer_device_name=${expected_peer_device}" | |
| if grep -Fq "RDMA slice affinity:" "${initiator_log}"; then | |
| if ! grep -Fq "${expected_affinity}" "${initiator_log}"; then | |
| grep -F "RDMA slice affinity:" "${initiator_log}" >&2 || true | |
| echo "ERROR: GPUDirect RDMA selected the wrong HCA for prefill GPU ${prefill_gpu_id}" >&2 | |
| exit 1 | |
| fi | |
| else | |
| echo "WARNING: RDMA WRITE completed, but HCA affinity logging was unavailable for prefill GPU ${prefill_gpu_id}" >&2 | |
| fi | |
| stop_target_process | |
| touch "${SMOKE_RUN_DIR}/node-local-passed-${SMOKE_RANK}-${prefill_gpu_id}-${decode_gpu_id}" | |
| echo "GPUDirect RDMA passed from prefill GPU ${prefill_gpu_id} to decode GPU ${decode_gpu_id} via decode HCAs ${decode_device_csv}" | |
| done | |
| echo "Node-local GPUDirect RDMA passed for all ${#local_prefill_gpu_ids[@]} prefill/decode GPU pairs" | |
| if [[ "${SMOKE_NODE_COUNT}" -lt 2 ]]; then | |
| echo "Single-node GPUDirect RDMA smoke test passed" | |
| exit 0 | |
| fi | |
| touch "${SMOKE_RUN_DIR}/local-gdr-done-${SMOKE_RANK}" | |
| if [[ "${SMOKE_RANK}" -le 1 ]]; then | |
| deadline=$((SECONDS + 300)) | |
| while true; do | |
| completed_nodes=0 | |
| for marker in "${SMOKE_RUN_DIR}"/local-gdr-done-*; do | |
| [[ -e "${marker}" ]] && completed_nodes=$((completed_nodes + 1)) | |
| done | |
| [[ "${completed_nodes}" -ge "${SMOKE_NODE_COUNT}" ]] && break | |
| if [[ "${SECONDS}" -ge "${deadline}" ]]; then | |
| echo "ERROR: timed out waiting for node-local GDR tests" >&2 | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| fi | |
| wait_for_file() { | |
| local path="$1" | |
| local timeout_seconds="$2" | |
| local description="$3" | |
| local deadline=$((SECONDS + timeout_seconds)) | |
| while [[ ! -s "${path}" ]]; do | |
| if [[ "${SECONDS}" -ge "${deadline}" ]]; then | |
| echo "ERROR: timed out waiting for ${description}: ${path}" >&2 | |
| exit 1 | |
| fi | |
| sleep 1 | |
| done | |
| } | |
| local_ip="" | |
| if command -v ip >/dev/null 2>&1; then | |
| local_ip="$(ip route get 1.1.1.1 2>/dev/null | awk ' | |
| { | |
| for (i = 1; i <= NF; i++) { | |
| if ($i == "src" && i < NF) { | |
| print $(i + 1) | |
| exit | |
| } | |
| } | |
| } | |
| ' || true)" | |
| fi | |
| if [[ -z "${local_ip}" ]]; then | |
| local_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)" | |
| fi | |
| if [[ -z "${local_ip}" ]]; then | |
| echo "ERROR: unable to determine node IP for cross-node GDR testing" >&2 | |
| exit 1 | |
| fi | |
| peer_info="${SMOKE_RUN_DIR}/cross-node-prefill-info" | |
| if [[ "${SMOKE_RANK}" -eq 0 ]]; then | |
| { | |
| echo "${local_ip}" | |
| echo "${gpu_count}" | |
| } > "${peer_info}.tmp" | |
| mv "${peer_info}.tmp" "${peer_info}" | |
| elif [[ "${SMOKE_RANK}" -eq 1 ]]; then | |
| wait_for_file "${peer_info}" 60 "prefill node information" | |
| mapfile -t peer_data < "${peer_info}" | |
| prefill_node_ip="${peer_data[0]:-}" | |
| prefill_gpu_count="${peer_data[1]:-0}" | |
| if [[ -z "${prefill_node_ip}" || "${prefill_gpu_count}" -ne "${gpu_count}" ]]; then | |
| echo "ERROR: cross-node GPU topology mismatch: prefill=${prefill_gpu_count}, decode=${gpu_count}" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [[ "${SMOKE_RANK}" -eq 0 || "${SMOKE_RANK}" -eq 1 ]]; then | |
| cross_prefill_gpu_ids=() | |
| cross_decode_gpu_ids=() | |
| for ((gpu_id = 0; gpu_id < gpu_count; gpu_id++)); do | |
| cross_prefill_gpu_ids+=("${gpu_id}") | |
| cross_decode_gpu_ids+=("${gpu_id}") | |
| done | |
| for ((prefill_gpu_id = 0; prefill_gpu_id < gpu_count - 1; prefill_gpu_id++)); do | |
| for ((decode_gpu_id = prefill_gpu_id + 1; decode_gpu_id < gpu_count; decode_gpu_id++)); do | |
| cross_prefill_gpu_ids+=("${prefill_gpu_id}") | |
| cross_decode_gpu_ids+=("${decode_gpu_id}") | |
| done | |
| done | |
| for ((pair_index = 0; pair_index < ${#cross_prefill_gpu_ids[@]}; pair_index++)); do | |
| prefill_gpu_id="${cross_prefill_gpu_ids[pair_index]}" | |
| decode_gpu_id="${cross_decode_gpu_ids[pair_index]}" | |
| prefill_rdma_device="${rdma_devices[prefill_gpu_id]}" | |
| decode_rdma_device="${rdma_devices[decode_gpu_id]}" | |
| decode_device_csv="${decode_rdma_device}" | |
| expected_peer_device="${decode_rdma_device}" | |
| is_cross_rail=0 | |
| if [[ "${prefill_gpu_id}" -ne "${decode_gpu_id}" ]]; then | |
| decode_device_csv="$(select_decode_ib_devices "${prefill_gpu_id}" "${decode_gpu_id}")" | |
| expected_peer_device="$(expected_decode_peer_device "${prefill_gpu_id}" "${decode_gpu_id}")" | |
| is_cross_rail=1 | |
| fi | |
| echo "=== Cross-node RDMA pair: prefill GPU ${prefill_gpu_id} (${prefill_rdma_device}) -> decode GPU ${decode_gpu_id} (${decode_device_csv}); expected peer HCA ${expected_peer_device} ===" | |
| prefill_matrix="/tmp/cross-node-prefill-gpu-${prefill_gpu_id}-nic-priority.json" | |
| decode_matrix="/tmp/cross-node-decode-gpu-${decode_gpu_id}-nic-priority.json" | |
| write_cuda0_nic_matrix "${prefill_matrix}" "${prefill_rdma_device}" | |
| if [[ "${is_cross_rail}" -eq 1 ]]; then | |
| IFS=',' read -r -a decode_ib_devices <<< "${decode_device_csv}" | |
| write_cuda0_nic_matrix "${decode_matrix}" "${decode_ib_devices[@]}" | |
| else | |
| write_cuda0_nic_matrix "${decode_matrix}" "${decode_rdma_device}" | |
| fi | |
| ready_file="${SMOKE_RUN_DIR}/cross-node-ready-prefill-${prefill_gpu_id}-decode-${decode_gpu_id}" | |
| result_file="${SMOKE_RUN_DIR}/cross-node-result-prefill-${prefill_gpu_id}-decode-${decode_gpu_id}" | |
| target_log="${SMOKE_RUN_DIR}/cross-node-prefill-${prefill_gpu_id}-decode-${decode_gpu_id}-target.log" | |
| if [[ "${SMOKE_RANK}" -eq 1 ]]; then | |
| rm -f \ | |
| "${ready_file}" "${ready_file}.tmp" \ | |
| "${result_file}" "${result_file}.tmp" | |
| log_progress "=== Cross-node decode target: ${local_ip}, GPU ${decode_gpu_id} (${decode_rdma_device}) ===" | |
| start_target_process "${decode_gpu_id}" "${target_log}" \ | |
| --mode=target \ | |
| --protocol=rdma \ | |
| --metadata_server=P2PHANDSHAKE \ | |
| --local_server_name="${local_ip}:0" \ | |
| --device_name="${decode_device_csv}" \ | |
| --nic_priority_matrix="${decode_matrix}" \ | |
| --use_vram=true \ | |
| --gpu_id=0 \ | |
| --buffer_size=100663296 | |
| target_name="$(wait_for_p2p_address "${target_log}" "${target_pid}" 30)" | |
| log_progress "Cross-node P2P target listening at ${target_name}" | |
| echo "${target_name}" > "${ready_file}.tmp" | |
| mv "${ready_file}.tmp" "${ready_file}" | |
| wait_for_file "${result_file}" 150 "prefill GPU ${prefill_gpu_id} to decode GPU ${decode_gpu_id} result" | |
| result="$(<"${result_file}")" | |
| stop_target_process | |
| if [[ "${result}" != "0" ]]; then | |
| cat "${target_log}" >&2 | |
| echo "ERROR: cross-node GPUDirect RDMA failed from prefill GPU ${prefill_gpu_id} to decode GPU ${decode_gpu_id}" >&2 | |
| exit 1 | |
| fi | |
| log_progress "Cross-node GPUDirect RDMA passed on decode GPU ${decode_gpu_id}" | |
| else | |
| wait_for_file "${ready_file}" 120 "decode GPU ${decode_gpu_id} target" | |
| target_name="$(<"${ready_file}")" | |
| initiator_log="/tmp/cross-node-prefill-${prefill_gpu_id}-decode-${decode_gpu_id}-initiator.log" | |
| echo "=== Cross-node prefill initiator: ${local_ip} GPU ${prefill_gpu_id} (${prefill_rdma_device}) -> ${target_name} decode GPU ${decode_gpu_id} ===" | |
| transfer_batch_size=4 | |
| transfer_block_size=1048576 | |
| transfer_duration=2 | |
| affinity_env=() | |
| if [[ "${is_cross_rail}" -eq 1 ]]; then | |
| transfer_batch_size=1 | |
| transfer_block_size=67108864 | |
| transfer_duration=1 | |
| affinity_env=(MC_LOG_RDMA_SLICE_AFFINITY=1 GLOG_v=1) | |
| fi | |
| set +e | |
| env "${affinity_env[@]}" HIP_VISIBLE_DEVICES="${prefill_gpu_id}" \ | |
| timeout 120 "${validator_bin}" \ | |
| --mode=initiator \ | |
| --protocol=rdma \ | |
| --metadata_server=P2PHANDSHAKE \ | |
| --local_server_name="${local_ip}:0" \ | |
| --segment_id="${target_name}" \ | |
| --device_name="${prefill_rdma_device}" \ | |
| --nic_priority_matrix="${prefill_matrix}" \ | |
| --use_vram=true \ | |
| --gpu_id=0 \ | |
| --buffer_size=100663296 \ | |
| --batch_size="${transfer_batch_size}" \ | |
| --block_size="${transfer_block_size}" \ | |
| --threads=1 \ | |
| --duration="${transfer_duration}" \ | |
| 2>&1 | tee "${initiator_log}" | |
| transfer_rc="${PIPESTATUS[0]}" | |
| set -e | |
| if [[ "${transfer_rc}" -eq 0 ]] && | |
| grep -q "RDMA WRITE transfers completed" "${initiator_log}"; then | |
| result=0 | |
| else | |
| result=1 | |
| fi | |
| if [[ "${result}" -eq 0 && "${is_cross_rail}" -eq 1 ]]; then | |
| expected_affinity="local_device_name=${prefill_rdma_device}, peer_device_name=${expected_peer_device}" | |
| if grep -Fq "RDMA slice affinity:" "${initiator_log}"; then | |
| if ! grep -Fq "${expected_affinity}" "${initiator_log}"; then | |
| grep -F "RDMA slice affinity:" "${initiator_log}" >&2 || true | |
| echo "ERROR: cross-node GPUDirect RDMA selected the wrong HCA from prefill GPU ${prefill_gpu_id} to decode GPU ${decode_gpu_id}" >&2 | |
| result=1 | |
| fi | |
| else | |
| echo "WARNING: cross-node RDMA WRITE completed, but HCA affinity logging was unavailable from prefill GPU ${prefill_gpu_id} to decode GPU ${decode_gpu_id}" >&2 | |
| fi | |
| fi | |
| echo "${result}" > "${result_file}.tmp" | |
| mv "${result_file}.tmp" "${result_file}" | |
| if [[ "${result}" -ne 0 ]]; then | |
| cat "${target_log}" >&2 || true | |
| echo "ERROR: cross-node GPUDirect RDMA WRITE failed from prefill GPU ${prefill_gpu_id} to decode GPU ${decode_gpu_id}" >&2 | |
| exit 1 | |
| fi | |
| touch "${SMOKE_RUN_DIR}/cross-node-passed-${prefill_gpu_id}-${decode_gpu_id}" | |
| echo "Cross-node GPUDirect RDMA passed from prefill GPU ${prefill_gpu_id} to decode GPU ${decode_gpu_id}" | |
| fi | |
| done | |
| fi | |
| if [[ "${SMOKE_RANK}" -eq 0 ]]; then | |
| echo "Mooncake cross-node GPUDirect RDMA passed for same-index GPU pairs 0-7 and all cross-rail pairs i < j" | |
| fi | |
| cleanup_processes | |
| log_progress "rank ${SMOKE_RANK} container script complete" | |
| CONTAINER_SCRIPT | |
| docker_rc=$? | |
| set -e | |
| echo "=== RDMA smoke container finished on rank ${rank}: rc=${docker_rc} ===" | |
| cleanup | |
| exit "${docker_rc}" | |
| RANK_SCRIPT | |
| chmod +x "${rank_script}" | |
| slurm_status_file="${run_dir}/slurm-job.rc" | |
| rm -f "${slurm_status_file}" "${slurm_status_file}.tmp" | |
| rm -f \ | |
| "${run_dir}"/rank-rc-* \ | |
| "${run_dir}"/node-local-passed-* \ | |
| "${run_dir}"/cross-node-passed-* | |
| batch_script="${run_dir}/rdma-smoke.sbatch.sh" | |
| if [[ "${USES_SPUR_CONTROLLER}" == "1" ]]; then | |
| # Spur launches this script on each node with SPUR_TASK_OFFSET; srun is unavailable. | |
| cat > "${batch_script}" <<EOF | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| exec "${rank_script}" | |
| EOF | |
| else | |
| cat > "${batch_script}" <<EOF | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| set +e | |
| srun --nodes=${node_count} --ntasks=${node_count} --ntasks-per-node=1 \ | |
| "${rank_script}" | |
| srun_rc=\$? | |
| set -e | |
| printf '%s\n' "\${srun_rc}" > "${slurm_status_file}.tmp" | |
| mv "${slurm_status_file}.tmp" "${slurm_status_file}" | |
| exit "\${srun_rc}" | |
| EOF | |
| fi | |
| chmod +x "${batch_script}" | |
| sbatch_cmd=( | |
| sbatch | |
| --parsable | |
| --exclusive | |
| --export=ALL | |
| --job-name "${SLURM_JOB_NAME}" | |
| --nodes "${node_count}" | |
| --ntasks "${node_count}" | |
| --ntasks-per-node 1 | |
| --time 01:00:00 | |
| --output "${SLURM_OUTPUT}" | |
| --error "${SLURM_ERROR}" | |
| ) | |
| if [[ -n "${nodes}" ]]; then | |
| slurm_node_selection_args "${nodes}" "${node_count}" | |
| sbatch_cmd+=("${SLURM_NODE_SELECTION_ARGS[@]}") | |
| fi | |
| if [[ "${SLURM_SUBMIT_RUNNER}" == "atomesh-cicd-mi350" ]]; then | |
| sbatch_cmd+=(--controller "${SPUR_CONTROLLER_ADDR}") | |
| else | |
| [[ -z "${SLURM_ACCOUNT}" ]] || sbatch_cmd+=(--account "${SLURM_ACCOUNT}") | |
| [[ -z "${SLURM_PARTITION}" ]] || sbatch_cmd+=(--partition "${SLURM_PARTITION}") | |
| [[ -z "${SLURM_QOS}" ]] || sbatch_cmd+=(--qos "${SLURM_QOS}") | |
| sbatch_cmd+=(--gres gpu:8) | |
| if [[ "${USES_SPUR_CONTROLLER}" == "1" && -n "${SPUR_CONTROLLER_ADDR}" ]]; then | |
| sbatch_cmd+=( | |
| --controller "${SPUR_CONTROLLER_ADDR}" | |
| ) | |
| fi | |
| fi | |
| sbatch_cmd+=("${batch_script}") | |
| printf ' %q' "${sbatch_cmd[@]}" | |
| echo | |
| write_slurm_cancel_helper "" | |
| set +e | |
| sbatch_output="$("${sbatch_cmd[@]}")" | |
| sbatch_rc=$? | |
| set -e | |
| echo "${sbatch_output}" | |
| if [[ "${sbatch_rc}" -ne 0 ]]; then | |
| echo "sbatch submit exit code: ${sbatch_rc}" | |
| exit "${sbatch_rc}" | |
| fi | |
| JOB_ID="$(parse_sbatch_job_id "${sbatch_output}")" | |
| SLURM_JOB_ACTIVE=1 | |
| echo "${JOB_ID}" | tee "${run_dir}/slurm-job-id" | |
| write_slurm_cancel_helper "${JOB_ID}" | |
| set_slurm_job_log_paths "${JOB_ID}" | |
| SLURM_STATUS_DIR="${run_dir}" | |
| SLURM_STATUS_RANKS="${node_count}" | |
| monitor_slurm_job "${JOB_ID}" | |
| # Spur leaves the job in COMPLETING while RDMA leftovers are reaped, | |
| # so give sacct longer than the 30s default to publish a final state. | |
| SLURM_ACCOUNTING_TIMEOUT="${SLURM_ACCOUNTING_TIMEOUT:-180}" | |
| read_slurm_exit_code "${JOB_ID}" | |
| read_slurm_status_files "${run_dir}" "${node_count}" | |
| SLURM_JOB_ACTIVE=0 | |
| echo "slurm_state=${SLURM_STATE}" | |
| echo "slurm_exit_code=${SLURM_EXIT_CODE}" | |
| echo "slurm job exit code: ${SLURM_JOB_RC}" | |
| exit "${SLURM_JOB_RC}" | |
| - name: Summarize Mooncake RDMA smoke test | |
| if: always() | |
| env: | |
| RDMA_SMOKE_OUTCOME: ${{ steps.rdma-smoke.outcome }} | |
| run: | | |
| set -uo pipefail | |
| current_user="$(id -un 2>/dev/null || id -u)" | |
| log_root="${ATOMESH_LOG_ROOT//\$\{USER\}/${current_user}}" | |
| log_root="${log_root//\$USER/${current_user}}" | |
| if [[ "$(hostname -s)" == "pit2-vm-amd-xl-02" ]]; then | |
| log_root="/share_nfs/ATOMESH_RUNNER/ATOMESH_LOG" | |
| fi | |
| run_dir="${log_root%/}/rdma-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| summary_path="${run_dir}/rdma-smoke-summary.md" | |
| mkdir -p "${run_dir}" | |
| if [[ "${RDMA_SMOKE_OUTCOME}" == "success" ]]; then | |
| outcome_icon="✅" | |
| else | |
| outcome_icon="❌" | |
| fi | |
| job_id="" | |
| if [[ -s "${run_dir}/slurm-job-id" ]]; then | |
| job_id="$(tr -d '[:space:]' < "${run_dir}/slurm-job-id")" | |
| fi | |
| shopt -s nullglob | |
| log_files=("${run_dir}"/slurm-*.out "${run_dir}"/slurm-*.err) | |
| shopt -u nullglob | |
| { | |
| echo "## Mooncake RDMA smoke test" | |
| echo | |
| echo "- Workflow result: ${outcome_icon} \`${RDMA_SMOKE_OUTCOME}\`" | |
| echo "- Image: \`${RDMA_SMOKE_IMAGE}\`" | |
| echo "- Requested nodes: \`${RDMA_SMOKE_NODE_COUNT}\`" | |
| echo "- Decode HCA mode: \`${RDMA_SMOKE_DECODE_HCA_MODE}\`" | |
| echo "- Mooncake GID index: \`${MC_GID_INDEX}\`" | |
| echo "- Slurm job: \`${job_id:-not submitted}\`" | |
| echo | |
| echo "### RDMA WRITE results" | |
| python3 - "${run_dir}" <<'PY' | |
| import os | |
| import sys | |
| from pathlib import Path | |
| run_dir = Path(sys.argv[1]) | |
| node_local_results = set() | |
| for path in run_dir.glob("node-local-passed-*-*-*"): | |
| try: | |
| _, _, _, rank, src, dst = path.name.split("-") | |
| node_local_results.add((int(rank), int(src), int(dst))) | |
| except ValueError: | |
| continue | |
| cross_node_pairs = set() | |
| for path in run_dir.glob("cross-node-passed-*-*"): | |
| try: | |
| _, _, _, src, dst = path.name.split("-") | |
| cross_node_pairs.add((int(src), int(dst))) | |
| except ValueError: | |
| continue | |
| cross_rail_pairs = {(src, dst) for src in range(7) for dst in range(src + 1, 8)} | |
| same_index_pairs = {(gpu, gpu) for gpu in range(8)} | |
| node_count = int(os.environ.get("RDMA_SMOKE_NODE_COUNT", "2")) | |
| expected_node_local_results = { | |
| (rank, src, dst) | |
| for rank in range(node_count) | |
| for src, dst in cross_rail_pairs | |
| } | |
| pair_label = "GPU" + "、".join( | |
| f"{src}→{dst}" for src, dst in sorted(cross_rail_pairs) | |
| ) | |
| node_local_icon = "✅" if expected_node_local_results <= node_local_results else "❌" | |
| same_index_icon = "✅" if same_index_pairs <= cross_node_pairs else "❌" | |
| cross_rail_icon = "✅" if cross_rail_pairs <= cross_node_pairs else "❌" | |
| print(f"Node-local {pair_label} {node_local_icon}") | |
| print(f"Cross-node same-index GPU0–7 {same_index_icon}") | |
| print(f"Cross-node cross-rail {pair_label} {cross_rail_icon}") | |
| PY | |
| echo | |
| echo "### Errors" | |
| if [[ "${#log_files[@]}" -gt 0 ]] && | |
| awk ' | |
| /^(ERROR:|E[0-9]{8})/ || | |
| /Permission denied|command not found|Detect data integrity/ { | |
| if (!seen[$0]++ && count++ < 20) { | |
| print "- ❌ `" $0 "`" | |
| } | |
| } | |
| END { exit(count ? 0 : 1) } | |
| ' "${log_files[@]}"; then | |
| : | |
| else | |
| echo "- No RDMA smoke errors found in Slurm logs." | |
| fi | |
| } | tee "${summary_path}" >> "${GITHUB_STEP_SUMMARY}" | |
| echo "RDMA smoke summary written to ${summary_path}" | |
| run-model-benchmark: | |
| name: ${{ matrix.id }} | |
| needs: [load-config] | |
| if: >- | |
| needs.load-config.outputs.has_matrix == 'true' | |
| && ( | |
| github.event_name == 'schedule' | |
| || (github.event_name == 'workflow_dispatch' && inputs.run_model_benchmark) | |
| ) | |
| runs-on: ${{ matrix.runner.slurm_submit_runner }} | |
| timeout-minutes: ${{ matrix.run_eval && matrix.accuracy.task == 'swebench_lite' && 780 || 420 }} | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 1 | |
| matrix: ${{ fromJson(needs.load-config.outputs.matrix_json) }} | |
| steps: | |
| - name: Checkout ATOM repo | |
| uses: actions/checkout@v6 | |
| - name: Cancel existing jobs on benchmark nodes | |
| if: ${{ github.event_name == 'schedule' && matrix.runner.slurm_submit_runner != 'atomesh-cicd' }} | |
| env: | |
| NODES: ${{ join(matrix.nodes, ',') }} | |
| SLURM_SUBMIT_RUNNER: ${{ matrix.runner.slurm_submit_runner }} | |
| run: | | |
| set -euo pipefail | |
| export PATH="/usr/local/slurm-24.05.5.1/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" | |
| if [[ "${SLURM_SUBMIT_RUNNER}" == "atomesh-cicd-mi350" ]]; then | |
| echo "Slurm job cleanup is disabled for ${SLURM_SUBMIT_RUNNER}; skipping." | |
| exit 0 | |
| fi | |
| if [[ -z "${NODES}" ]]; then | |
| echo "No explicit benchmark nodes were selected; skipping Slurm job cleanup." | |
| exit 0 | |
| fi | |
| for command_name in squeue scancel; do | |
| if ! command -v "${command_name}" >/dev/null 2>&1; then | |
| echo "ERROR: ${command_name} not found on $(hostname). PATH=${PATH}" >&2 | |
| exit 127 | |
| fi | |
| done | |
| squeue_cmd=(squeue) | |
| scancel_cmd=(scancel) | |
| if [[ "${SLURM_SUBMIT_RUNNER}" == "atomesh-cicd-mi355-crusoe" ]]; then | |
| controller="http://crs-m2m-cpu-spur-v2-001.crusoe.amd.com:6817" | |
| squeue_cmd+=(--controller "${controller}") | |
| scancel_cmd+=(--controller "${controller}") | |
| fi | |
| echo "=== Checking all users' Slurm jobs on ${NODES} ===" | |
| job_ids="$("${squeue_cmd[@]}" --noheader -w "${NODES}" --format="%A" | sort -u)" | |
| if [[ -z "${job_ids}" ]]; then | |
| echo "No existing Slurm jobs found on ${NODES}." | |
| exit 0 | |
| fi | |
| echo "Cancelling Slurm jobs on ${NODES}:" | |
| printf '%s\n' "${job_ids}" | |
| mapfile -t job_id_list <<< "${job_ids}" | |
| for job_id in "${job_id_list[@]}"; do | |
| if timeout 60s "${scancel_cmd[@]}" "${job_id}"; then | |
| echo "Finished cancelling Slurm job ${job_id}." | |
| else | |
| status=$? | |
| if [[ "${status}" -eq 124 ]]; then | |
| echo "ERROR: Timed out after 60 seconds cancelling Slurm job ${job_id}." >&2 | |
| else | |
| echo "ERROR: Failed cancelling Slurm job ${job_id} with exit code ${status}." >&2 | |
| fi | |
| exit "${status}" | |
| fi | |
| done | |
| - name: Resolve machine-specific benchmark paths | |
| id: machine-paths | |
| env: | |
| CELL_JSON: ${{ toJson(matrix) }} | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import socket | |
| from pathlib import Path | |
| cell = json.loads(os.environ["CELL_JSON"]) | |
| # The matrix is expanded on ubuntu-latest, before the submit machine is known. | |
| machine_name = socket.gethostname().split(".", 1)[0] | |
| if machine_name == "pit2-vm-amd-xl-02": | |
| old_model_root = os.environ["ATOMESH_MODEL_ROOT"].rstrip("/") | |
| def remap_model_paths(value): | |
| if isinstance(value, dict): | |
| return {key: remap_model_paths(item) for key, item in value.items()} | |
| if isinstance(value, list): | |
| return [remap_model_paths(item) for item in value] | |
| if isinstance(value, str) and (value == old_model_root or value.startswith(old_model_root + "/")): | |
| return "/share_nfs/models" + value[len(old_model_root):] | |
| return value | |
| cell = remap_model_paths(cell) | |
| cell["runner"]["log_root"] = "/share_nfs/ATOMESH_RUNNER/ATOMESH_LOG" | |
| cell["runner"]["slurm_partition"] = "amd-spur" | |
| cell["runner"]["slurm_qos"] = "amd-frameworks-qos" | |
| if ( | |
| cell["runner"].get("slurm_account") == "amd-atom" | |
| and cell["runner"].get("slurm_submit_runner") != "atomesh-cicd-mi355-crusoe" | |
| ): | |
| cell["runner"]["slurm_qos"] = "amd-atom-qos" | |
| with Path(os.environ["GITHUB_OUTPUT"]).open("a", encoding="utf-8") as output: | |
| output.write(f"cell_json={json.dumps(cell, separators=(',', ':'))}\n") | |
| PY | |
| - name: Print benchmark config | |
| env: | |
| CELL_JSON: ${{ steps.machine-paths.outputs.cell_json }} | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| cell = json.loads(os.environ["CELL_JSON"]) | |
| def get(path, default=""): | |
| cur = cell | |
| for key in path.split("."): | |
| if not isinstance(cur, dict) or key not in cur: | |
| return default | |
| cur = cur[key] | |
| return cur | |
| summary = { | |
| "id": get("id"), | |
| "suite": get("suite"), | |
| "model": get("model"), | |
| "case": get("name"), | |
| "image": get("image"), | |
| "model_path": get("model_path"), | |
| "precision": get("precision"), | |
| "runner": get("runner.slurm_submit_runner"), | |
| "slurm_account": get("runner.slurm_account"), | |
| "slurm_partition": get("runner.slurm_partition"), | |
| "slurm_qos": get("runner.slurm_qos"), | |
| "log_root": get("runner.log_root"), | |
| "nodes": ",".join(get("nodes", [])), | |
| "num_nodes": get("num_nodes"), | |
| "topology": get("topology"), | |
| "pd_worker_layout": get("pd_worker_layout"), | |
| "prefill": get("service.prefill"), | |
| "decode": get("service.decode"), | |
| "router": get("service.router"), | |
| "server_args": get("server_args"), | |
| "isl": get("isl"), | |
| "osl": get("osl"), | |
| "concurrency": get("concurrency"), | |
| "run_eval": get("run_eval"), | |
| "accuracy": get("accuracy"), | |
| } | |
| print("=== ATOMesh benchmark config summary ===") | |
| for key, value in summary.items(): | |
| print(f"{key}: {value}") | |
| print("=== ATOMesh benchmark full cell JSON ===") | |
| print(json.dumps(cell, indent=2, sort_keys=True)) | |
| PY | |
| - name: Run Slurm benchmark cell | |
| env: | |
| # Build the reviewed source in a separate setup container. Use the | |
| # existing release profile for both benchmark and eval services. | |
| ATOMESH_BUILD_MESH: ${{ matrix.benchmark.kind == 'aiperf_agentic' && 'true' || 'false' }} | |
| ATOMESH_MESH_BUILD_PROFILE: release | |
| CELL_JSON: ${{ steps.machine-paths.outputs.cell_json }} | |
| RESULT_DIR: atomesh-results | |
| SLURM_SUBMIT_RUNNER: ${{ matrix.runner.slurm_submit_runner }} | |
| run: | | |
| set -euo pipefail | |
| ORIGINAL_WORKSPACE="${GITHUB_WORKSPACE}" | |
| CURRENT_USER="$(id -un 2>/dev/null || id -u)" | |
| if [[ "${SLURM_SUBMIT_RUNNER}" == "atomesh-cicd-mi350" ]]; then | |
| SHARED_WORKSPACE="/data/${CURRENT_USER}/atomesh-ci/${GITHUB_RUN_ID}/${{ matrix.id }}" | |
| rm -rf "${SHARED_WORKSPACE}" | |
| mkdir -p "${SHARED_WORKSPACE}" | |
| if command -v rsync >/dev/null 2>&1; then | |
| rsync -a --delete "${ORIGINAL_WORKSPACE}/" "${SHARED_WORKSPACE}/" | |
| else | |
| tar -C "${ORIGINAL_WORKSPACE}" -cf - . | tar -C "${SHARED_WORKSPACE}" -xf - | |
| fi | |
| cd "${SHARED_WORKSPACE}" | |
| RESULT_DIR="${ORIGINAL_WORKSPACE}/${RESULT_DIR}" | |
| fi | |
| chmod +x .github/scripts/atomesh/*.sh .github/scripts/atomesh/*.py | |
| SLURM_CANCEL_HELPER="${RESULT_DIR}/${{ matrix.id }}.slurm-cancel.sh" | |
| SLURM_JOB_NAME="${{ matrix.id }}-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}" | |
| SPUR_CONTROLLER_ADDR="${SPUR_CONTROLLER_ADDR:-}" | |
| if [[ "${SLURM_SUBMIT_RUNNER}" == "atomesh-cicd-mi355-crusoe" ]]; then | |
| SPUR_CONTROLLER_ADDR="http://crs-m2m-cpu-spur-v2-001.crusoe.amd.com:6817" | |
| elif [[ -z "${SPUR_CONTROLLER_ADDR}" && "${SLURM_SUBMIT_RUNNER}" == "atomesh-cicd-mi350" ]]; then | |
| SPUR_CONTROLLER_ADDR="http://134.199.196.72:6817" | |
| fi | |
| source "${GITHUB_WORKSPACE}/.github/scripts/slurm_submit_helpers.sh" | |
| detect_slurm_backend | |
| PD_SUBMIT_PID="" | |
| scancel_slurm_by_name_from_wrapper() { | |
| command -v scancel >/dev/null 2>&1 || return 0 | |
| local -a scancel_cmd=(scancel) | |
| if [[ "${USES_SPUR_CONTROLLER}" == "1" && -n "${SPUR_CONTROLLER_ADDR}" ]]; then | |
| scancel_cmd+=(--controller "${SPUR_CONTROLLER_ADDR}") | |
| fi | |
| scancel_cmd+=(--user "${CURRENT_USER}" --name "${SLURM_JOB_NAME}") | |
| if command -v timeout >/dev/null 2>&1; then | |
| timeout "${SLURM_SCANCEL_TIMEOUT_SECONDS:-8}" "${scancel_cmd[@]}" || true | |
| else | |
| "${scancel_cmd[@]}" || true | |
| fi | |
| } | |
| cleanup_slurm_from_wrapper() { | |
| local rc=$? | |
| trap - EXIT HUP INT TERM | |
| if [[ "${rc}" -eq 75 && -s "${SLURM_CANCEL_HELPER}.status-unknown" ]]; then | |
| # Shared helper's EX_TEMPFAIL: status is unknown, not a batch failure. | |
| echo "WARNING: Slurm monitoring failed; preserving job ${SLURM_JOB_NAME}. Check the recorded job ID and controller before retrying." | |
| elif [[ "${rc}" -ne 0 ]]; then | |
| echo "=== cancelling Slurm job from workflow wrapper rc=${rc} ===" | |
| if [[ -x "${SLURM_CANCEL_HELPER}" ]]; then | |
| SLURM_CANCEL_WAIT_SECONDS="${SLURM_CANCEL_WAIT_SECONDS:-5}" \ | |
| SLURM_CANCEL_KILL_WAIT_SECONDS="${SLURM_CANCEL_KILL_WAIT_SECONDS:-5}" \ | |
| bash "${SLURM_CANCEL_HELPER}" || true | |
| else | |
| echo "Slurm cancel helper is not available yet" | |
| fi | |
| scancel_slurm_by_name_from_wrapper | |
| fi | |
| if [[ -n "${PD_SUBMIT_PID}" ]] && kill -0 "${PD_SUBMIT_PID}" 2>/dev/null; then | |
| kill "${PD_SUBMIT_PID}" 2>/dev/null || true | |
| wait "${PD_SUBMIT_PID}" 2>/dev/null || true | |
| fi | |
| exit "${rc}" | |
| } | |
| trap cleanup_slurm_from_wrapper EXIT | |
| trap 'exit 129' HUP | |
| trap 'exit 130' INT | |
| trap 'exit 143' TERM | |
| bash .github/scripts/atomesh/pd_submit.sh \ | |
| --cell-json "$CELL_JSON" \ | |
| --result-dir "$RESULT_DIR" & | |
| PD_SUBMIT_PID=$! | |
| wait "${PD_SUBMIT_PID}" | |
| - name: Summarize ${{ matrix.id }} benchmark result | |
| if: always() | |
| env: | |
| MATRIX_ID: ${{ matrix.id }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p atomesh-results | |
| SUMMARY_PATH="atomesh-results/${MATRIX_ID}-summary.md" | |
| AIPERF_RAW_PATH="atomesh-results/${MATRIX_ID}-aiperf-raw.md" | |
| BENCHMARK_ACTION_INPUT="atomesh-results/${MATRIX_ID}-benchmark-action.json" | |
| python3 .github/scripts/atomesh/process_result.py \ | |
| "atomesh-results/${MATRIX_ID}" \ | |
| --output "${BENCHMARK_ACTION_INPUT}" \ | |
| --summary "${SUMMARY_PATH}" \ | |
| --hardware "${ATOMESH_DASHBOARD_HARDWARE}" \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| # Empty for standard ISL/OSL cases: only AIPerf writes the console dump. | |
| python3 .github/scripts/atomesh/aiperf_console_report.py \ | |
| "atomesh-results/${MATRIX_ID}" \ | |
| --output "${AIPERF_RAW_PATH}" | |
| { | |
| echo "## ${MATRIX_ID}" | |
| echo | |
| if [ -f "atomesh-results/${MATRIX_ID}.slurm-job-id" ]; then | |
| echo "Slurm job: $(cat "atomesh-results/${MATRIX_ID}.slurm-job-id")" | |
| echo | |
| fi | |
| if [ -f "atomesh-results/${MATRIX_ID}.log-root" ]; then | |
| echo "Log path: \`$(cat "atomesh-results/${MATRIX_ID}.log-root")\`" | |
| echo | |
| fi | |
| cat "${SUMMARY_PATH}" | |
| if [ -s "${AIPERF_RAW_PATH}" ]; then | |
| echo | |
| cat "${AIPERF_RAW_PATH}" | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| cat "${SUMMARY_PATH}" | |
| if [ -s "${AIPERF_RAW_PATH}" ]; then | |
| cat "${AIPERF_RAW_PATH}" | |
| fi | |
| - name: Upload AIPerf Perfetto traces | |
| if: ${{ always() && matrix.benchmark.kind == 'aiperf_agentic' }} | |
| uses: ./.github/actions/upload-aiperf-traces | |
| with: | |
| result-dir: atomesh-results/${{ matrix.id }} | |
| artifact-name: aiperf-chrome-traces-${{ matrix.id }} | |
| output-dir: aiperf-traces/${{ matrix.id }} | |
| run-url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| - name: Prepare agentic PD latency reports | |
| if: ${{ always() && matrix.benchmark.kind == 'aiperf_agentic' }} | |
| id: metrics-reports | |
| env: | |
| MATRIX_ID: ${{ matrix.id }} | |
| run: | | |
| python3 .github/scripts/atomesh/observability/stage_reports.py \ | |
| --cell "$MATRIX_ID" \ | |
| --output "atomesh-reports/$MATRIX_ID" | |
| - name: Upload agentic PD latency reports | |
| if: ${{ always() && steps.metrics-reports.outcome == 'success' }} | |
| id: metrics-artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: atomesh-latency-${{ matrix.id }}-${{ github.run_attempt }} | |
| path: atomesh-reports/${{ matrix.id }}/ | |
| if-no-files-found: error | |
| - name: Link latency reports in job summary | |
| if: ${{ always() && steps.metrics-artifact.outcome == 'success' }} | |
| env: | |
| MATRIX_ID: ${{ matrix.id }} | |
| METRICS_ARTIFACT_URL: ${{ steps.metrics-artifact.outputs.artifact-url }} | |
| run: | | |
| python3 .github/scripts/atomesh/observability/stage_reports.py \ | |
| --cell "$MATRIX_ID" \ | |
| --output "atomesh-reports/$MATRIX_ID" \ | |
| --summary-only | |
| - name: Upload model benchmark artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: atomesh-model-benchmark-${{ matrix.id }} | |
| path: | | |
| atomesh-results/ | |
| if-no-files-found: ignore | |
| summarize-results: | |
| name: Summarize ATOMesh benchmark results | |
| needs: [run-model-benchmark] | |
| if: ${{ always() && !cancelled() && needs.run-model-benchmark.result != 'skipped' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_results: ${{ steps.summary.outputs.has_results }} | |
| steps: | |
| - name: Checkout ATOM repo | |
| uses: actions/checkout@v6 | |
| - name: Download model benchmark artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: atomesh-model-benchmark-* | |
| merge-multiple: true | |
| path: atomesh-results | |
| - name: Build benchmark summary | |
| id: summary | |
| run: | | |
| set -euo pipefail | |
| mkdir -p atomesh-results | |
| python3 .github/scripts/atomesh/process_result.py \ | |
| atomesh-results \ | |
| --output atomesh-benchmark-action.json \ | |
| --summary atomesh-benchmark-summary.md \ | |
| --hardware "${ATOMESH_DASHBOARD_HARDWARE}" \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| # Empty for standard ISL/OSL cases: only AIPerf writes the console dump. | |
| python3 .github/scripts/atomesh/aiperf_console_report.py \ | |
| atomesh-results \ | |
| --output atomesh-aiperf-raw.md | |
| python3 - <<'PY' > atomesh-case-log-paths.md | |
| import json | |
| import re | |
| from pathlib import Path | |
| results = Path("atomesh-results") | |
| def sort_key(case_id): | |
| """Order this table like the perf summary: (benchmark_model_name, | |
| max_concurrency), read from the case's own result JSON so both tables | |
| key off the same fields. A cell that sweeps several concurrencies | |
| leads at its lowest one; a case that died before writing any result | |
| falls back to its id, whose prefix ahead of the NpMd topology token is | |
| the model name. | |
| """ | |
| keys = [] | |
| for path in sorted((results / case_id).rglob("pd-*.json")): | |
| try: | |
| payload = json.loads(path.read_text(encoding="utf-8")) | |
| model = str(payload.get("benchmark_model_name") or "") | |
| conc = int(payload.get("max_concurrency") or 0) | |
| except (OSError, ValueError, TypeError): | |
| continue | |
| keys.append((model.rstrip("/").split("/")[-1].lower(), conc)) | |
| if keys: | |
| return min(keys) | |
| topology = re.search(r"-\d+p\d+d-", case_id) | |
| found = re.findall(r"-c(\d+)-", case_id) | |
| return ( | |
| case_id[: topology.start()] if topology else case_id, | |
| int(found[-1]) if found else 0, | |
| ) | |
| rows = [] | |
| for path in results.glob("*.log-root"): | |
| case_id = path.name[: -len(".log-root")] | |
| job_file = path.with_name(f"{case_id}.slurm-job-id") | |
| job_id = job_file.read_text().strip() if job_file.is_file() else "" | |
| rows.append((case_id, job_id or "--", path.read_text().strip())) | |
| if rows: | |
| print("### ATOMesh Case Log Paths") | |
| print() | |
| print("| Case | Slurm job | Log path |") | |
| print("| --- | --- | --- |") | |
| for case_id, job_id, log_root in sorted( | |
| rows, key=lambda row: sort_key(row[0]) + (row[0],) | |
| ): | |
| print(f"| {case_id} | {job_id} | `{log_root}` |") | |
| print() | |
| PY | |
| if [[ -s atomesh-case-log-paths.md ]]; then | |
| cat atomesh-case-log-paths.md >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| cat atomesh-benchmark-summary.md >> "$GITHUB_STEP_SUMMARY" | |
| if [[ -s atomesh-aiperf-raw.md ]]; then | |
| { | |
| echo | |
| cat atomesh-aiperf-raw.md | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| ENTRY_COUNT="$(python3 - <<'PY' | |
| import json | |
| from pathlib import Path | |
| path = Path("atomesh-benchmark-action.json") | |
| entries = json.loads(path.read_text(encoding="utf-8")) if path.is_file() else [] | |
| print(len(entries)) | |
| PY | |
| )" | |
| echo "entry_count=${ENTRY_COUNT}" >> "$GITHUB_OUTPUT" | |
| if [[ "${ENTRY_COUNT}" -gt 0 ]]; then | |
| echo "has_results=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_results=false" >> "$GITHUB_OUTPUT" | |
| echo "No successful ATOMesh benchmark result JSONs were produced; dashboard publish will be skipped." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Upload AIPerf Perfetto traces | |
| if: always() | |
| uses: ./.github/actions/upload-aiperf-traces | |
| with: | |
| result-dir: atomesh-results | |
| artifact-name: aiperf-chrome-traces-${{ github.run_id }} | |
| output-dir: aiperf-traces | |
| run-url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| - name: Upload benchmark summary | |
| if: ${{ steps.summary.outputs.has_results == 'true' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: atomesh-benchmark-summary | |
| path: | | |
| atomesh-benchmark-action.json | |
| atomesh-benchmark-summary.md | |
| atomesh-aiperf-raw.md | |
| atomesh-case-log-paths.md | |
| if-no-files-found: ignore | |
| dashboard: | |
| concurrency: | |
| group: gh-pages-deploy | |
| cancel-in-progress: false | |
| name: Update ATOMesh Benchmark Dashboard | |
| needs: [summarize-results] | |
| if: >- | |
| !cancelled() | |
| && needs.summarize-results.result == 'success' | |
| && needs.summarize-results.outputs.has_results == 'true' | |
| && ( | |
| github.event_name == 'schedule' | |
| || (github.event_name == 'workflow_dispatch' && inputs.publish_dashboard) | |
| ) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout ATOM repo | |
| uses: actions/checkout@v6 | |
| - name: Download CI benchmark dashboard inputs | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: atomesh-benchmark-summary | |
| path: atomesh-dashboard-input | |
| - name: Prepare ATOMesh dashboard assets | |
| run: | | |
| set -euo pipefail | |
| test -s atomesh-dashboard-input/atomesh-benchmark-action.json | |
| cp .github/dashboard/index.html /tmp/atomesh_dashboard_index.html | |
| - name: Prepare existing dashboard data | |
| id: prepare_dashboard_data | |
| uses: ./.github/actions/prepare-dashboard-data | |
| with: | |
| data-path: benchmark-dashboard/data.js | |
| - name: Store ATOMesh benchmark result to dashboard | |
| uses: benchmark-action/github-action-benchmark@v1 | |
| with: | |
| tool: customBiggerIsBetter | |
| output-file-path: atomesh-dashboard-input/atomesh-benchmark-action.json | |
| gh-pages-branch: gh-pages | |
| benchmark-data-dir-path: benchmark-dashboard | |
| auto-push: false | |
| max-items-in-chart: 300 | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Validate dashboard data after write | |
| uses: ./.github/actions/validate-dashboard-data | |
| with: | |
| data-path: benchmark-dashboard/data.js | |
| min-entries: ${{ steps.prepare_dashboard_data.outputs.entry_count }} | |
| - name: Deploy ATOMesh dashboard shell to gh-pages | |
| run: | | |
| set -euo pipefail | |
| 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 | |
| git pull --ff-only origin gh-pages | |
| mkdir -p benchmark-dashboard | |
| cp /tmp/atomesh_dashboard_index.html benchmark-dashboard/index.html | |
| git add benchmark-dashboard/index.html | |
| git diff --cached --quiet || git commit -m "Update ATOMesh benchmark dashboard" | |
| git push origin gh-pages |