ATOM Benchmark #65
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 Benchmark | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| on: | |
| schedule: | |
| # Nightly at 01:00 Beijing time (17:00 UTC) | |
| - cron: '0 17 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| deepseek-r1-0528: | |
| description: "Benchmark DeepSeek-R1-0528 (+ MTP3)" | |
| type: boolean | |
| default: true | |
| glm-5-fp8: | |
| description: "Benchmark GLM-5-FP8" | |
| type: boolean | |
| default: true | |
| deepseek-r1-0528-mxfp4: | |
| description: "Benchmark DeepSeek-R1-0528 MXFP4 (+ MXFP4-MTP3)" | |
| type: boolean | |
| default: true | |
| gpt-oss-120b: | |
| description: "Benchmark gpt-oss-120b" | |
| type: boolean | |
| default: true | |
| kimi-k25-mxfp4: | |
| description: "Benchmark Kimi-K2.5-MXFP4" | |
| type: boolean | |
| default: true | |
| MiniMax-M2.5: | |
| description: "Benchmark MiniMax-M2.5" | |
| type: boolean | |
| default: true | |
| extra_args: | |
| description: "Extra arguments to pass to the ATOM server" | |
| type: string | |
| image: | |
| description: "Image to use for the benchmark" | |
| type: string | |
| default: "rocm/atom-dev:latest" | |
| enable_profiler: | |
| description: "Enable torch profiler to collect trace for performance debugging" | |
| type: boolean | |
| default: false | |
| param_lists: | |
| description: | | |
| "Benchmark parameter lists. | |
| Input as a single or multiple sets (comma-separated, semicolon between sets), | |
| format: input_length,output_length,concurrency,random_range_ratio. | |
| Example (single set): 1024,1024,128,0.8 | |
| Example (multiple sets): 1024,1024,128,0.8;2048,1024,256,0.7" | |
| type: string | |
| default: "1024,1024,128,0.8" | |
| jobs: | |
| parse-param-lists: | |
| name: Parse parameter lists | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix_json: ${{ steps.parse-param-lists.outputs.matrix_json }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Parse parameter lists | |
| id: parse-param-lists | |
| run: | | |
| if [ "${{ github.event_name }}" = "schedule" ]; then | |
| echo "Using nightly param lists from .github/benchmark/nightly_params.json" | |
| # Expand grouped format (isl/osl + concurrency array) into flat matrix | |
| MATRIX_JSON=$(jq -c '[.[] | . as $group | .concurrency[] | {input_length: $group.isl, output_length: $group.osl, concurrency: ., random_range_ratio: $group.random_range_ratio}]' .github/benchmark/nightly_params.json) | |
| else | |
| PARAM_LISTS="${{ inputs.param_lists || '1024,1024,128,0.8' }}" | |
| echo "Using param_lists: ${PARAM_LISTS}" | |
| IFS=';' read -ra SETS <<< "${PARAM_LISTS}" | |
| MATRIX_JSON="[" | |
| SEP="" | |
| for SET in "${SETS[@]}"; do | |
| IFS=',' read -ra PARAMS <<< "$SET" | |
| MATRIX_JSON="${MATRIX_JSON}${SEP}{\"input_length\":${PARAMS[0]},\"output_length\":${PARAMS[1]},\"concurrency\":${PARAMS[2]},\"random_range_ratio\":${PARAMS[3]}}" | |
| SEP="," | |
| done | |
| MATRIX_JSON="${MATRIX_JSON}]" | |
| fi | |
| echo "Matrix JSON: ${MATRIX_JSON}" | |
| echo "matrix_json=${MATRIX_JSON}" >> $GITHUB_OUTPUT | |
| load-models: | |
| name: Load model configs | |
| runs-on: ubuntu-latest | |
| outputs: | |
| models_json: ${{ steps.load.outputs.models_json }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - id: load | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| INPUTS_JSON: ${{ toJson(inputs) }} | |
| run: | | |
| python3 << 'PY' | |
| import json, os | |
| event = os.environ["EVENT_NAME"] | |
| inputs = json.loads(os.environ.get("INPUTS_JSON", "{}")) | |
| models = json.load(open(".github/benchmark/models.json", encoding="utf-8")) | |
| if event == "schedule": | |
| filtered = models | |
| else: | |
| filtered = [m for m in models if inputs.get(m["prefix"], True)] | |
| with open(os.environ["GITHUB_OUTPUT"], "a") as f: | |
| f.write(f"models_json={json.dumps(filtered)}\n") | |
| print(f"Event={event}: {len(filtered)}/{len(models)} models enabled") | |
| PY | |
| benchmark: | |
| name: ${{ matrix.model.display }} (isl=${{ matrix.config.input_length }} osl=${{ matrix.config.output_length }} c=${{ matrix.config.concurrency }}) | |
| needs: [parse-param-lists, load-models] | |
| if: >- | |
| always() | |
| && needs.parse-param-lists.result == 'success' | |
| && needs.load-models.result == 'success' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| model: ${{ fromJson(needs.load-models.outputs.models_json) }} | |
| config: ${{ fromJson(needs.parse-param-lists.outputs.matrix_json) }} | |
| exclude: | |
| # Skip low-concurrency MTP runs to limit CI time | |
| - model: { suffix: "-mtp3" } | |
| config: { concurrency: 1 } | |
| - model: { suffix: "-mtp3" } | |
| config: { concurrency: 2 } | |
| runs-on: ${{ matrix.model.runner }} | |
| env: | |
| MODEL_PATH: ${{ matrix.model.path }} | |
| ARGS: ${{ matrix.model.args }} | |
| ISL: ${{ matrix.config.input_length }} | |
| OSL: ${{ matrix.config.output_length }} | |
| CONC: ${{ matrix.config.concurrency }} | |
| RANDOM_RANGE_RATIO: ${{ matrix.config.random_range_ratio }} | |
| RESULT_FILENAME: ${{ matrix.model.prefix }}${{ matrix.model.suffix }}-${{ matrix.config.input_length }}-${{ matrix.config.output_length }}-${{ matrix.config.concurrency }}-${{ matrix.config.random_range_ratio }} | |
| steps: | |
| - name: Kill all Docker containers | |
| run: | | |
| echo "=== Cleaning up containers on $(hostname) ===" | |
| containers=$(docker ps -q) | |
| if [ -n "$containers" ]; then | |
| docker kill $containers || true | |
| fi | |
| docker run --rm -v "${GITHUB_WORKSPACE:-$PWD}":/workspace -w /workspace --privileged rocm/pytorch:latest bash -lc "ls -la /workspace/ && find /workspace -mindepth 1 -delete" || true | |
| - name: Show ROCm status (host) | |
| run: docker ps -a && rocm-smi --showmemuse 2>/dev/null || true | |
| - name: Checkout ATOM repo | |
| uses: actions/checkout@v6 | |
| - name: Start CI container | |
| run: | | |
| docker ps -aq -f name=atom-benchmark | xargs -r docker stop | xargs -r docker rm | |
| DEVICE_FLAG=$(cat /etc/podinfo/gha-render-devices 2>/dev/null || echo "--device /dev/dri") | |
| MODEL_MOUNT="" | |
| [ -d "/models" ] && MODEL_MOUNT="-v /models:/models" | |
| # Build env var flags from model config | |
| ENV_FLAGS="" | |
| if [ -n "${{ matrix.model.env_vars }}" ]; then | |
| for ev in ${{ matrix.model.env_vars }}; do | |
| ENV_FLAGS="$ENV_FLAGS -e $ev" | |
| done | |
| fi | |
| docker run -dt --device=/dev/kfd $DEVICE_FLAG \ | |
| -v "${GITHUB_WORKSPACE:-$PWD}":/workspace $MODEL_MOUNT \ | |
| -w /workspace --ipc=host --group-add video \ | |
| --shm-size=16G --privileged --cap-add=SYS_PTRACE \ | |
| -e HF_TOKEN="${HF_TOKEN:-}" \ | |
| --security-opt seccomp=unconfined \ | |
| --ulimit memlock=-1 --ulimit stack=67108864 --pull always \ | |
| -e ATOM_DISABLE_MMAP=true \ | |
| -e ISL=${{ env.ISL }} -e OSL=${{ env.OSL }} \ | |
| -e CONC=${{ env.CONC }} -e RANDOM_RANGE_RATIO=${{ env.RANDOM_RANGE_RATIO }} \ | |
| -e ENABLE_TORCH_PROFILER=${{ inputs.enable_profiler && '1' || '0' }} \ | |
| $ENV_FLAGS \ | |
| --name atom-benchmark \ | |
| ${{ inputs.image || 'rocm/atom-dev:latest' }} | |
| env: | |
| GITHUB_WORKSPACE: ${{ github.workspace }} | |
| - name: Collect GPU info (inside container) | |
| id: gpu-info | |
| run: | | |
| # Run rocm-smi inside the container for consistent output across runners | |
| GPU_NAME=$(docker exec atom-benchmark rocm-smi --showproductname 2>/dev/null | grep -i "Card Series" | head -1 | sed 's/.*:\s*//' || echo "") | |
| GPU_VRAM_GB=$(docker exec atom-benchmark rocm-smi --showmeminfo vram 2>/dev/null | grep -i "VRAM Total Memory" | head -1 | awk -F: '{printf "%.0f", $NF/(1024*1024*1024)}' || echo "0") | |
| ROCM_VERSION=$(docker exec atom-benchmark cat /opt/rocm/.info/version 2>/dev/null || echo "unknown") | |
| echo "gpu_name=${GPU_NAME}" >> $GITHUB_OUTPUT | |
| echo "gpu_vram_gb=${GPU_VRAM_GB}" >> $GITHUB_OUTPUT | |
| echo "rocm_version=${ROCM_VERSION}" >> $GITHUB_OUTPUT | |
| echo "GPU: ${GPU_NAME}, VRAM: ${GPU_VRAM_GB}GB, ROCm: ${ROCM_VERSION}" | |
| - name: Download models | |
| run: | | |
| if [ -d "/models" ]; then | |
| docker exec -e HF_TOKEN=${{ secrets.AMD_HF_TOKEN }} atom-benchmark bash -lc \ | |
| "hf download ${{ env.MODEL_PATH }} --local-dir /models/${{ env.MODEL_PATH }}" || exit 1 | |
| fi | |
| - name: Run benchmark | |
| timeout-minutes: 60 | |
| run: | | |
| set -euo pipefail | |
| if [ -d "/models" ]; then model_path="/models/${{ env.MODEL_PATH }}" | |
| else model_path="${{ env.MODEL_PATH }}"; fi | |
| docker exec atom-benchmark bash -lc "set -euo pipefail | |
| ENABLE_TORCH_PROFILER=${{ inputs.enable_profiler && '1' || '0' }} \ | |
| .github/scripts/atom_test.sh launch $model_path ${{ env.ARGS }} ${{ inputs.extra_args || '' }} | |
| echo '========== Running benchmark ==========' | |
| ENABLE_TORCH_PROFILER=${{ inputs.enable_profiler && '1' || '0' }} \ | |
| RESULT_FILENAME=${{ env.RESULT_FILENAME }} \ | |
| SERVER_ARGS='${{ env.ARGS }}' \ | |
| BENCH_EXTRA_ARGS='${{ matrix.model.bench_args }}' \ | |
| .github/scripts/atom_test.sh benchmark $model_path | |
| " | |
| - name: Inject GPU metadata into benchmark result | |
| run: | | |
| docker exec \ | |
| -e GPU_NAME="${{ steps.gpu-info.outputs.gpu_name }}" \ | |
| -e GPU_VRAM_GB="${{ steps.gpu-info.outputs.gpu_vram_gb }}" \ | |
| -e ROCM_VERSION="${{ steps.gpu-info.outputs.rocm_version }}" \ | |
| -e DOCKER_IMAGE="${{ inputs.image || 'rocm/atom-dev:latest' }}" \ | |
| -e RESULT_PATH="${{ env.RESULT_FILENAME }}.json" \ | |
| atom-benchmark python3 -c " | |
| import json, os | |
| p = os.environ['RESULT_PATH'] | |
| if not os.path.exists(p): | |
| print(f'{p} not found, skipping GPU metadata injection') | |
| else: | |
| with open(p) as f: | |
| d = json.load(f) | |
| d['gpu_name'] = os.environ.get('GPU_NAME', '') | |
| d['gpu_vram_gb'] = int(os.environ.get('GPU_VRAM_GB') or 0) | |
| d['rocm_version'] = os.environ.get('ROCM_VERSION', '') | |
| d['docker_image'] = os.environ.get('DOCKER_IMAGE', '') | |
| with open(p, 'w') as f: | |
| json.dump(d, f, indent=2) | |
| " | |
| - name: Copy profiler traces | |
| if: inputs.enable_profiler | |
| run: docker cp atom-benchmark:/app/trace ./profiler-traces 2>/dev/null || true | |
| - name: Upload profiler traces | |
| if: inputs.enable_profiler | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: profiler-traces-${{ env.RESULT_FILENAME }} | |
| path: profiler-traces/ | |
| - name: Upload benchmark result | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ env.RESULT_FILENAME }} | |
| path: ${{ env.RESULT_FILENAME }}.json | |
| - name: Clean Up | |
| if: always() | |
| run: | | |
| docker run --rm -v "${GITHUB_WORKSPACE:-$PWD}":/workspace -w /workspace --privileged \ | |
| ${{ inputs.image || 'rocm/atom-dev:latest' }} bash -lc "rm -rf /workspace/atom/ /workspace/aiter/ /workspace/bench_serving/" || true | |
| docker stop atom-benchmark || true | |
| docker rm atom-benchmark || true | |
| summarize-benchmark-result: | |
| if: always() | |
| name: Summarize benchmark result | |
| needs: [benchmark] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_regression: ${{ steps.check-regression.outputs.has_regression }} | |
| steps: | |
| - name: Checkout ATOM repo | |
| uses: actions/checkout@v6 | |
| - name: Download all benchmark results | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: '*' | |
| merge-multiple: true | |
| path: . | |
| - name: Download baseline from previous nightly run | |
| id: baseline | |
| run: | | |
| # Use the most recent completed nightly run (regardless of success/failure) | |
| # because even failed runs have valid per-model benchmark artifacts. | |
| # Only requiring --status=success misses baselines when any single job fails. | |
| PREV_RUN_ID=$(gh run list \ | |
| --workflow="ATOM Benchmark" \ | |
| --branch=main \ | |
| --event=schedule \ | |
| --limit=5 \ | |
| --json databaseId,status \ | |
| --jq '[.[] | select(.status == "completed")][0].databaseId // empty') | |
| if [ -n "$PREV_RUN_ID" ] && [ "$PREV_RUN_ID" != "${{ github.run_id }}" ]; then | |
| echo "Downloading baseline from run #$PREV_RUN_ID" | |
| mkdir -p /tmp/baseline | |
| gh run download "$PREV_RUN_ID" --dir /tmp/baseline || echo "::warning::Failed to download baseline artifacts" | |
| BASELINE_COUNT=$(find /tmp/baseline -name '*.json' | wc -l) | |
| echo "Downloaded $BASELINE_COUNT baseline files" | |
| echo "baseline_dir=/tmp/baseline" >> $GITHUB_OUTPUT | |
| else | |
| echo "No previous completed nightly run found" | |
| echo "baseline_dir=" >> $GITHUB_OUTPUT | |
| fi | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| - name: List all benchmark results | |
| run: | | |
| echo "=== Current results ===" | |
| ls -la *.json 2>/dev/null || echo "No JSON files in current dir" | |
| if [ -d "/tmp/baseline" ]; then | |
| echo "=== Baseline results ===" | |
| find /tmp/baseline -name '*.json' | head -20 | |
| fi | |
| - name: Summarize benchmark result | |
| id: check-regression | |
| run: | | |
| BASELINE_ARG="" | |
| if [ -n "${{ steps.baseline.outputs.baseline_dir }}" ]; then | |
| BASELINE_ARG="--baseline-dir ${{ steps.baseline.outputs.baseline_dir }}" | |
| fi | |
| .github/scripts/summarize.py . $BASELINE_ARG \ | |
| --output-json regression_report.json \ | |
| >> $GITHUB_STEP_SUMMARY || true | |
| # Check if regression was detected (exit code 2) | |
| if [ -f regression_report.json ]; then | |
| REG_COUNT=$(python3 -c "import json; print(json.load(open('regression_report.json'))['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: steps.check-regression.outputs.has_regression == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: regression-report | |
| path: regression_report.json | |
| - name: Transform results for benchmark dashboard | |
| run: | | |
| python3 -c " | |
| import json, glob, re | |
| run_url = f'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' | |
| entries = [] | |
| for f in sorted(glob.glob('*.json')): | |
| if f == 'regression_report.json': | |
| continue | |
| try: | |
| d = json.load(open(f)) | |
| except (json.JSONDecodeError, OSError): | |
| continue | |
| if 'output_throughput' not in d: | |
| continue | |
| model = d.get('model_id', '').split('/')[-1] | |
| m = re.search(r'-(mtp\d*)-', f) | |
| if m: | |
| model = f'{model}-{m.group(1)}' | |
| isl = d.get('random_input_len', 0) | |
| osl = d.get('random_output_len', 0) | |
| conc = d.get('max_concurrency', 0) | |
| label = f'{model} {isl}/{osl} c={conc}' | |
| gpu_name = d.get('gpu_name', '') | |
| gpu_vram = d.get('gpu_vram_gb', 0) | |
| rocm_ver = d.get('rocm_version', '') | |
| extra = f'Run: {run_url}' | |
| if gpu_name: | |
| extra += f' | GPU: {gpu_name}' | |
| if gpu_vram: | |
| extra += f' | VRAM: {gpu_vram}GB' | |
| if rocm_ver: | |
| extra += f' | ROCm: {rocm_ver}' | |
| entries.append({'name': f'{label} throughput (tok/s)', 'unit': 'tok/s', | |
| 'value': round(d['output_throughput'], 2), 'extra': extra}) | |
| entries.append({'name': f'{label} Total Tput (tok/s)', 'unit': 'tok/s', | |
| 'value': round(d.get('total_token_throughput', 0), 2), 'extra': extra}) | |
| entries.append({'name': f'{label} TTFT (ms)', 'unit': 'ms', | |
| 'value': round(d.get('mean_ttft_ms', 0), 2), 'extra': extra}) | |
| entries.append({'name': f'{label} TPOT (ms)', 'unit': 'ms', | |
| 'value': round(d.get('mean_tpot_ms', 0), 2), 'extra': extra}) | |
| tp = d.get('tensor_parallel_size', 1) | |
| entries.append({'name': f'{label} _gpu_count', 'unit': '', | |
| 'value': int(tp)}) | |
| json.dump(entries, open('benchmark-action-input.json', 'w'), indent=2) | |
| print(f'Generated {len(entries)} entries for benchmark dashboard') | |
| " | |
| - name: Store benchmark result to dashboard | |
| 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: 90 | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Deploy custom dashboard to gh-pages | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| CURRENT_SHA=$(git rev-parse HEAD) | |
| # Save dashboard assets before switching branches | |
| cp .github/dashboard/index.html /tmp/dashboard_index.html | |
| cp docs/assets/atom_logo.png /tmp/dashboard_logo.png | |
| git fetch origin gh-pages | |
| git checkout gh-pages | |
| # Replace auto-generated index.html with custom dashboard + logo | |
| cp /tmp/dashboard_index.html benchmark-dashboard/index.html | |
| cp /tmp/dashboard_logo.png benchmark-dashboard/atom_logo.png | |
| git add benchmark-dashboard/ | |
| git diff --cached --quiet || git commit -m "Update benchmark data and dashboard" | |
| git push origin gh-pages | |
| git checkout "$CURRENT_SHA" | |
| # ---------- Generate regression matrix (lightweight, ubuntu) ---------- | |
| generate-regression-matrix: | |
| if: always() && needs.summarize-benchmark-result.outputs.has_regression == 'true' | |
| name: Generate regression rerun matrix | |
| needs: [summarize-benchmark-result] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix_json: ${{ steps.gen.outputs.matrix_json }} | |
| has_matrix: ${{ steps.gen.outputs.has_matrix }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: regression-report | |
| - name: Generate matrix from regression report | |
| id: gen | |
| run: | | |
| python3 .github/scripts/regression_rerun.py \ | |
| regression_report.json \ | |
| .github/benchmark/models.json \ | |
| --output-matrix /tmp/matrix.json | |
| if [ -s /tmp/matrix.json ] && [ "$(cat /tmp/matrix.json)" != "[]" ]; then | |
| echo "matrix_json=$(cat /tmp/matrix.json)" >> $GITHUB_OUTPUT | |
| echo "has_matrix=true" >> $GITHUB_OUTPUT | |
| echo "=== Matrix cells ===" | |
| python3 -c " | |
| import json | |
| cells = json.load(open('/tmp/matrix.json')) | |
| for c in cells: | |
| configs = json.loads(c['configs']) | |
| print(f\" {c['prefix']}: {len(configs)} config(s) on {c['runner']}\") | |
| " | |
| else | |
| echo "matrix_json=[]" >> $GITHUB_OUTPUT | |
| echo "has_matrix=false" >> $GITHUB_OUTPUT | |
| echo "No regression configs generated" | |
| fi | |
| # ---------- Regression re-run (GPU, matrix by model) ---------- | |
| regression-rerun: | |
| if: >- | |
| always() | |
| && needs.generate-regression-matrix.outputs.has_matrix == 'true' | |
| name: Rerun ${{ matrix.cell.prefix }} | |
| needs: [generate-regression-matrix] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| cell: ${{ fromJson(needs.generate-regression-matrix.outputs.matrix_json) }} | |
| runs-on: ${{ matrix.cell.runner }} | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Kill all Docker containers | |
| run: | | |
| echo "=== Cleaning up containers on $(hostname) ===" | |
| containers=$(docker ps -q) | |
| if [ -n "$containers" ]; then | |
| docker kill $containers || true | |
| fi | |
| docker run --rm -v "${GITHUB_WORKSPACE:-$PWD}":/workspace -w /workspace --privileged rocm/pytorch:latest bash -lc "find /workspace -mindepth 1 -delete" || true | |
| - name: Checkout ATOM repo | |
| uses: actions/checkout@v6 | |
| - name: Docker Login | |
| run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin | |
| - name: Start CI container | |
| run: | | |
| DEVICE_FLAG=$(cat /etc/podinfo/gha-render-devices 2>/dev/null || echo "--device /dev/dri") | |
| MODEL_MOUNT="" | |
| [ -d "/models" ] && MODEL_MOUNT="-v /models:/models" | |
| # Build env var flags from model config | |
| ENV_FLAGS="" | |
| if [ -n "${{ matrix.cell.env_vars }}" ]; then | |
| for ev in ${{ matrix.cell.env_vars }}; do | |
| ENV_FLAGS="$ENV_FLAGS -e $ev" | |
| done | |
| fi | |
| docker run -dt --device=/dev/kfd $DEVICE_FLAG \ | |
| -v "${GITHUB_WORKSPACE:-$PWD}":/workspace $MODEL_MOUNT \ | |
| -w /workspace --ipc=host --group-add video \ | |
| --shm-size=16G --privileged --cap-add=SYS_PTRACE \ | |
| -e HF_TOKEN="${HF_TOKEN:-}" \ | |
| --security-opt seccomp=unconfined \ | |
| --ulimit memlock=-1 --ulimit stack=67108864 --pull always \ | |
| -e ATOM_DISABLE_MMAP=true \ | |
| $ENV_FLAGS \ | |
| --name atom-regression \ | |
| ${{ inputs.image || 'rocm/atom-dev:latest' }} | |
| env: | |
| GITHUB_WORKSPACE: ${{ github.workspace }} | |
| - name: Download model | |
| run: | | |
| if [ -d "/models" ]; then | |
| echo "=== Downloading ${{ matrix.cell.model_path }} ===" | |
| docker exec -e HF_TOKEN=${{ secrets.AMD_HF_TOKEN }} atom-regression bash -lc \ | |
| "hf download ${{ matrix.cell.model_path }} --local-dir /models/${{ matrix.cell.model_path }}" \ | |
| || echo "::warning::Failed to download ${{ matrix.cell.model_path }}, will use HF online" | |
| fi | |
| - name: Launch server | |
| run: | | |
| if [ -d "/models" ]; then MODEL_DIR="/models/${{ matrix.cell.model_path }}" | |
| else MODEL_DIR="${{ matrix.cell.model_path }}"; fi | |
| docker exec atom-regression bash -lc "set -euo pipefail | |
| ${{ matrix.cell.env_vars }} \ | |
| ENABLE_TORCH_PROFILER=1 \ | |
| .github/scripts/atom_test.sh launch $MODEL_DIR ${{ matrix.cell.server_args }}" | |
| - name: Run regression configs with profiler | |
| run: | | |
| set -uo pipefail # no -e: allow per-config failure without aborting loop | |
| if [ -d "/models" ]; then MODEL_DIR="/models/${{ matrix.cell.model_path }}" | |
| else MODEL_DIR="${{ matrix.cell.model_path }}"; fi | |
| FAIL_COUNT=0 | |
| TOTAL_COUNT=0 | |
| # Parse configs JSON into pipe-delimited lines for shell loop | |
| echo '${{ matrix.cell.configs }}' | python3 -c " | |
| import json, sys | |
| for c in json.load(sys.stdin): | |
| print('{isl}|{osl}|{conc}|{bench_args}|{prefix}'.format(**c)) | |
| " > /tmp/regression_configs.txt | |
| while IFS='|' read -r ISL OSL CONC BENCH_ARGS PREFIX; do | |
| TOTAL_COUNT=$((TOTAL_COUNT + 1)) | |
| TRACE_SUBDIR="${PREFIX}-${ISL}-${OSL}-${CONC}" | |
| docker exec atom-regression bash -lc "mkdir -p /app/trace/$TRACE_SUBDIR" | |
| # Record profiler stop count before benchmark (to detect new completions) | |
| STOP_COUNT_BEFORE=$(docker exec atom-regression grep -c "Profiler stopped." /tmp/atom_server.log 2>/dev/null || echo 0) | |
| echo "=== Profiling: $PREFIX ISL=$ISL OSL=$OSL CONC=$CONC ===" | |
| if ! docker exec atom-regression bash -lc " | |
| ${{ matrix.cell.env_vars }} \ | |
| ENABLE_TORCH_PROFILER=1 \ | |
| ISL=$ISL OSL=$OSL CONC=$CONC \ | |
| RANDOM_RANGE_RATIO=0.8 \ | |
| NUM_PROMPTS_OVERRIDE=$((CONC*2)) \ | |
| RESULT_FILENAME=regression-${PREFIX}-${ISL}-${OSL}-${CONC} \ | |
| BENCH_EXTRA_ARGS='$BENCH_ARGS' \ | |
| SERVER_ARGS='${{ matrix.cell.server_args }}' \ | |
| .github/scripts/atom_test.sh benchmark $MODEL_DIR"; then | |
| echo "::warning::Benchmark failed for $PREFIX ISL=$ISL OSL=$OSL CONC=$CONC" | |
| FAIL_COUNT=$((FAIL_COUNT + 1)) | |
| continue | |
| fi | |
| # Wait for engine core to finish exporting profiler trace (blocks inference) | |
| # See .claude/plan/profiler-async-refactor.md for long-term fix | |
| for i in $(seq 1 300); do | |
| STOP_COUNT=$(docker exec atom-regression grep -c "Profiler stopped." /tmp/atom_server.log 2>/dev/null || echo 0) | |
| [ "$STOP_COUNT" -gt "$STOP_COUNT_BEFORE" ] && echo "Profiler trace export done after ${i}s" && break | |
| [ "$i" -eq 1 ] && echo "Waiting for profiler trace export..." | |
| [ "$i" -eq 300 ] && echo "WARNING: Profiler did not finish after 300s, proceeding anyway" | |
| sleep 1 | |
| done | |
| # Move trace files into per-config subdir | |
| docker exec atom-regression bash -lc " | |
| for d in /app/trace/rank_*; do | |
| [ -d \"\$d\" ] && mv \"\$d\" /app/trace/$TRACE_SUBDIR/ 2>/dev/null || true | |
| done" | |
| done < /tmp/regression_configs.txt | |
| echo "=== Regression re-run complete: $((TOTAL_COUNT - FAIL_COUNT))/$TOTAL_COUNT succeeded ===" | |
| if [ "$FAIL_COUNT" -gt 0 ]; then | |
| echo "::warning::$FAIL_COUNT/$TOTAL_COUNT configs failed" | |
| fi | |
| - name: Stop server | |
| if: always() | |
| run: | | |
| docker exec atom-regression bash -lc \ | |
| "TORCH_PROFILER_DIR=/app/trace .github/scripts/atom_test.sh stop" || true | |
| - name: Copy profiler traces from container | |
| id: copy-traces | |
| if: always() | |
| run: | | |
| docker cp atom-regression:/app/trace ./regression-traces 2>/dev/null || echo "No traces found" | |
| TRACE_COUNT=$(find ./regression-traces -type f \( -name "*.json.gz" -o -name "*.pt.trace.*" \) 2>/dev/null | wc -l | tr -d ' ') | |
| echo "trace_count=$TRACE_COUNT" >> $GITHUB_OUTPUT | |
| if [ "$TRACE_COUNT" -gt 0 ]; then | |
| echo "Found $TRACE_COUNT trace files" | |
| ls -lhR ./regression-traces/ | |
| else | |
| echo "::warning::No trace files found in container — profiler may not have produced output" | |
| fi | |
| - name: Upload regression traces | |
| if: always() && fromJSON(steps.copy-traces.outputs.trace_count) > 0 | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: regression-traces-${{ github.run_id }}-${{ matrix.cell.prefix }} | |
| path: regression-traces/ | |
| - name: Upload regression results | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: regression-results-${{ github.run_id }}-${{ matrix.cell.prefix }} | |
| path: regression-*.json | |
| if-no-files-found: ignore | |
| - name: Clean Up | |
| if: always() | |
| run: | | |
| docker stop atom-regression || true | |
| docker rm atom-regression || true | |
| # ---------- Collect regression traces from all matrix cells ---------- | |
| collect-regression-traces: | |
| if: always() && needs.regression-rerun.result != 'skipped' | |
| name: Collect regression traces | |
| needs: [regression-rerun] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download all regression trace artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: regression-traces-${{ github.run_id }}-* | |
| merge-multiple: true | |
| path: regression-traces/ | |
| continue-on-error: true | |
| - name: Upload combined regression traces | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: regression-traces-${{ github.run_id }} | |
| path: regression-traces/ | |
| if-no-files-found: ignore | |
| # ---------- Profiler analysis (lightweight, runs on regression OR manual profiler) ---------- | |
| profiler-analysis: | |
| if: >- | |
| always() | |
| && (needs.collect-regression-traces.result != 'cancelled' | |
| && needs.collect-regression-traces.result != 'skipped' | |
| || inputs.enable_profiler == true) | |
| name: Profiler trace analysis | |
| needs: [benchmark, summarize-benchmark-result, generate-regression-matrix, regression-rerun, collect-regression-traces] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout ATOM repo | |
| uses: actions/checkout@v6 | |
| - name: Download regression traces | |
| if: needs.collect-regression-traces.result == 'success' | |
| uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| name: regression-traces-${{ github.run_id }} | |
| path: profiler-traces/ | |
| - name: Download benchmark profiler traces | |
| if: needs.collect-regression-traces.result != 'success' | |
| uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| pattern: profiler-traces-* | |
| merge-multiple: true | |
| path: profiler-traces/ | |
| - name: Analyze traces | |
| run: | | |
| mkdir -p profiler-analysis | |
| pip install -q openpyxl 2>/dev/null || true | |
| # Find all config subdirectories containing rank_* dirs | |
| # Structure: profiler-traces/<config-name>/rank_0/*.gz | |
| CONFIG_DIRS=$(find profiler-traces -mindepth 1 -maxdepth 1 -type d 2>/dev/null || true) | |
| if [ -z "$CONFIG_DIRS" ]; then | |
| echo "No config directories found in profiler-traces/" | |
| ls -lhR profiler-traces/ 2>/dev/null || true | |
| exit 0 | |
| fi | |
| for config_dir in $CONFIG_DIRS; do | |
| config=$(basename "$config_dir") | |
| echo "" | |
| echo "========== Config: $config ==========" | |
| mkdir -p "profiler-analysis/$config" | |
| # Analyze rank_0 trace (representative for kernel breakdown + summary) | |
| RANK0_TRACE=$(find "$config_dir/rank_0" -name "*.pt.trace.json.gz" ! -name "capture_graph_*" 2>/dev/null | sort | tail -1) | |
| if [ -z "$RANK0_TRACE" ]; then | |
| echo "No rank_0 trace found for $config, skipping" | |
| continue | |
| fi | |
| echo "=== Analyzing: $RANK0_TRACE ===" | |
| cd "profiler-analysis/$config" | |
| python ../../tools/parse_trace.py "../../$RANK0_TRACE" --layer 3 2>&1 | tee analysis.log || true | |
| python ../../tools/analyze_trace_summary.py "../../$RANK0_TRACE" \ | |
| --output performance_summary.md 2>&1 || true | |
| if [ -f performance_summary.md ]; then | |
| echo "=== Performance Summary ($config) ===" | |
| cat performance_summary.md | |
| fi | |
| cd ../.. | |
| done | |
| ls -laR profiler-analysis/ 2>/dev/null || true | |
| - name: Upload analysis results | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: profiler-analysis-${{ github.run_id }} | |
| path: profiler-analysis/ | |
| - name: Download regression report | |
| if: needs.summarize-benchmark-result.outputs.has_regression == 'true' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: regression-report | |
| continue-on-error: true | |
| - name: Create GitHub Issue for regression | |
| if: needs.summarize-benchmark-result.outputs.has_regression == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let report; | |
| try { | |
| report = JSON.parse(fs.readFileSync('regression_report.json', 'utf8')); | |
| } catch (e) { | |
| console.log('No regression report found, skipping issue creation'); | |
| return; | |
| } | |
| if (!report.regressions || report.regressions.length === 0) return; | |
| const regs = report.regressions.map(r => { | |
| const throughput = r.metrics.output_throughput || {}; | |
| const tpot = r.metrics.mean_tpot_ms || {}; | |
| return `| ${r.model} | ${r.isl}/${r.osl} | ${r.conc} | ${throughput.current?.toFixed(1) || 'N/A'} | ${throughput.baseline?.toFixed(1) || 'N/A'} | ${throughput.pct?.toFixed(1) || 'N/A'}% | ${tpot.current?.toFixed(2) || 'N/A'} | ${tpot.baseline?.toFixed(2) || 'N/A'} | ${tpot.pct?.toFixed(1) || 'N/A'}% |`; | |
| }).join('\n'); | |
| let summary = 'Summary not available'; | |
| try { | |
| const dirs = fs.readdirSync('profiler-analysis').filter(d => | |
| fs.statSync(`profiler-analysis/${d}`).isDirectory()); | |
| for (const d of dirs) { | |
| const p = `profiler-analysis/${d}/performance_summary.md`; | |
| if (fs.existsSync(p)) { summary = fs.readFileSync(p, 'utf8'); break; } | |
| } | |
| } catch(e) { /* ignore */ } | |
| const body = `## Performance Regression Detected | |
| **Commit:** \`${context.sha.substring(0, 8)}\` | |
| **Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId} | |
| **Date:** ${report.timestamp} | |
| ### Regressed Configurations | |
| | Model | ISL/OSL | Conc | Tput (cur) | Tput (base) | Δ% | TPOT (cur) | TPOT (base) | Δ% | | |
| |-------|---------|------|-----------|------------|-----|-----------|------------|-----| | |
| ${regs} | |
| ### Performance Summary | |
| \`\`\` | |
| ${summary} | |
| \`\`\` | |
| ### Profiler Traces | |
| Download from [workflow artifacts](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}). | |
| Open in [Perfetto UI](https://ui.perfetto.dev/) or Chrome \`chrome://tracing\` for analysis. | |
| ### Next Steps | |
| 1. Download \`profiler-analysis-${context.runId}\` artifact | |
| 2. Open trace files in Perfetto UI | |
| 3. Compare kernel durations against previous traces | |
| 4. Identify bottleneck changes | |
| `.replace(/^ /gm, ''); | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `[Perf Regression] ${report.regressions.length} config(s) regressed @ ${context.sha.substring(0, 8)}`, | |
| body: body, | |
| labels: ['performance', 'regression'] | |
| }); |