|
| 1 | +name: Benchmarks |
| 2 | + |
| 3 | +# Flattening benchmarks on the SLURM cluster. GitHub-hosted runners cannot reach |
| 4 | +# it, and running a self-hosted runner on a shared academic machine is its own |
| 5 | +# maintenance burden, so the runner connects over SSH instead: it ships the |
| 6 | +# compiler across, submits the work, and copies the results back. Everything |
| 7 | +# heavy happens on the cluster. |
| 8 | +# |
| 9 | +# Secrets: |
| 10 | +# BENCH_SSH_KEY private key, no passphrase, whose public half is in |
| 11 | +# the cluster account's authorized_keys |
| 12 | +# BENCH_SSH_HOST hostname of the submit node |
| 13 | +# BENCH_SSH_USER account to connect as |
| 14 | +# BENCH_SSH_KNOWN_HOSTS output of `ssh-keyscan <host>`. Without it the |
| 15 | +# connection would be trusted blindly, which is how one |
| 16 | +# ends up benchmarking someone else's machine. |
| 17 | +# |
| 18 | +# Two steps, because these runs take hours: `submit` queues an sbatch job and |
| 19 | +# exits, and `collect` fetches the results once it has finished. Holding an SSH |
| 20 | +# session open for the duration would be at the mercy of idle timeouts, network |
| 21 | +# blips and the six-hour limit on a job, and would bill a runner for the wait. |
| 22 | + |
| 23 | +on: |
| 24 | + workflow_dispatch: |
| 25 | + inputs: |
| 26 | + action: |
| 27 | + description: "submit queues the job; collect fetches a finished one." |
| 28 | + type: choice |
| 29 | + options: [submit, collect] |
| 30 | + default: submit |
| 31 | + mznlib: |
| 32 | + description: "Library to flatten against (submit)." |
| 33 | + type: choice |
| 34 | + options: [std, linear] |
| 35 | + default: std |
| 36 | + release: |
| 37 | + description: "libminizinc release to benchmark (blank = edge, submit)." |
| 38 | + type: string |
| 39 | + default: "" |
| 40 | + job: |
| 41 | + description: "Remote directory reported by submit, e.g. 12345678 (collect)." |
| 42 | + type: string |
| 43 | + default: "" |
| 44 | + |
| 45 | +permissions: |
| 46 | + contents: read |
| 47 | + |
| 48 | +env: |
| 49 | + GH_TOKEN: ${{ github.token }} |
| 50 | + |
| 51 | +jobs: |
| 52 | + bench: |
| 53 | + name: ${{ inputs.action }} (${{ inputs.mznlib }}) |
| 54 | + runs-on: ubuntu-24.04 |
| 55 | + steps: |
| 56 | + - uses: actions/checkout@v7 |
| 57 | + |
| 58 | + - name: Set up SSH |
| 59 | + shell: bash |
| 60 | + run: | |
| 61 | + set -eu |
| 62 | + # Fail here rather than half way through a cluster job. |
| 63 | + test -n "${{ secrets.BENCH_SSH_KEY }}" |
| 64 | + test -n "${{ secrets.BENCH_SSH_KNOWN_HOSTS }}" |
| 65 | + mkdir -p ~/.ssh && chmod 700 ~/.ssh |
| 66 | + printf '%s\n' "${{ secrets.BENCH_SSH_KEY }}" > ~/.ssh/id_bench |
| 67 | + chmod 600 ~/.ssh/id_bench |
| 68 | + printf '%s\n' "${{ secrets.BENCH_SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts |
| 69 | + cat >> ~/.ssh/config <<'EOF' |
| 70 | + Host bench |
| 71 | + IdentityFile ~/.ssh/id_bench |
| 72 | + IdentitiesOnly yes |
| 73 | + StrictHostKeyChecking yes |
| 74 | + BatchMode yes |
| 75 | + EOF |
| 76 | + printf ' HostName %s\n User %s\n' \ |
| 77 | + "${{ secrets.BENCH_SSH_HOST }}" "${{ secrets.BENCH_SSH_USER }}" >> ~/.ssh/config |
| 78 | +
|
| 79 | + - name: Send the compiler and the benchmark script |
| 80 | + if: inputs.action == 'submit' |
| 81 | + shell: bash |
| 82 | + run: | |
| 83 | + set -eux |
| 84 | + ref="${{ inputs.release }}"; [ -n "$ref" ] || ref=edge |
| 85 | + gh release download "$ref" --repo "$GITHUB_REPOSITORY" \ |
| 86 | + --pattern "minizinc-compiler-only-x86_64-linux-gnu.tar.gz" --dir . --clobber |
| 87 | + # One directory per run: concurrent runs must not share state, and the |
| 88 | + # cluster keeps them around for post-mortems. |
| 89 | + remote="minizinc-bench/${GITHUB_RUN_ID}" |
| 90 | + echo "REMOTE=$remote" >> "$GITHUB_ENV" |
| 91 | + ssh bench "mkdir -p '$remote'" |
| 92 | + scp minizinc-compiler-only-x86_64-linux-gnu.tar.gz "bench:$remote/" |
| 93 | + scp tests/flattening/flatten.py "bench:$remote/" |
| 94 | +
|
| 95 | + - name: Queue the job |
| 96 | + if: inputs.action == 'submit' |
| 97 | + shell: bash |
| 98 | + run: | |
| 99 | + set -eux |
| 100 | + # sbatch, not a foreground srun: the work outlives this workflow. The |
| 101 | + # script is written on the cluster so nothing depends on the connection. |
| 102 | + ssh bench bash -seu <<EOF |
| 103 | + cd '$REMOTE' |
| 104 | + mkdir -p minizinc results |
| 105 | + tar -xzf minizinc-compiler-only-x86_64-linux-gnu.tar.gz -C minizinc |
| 106 | + cat > job.sh <<'SCRIPT' |
| 107 | + #!/bin/bash |
| 108 | + set -eux |
| 109 | + cd "\$(dirname "\$0")" |
| 110 | + curl --retry 10 --location --silent \ |
| 111 | + https://gitlab.com/-/snippets/2095682/raw/master/instances.csv | tr -d '\r' > instances.csv |
| 112 | + [ -d minizinc-benchmarks ] || git clone --depth 1 \ |
| 113 | + https://github.com/minizinc/minizinc-benchmarks.git |
| 114 | + python3 flatten.py --minizinc="\$PWD/minizinc/bin/minizinc" \ |
| 115 | + --prefix-args="srun -w critical001 --nice --mem=8192 -t 10" \ |
| 116 | + --args="-G ${{ inputs.mznlib }}" --parallel="-1" instances.csv results/stats.csv |
| 117 | + SCRIPT |
| 118 | + chmod +x job.sh |
| 119 | + sbatch --parsable --job-name=mzn-flatten-${{ inputs.mznlib }} \ |
| 120 | + --output=slurm-%j.out job.sh > jobid |
| 121 | + cat jobid |
| 122 | + EOF |
| 123 | + id=$(ssh bench "cat '$REMOTE/jobid'") |
| 124 | + { |
| 125 | + echo "### Benchmark submitted" |
| 126 | + echo |
| 127 | + echo "| | |" |
| 128 | + echo "|-|-|" |
| 129 | + echo "| SLURM job | \`$id\` |" |
| 130 | + echo "| directory | \`$REMOTE\` |" |
| 131 | + echo |
| 132 | + echo "Collect it with this workflow, action \`collect\`, job \`$GITHUB_RUN_ID\`." |
| 133 | + } >> "$GITHUB_STEP_SUMMARY" |
| 134 | +
|
| 135 | + - name: Collect the results |
| 136 | + if: inputs.action == 'collect' |
| 137 | + shell: bash |
| 138 | + run: | |
| 139 | + set -eux |
| 140 | + test -n "${{ inputs.job }}" |
| 141 | + remote="minizinc-bench/${{ inputs.job }}" |
| 142 | + id=$(ssh bench "cat '$remote/jobid'") |
| 143 | + # Refuse to report partial results as if they were a finished run. |
| 144 | + state=$(ssh bench "sacct -j $id -n -o State%20 | head -1 | tr -d ' '") |
| 145 | + echo "SLURM job $id is $state" |
| 146 | + [ "$state" = COMPLETED ] || { echo "job has not completed" >&2; exit 1; } |
| 147 | + scp -r "bench:$remote/results" ./results |
| 148 | + scp "bench:$remote/slurm-$id.out" ./results/ |
| 149 | +
|
| 150 | + - uses: actions/upload-artifact@v7 |
| 151 | + if: inputs.action == 'collect' |
| 152 | + with: |
| 153 | + name: bench-${{ inputs.job }} |
| 154 | + path: results/ |
0 commit comments