|
| 1 | +# Ceno GPU self-hosted CI runner (Docker + cron auto-restart) |
| 2 | + |
| 3 | +A GPU-capable, **ephemeral** GitHub Actions runner for this repo, packaged as a |
| 4 | +Docker image and kept alive by a cron watchdog. |
| 5 | + |
| 6 | +- **Ephemeral**: the runner exits cleanly after each job, so no state leaks |
| 7 | + between CI runs. The watchdog immediately brings up a fresh one. |
| 8 | +- **Self-healing tokens**: the container mints a fresh registration token from |
| 9 | + the GitHub API on every start, so restarts never fail on an expired token. |
| 10 | +- **Cron watchdog**: restarts the container if it stops *or* if the GPU becomes |
| 11 | + unreachable inside it (your stated failure mode). |
| 12 | + |
| 13 | +## One-time host setup |
| 14 | + |
| 15 | +The host must have an NVIDIA driver, Docker, and the **NVIDIA Container Toolkit** |
| 16 | +(this is what makes `--gpus all` work): |
| 17 | + |
| 18 | +```sh |
| 19 | +# NVIDIA Container Toolkit (Ubuntu) — see NVIDIA docs for the current repo lines |
| 20 | +curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \ |
| 21 | + | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg |
| 22 | +curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \ |
| 23 | + | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \ |
| 24 | + | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list |
| 25 | +sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit |
| 26 | +sudo nvidia-ctk runtime configure --runtime=docker |
| 27 | +sudo systemctl restart docker |
| 28 | + |
| 29 | +# sanity check: should print your GPU(s) |
| 30 | +docker run --rm --gpus all nvidia/cuda:12.6.2-base-ubuntu24.04 nvidia-smi |
| 31 | +``` |
| 32 | + |
| 33 | +Check the CUDA version your driver supports (top-right of `nvidia-smi`) and set |
| 34 | +`CUDA_VERSION` in the Dockerfile / build arg to match (must be ≤ that). |
| 35 | + |
| 36 | +## Private GPU backend: deploy key comes from GitHub secrets (not the image) |
| 37 | + |
| 38 | +All cargo *git* deps pinned in `Cargo.toml` are **public** (`ceno-gpu-mock`, |
| 39 | +`gkr-backend`, `ceno-patch`, `openvm`) and fetch over HTTPS with no auth. The |
| 40 | +real GPU backend, however, is the **private `scroll-tech/ceno-gpu`** repo: |
| 41 | +Cargo.toml's active `[patch]` redirects `ceno_gpu` away from the public |
| 42 | +`ceno-gpu-mock` placeholder to the **local path `../ceno-gpu/cuda_hal`**. So a |
| 43 | +`--features gpu` build needs `scroll-tech/ceno-gpu` cloned to `../ceno-gpu` |
| 44 | +(a sibling of the checkout). |
| 45 | + |
| 46 | +The runner **image carries no secrets**. The deploy key for `ceno-gpu` is stored |
| 47 | +as a repo secret and loaded into an ssh-agent *inside each job*, which then |
| 48 | +clones the repo to the path the patch expects. |
| 49 | + |
| 50 | +Setup: |
| 51 | +1. Add a **read-only deploy key** to the `scroll-tech/ceno-gpu` repo, and store |
| 52 | + the private half as a repo secret, e.g. `CENO_GPU_DEPLOY_KEY`. |
| 53 | +2. In any GPU workflow, load it and clone `ceno-gpu` before the build: |
| 54 | + |
| 55 | +```yaml |
| 56 | +jobs: |
| 57 | + gpu-job: |
| 58 | + runs-on: [ self-hosted, Linux, X64, gpu ] |
| 59 | + steps: |
| 60 | + - uses: webfactory/ssh-agent@v0.9.0 |
| 61 | + with: |
| 62 | + ssh-private-key: ${{ secrets.CENO_GPU_DEPLOY_KEY }} |
| 63 | + - uses: actions/checkout@v4 |
| 64 | + with: |
| 65 | + lfs: true |
| 66 | + # the active [patch] expects ../ceno-gpu/cuda_hal to exist |
| 67 | + - run: git clone git@github.com:scroll-tech/ceno-gpu.git ../ceno-gpu |
| 68 | + - run: cargo build --release --features gpu |
| 69 | +``` |
| 70 | +
|
| 71 | +(`webfactory/ssh-agent` also adds github.com to known_hosts for the job.) |
| 72 | + |
| 73 | +## Configure & first run |
| 74 | + |
| 75 | +```sh |
| 76 | +cp ci/gpu-runner/runner.env.example ci/gpu-runner/runner.env |
| 77 | +$EDITOR ci/gpu-runner/runner.env # paste PAT + repo URL |
| 78 | +
|
| 79 | +ci/gpu-runner/start-runner.sh |
| 80 | +docker logs -f ceno-gpu-runner # confirm it registered |
| 81 | +``` |
| 82 | + |
| 83 | +To build manually: |
| 84 | + |
| 85 | +```sh |
| 86 | +docker build -t ceno-gpu-runner:latest -f ci/gpu-runner/Dockerfile . |
| 87 | +``` |
| 88 | + |
| 89 | +In repo **Settings → Actions → Runners** you should now see a runner with the |
| 90 | +`gpu` label. Point GPU jobs at it: |
| 91 | + |
| 92 | +```yaml |
| 93 | +runs-on: [ self-hosted, Linux, X64, gpu ] |
| 94 | +``` |
| 95 | + |
| 96 | +(Non-GPU jobs keep using `[ self-hosted, Linux, X64 ]` and won't be scheduled |
| 97 | +here because they don't request the `gpu` label.) |
| 98 | + |
| 99 | +## Enable the cron watchdog |
| 100 | + |
| 101 | +```sh |
| 102 | +crontab -e |
| 103 | +# add (use the ABSOLUTE path): |
| 104 | +* * * * * /home/zbx/blockchain/ceno/ci/gpu-runner/watchdog.sh >> /var/log/ceno-gpu-runner.log 2>&1 |
| 105 | +``` |
| 106 | + |
| 107 | +The watchdog checks every minute and restarts on stop or GPU-unreachable. |
| 108 | + |
| 109 | +## Files |
| 110 | + |
| 111 | +| file | purpose | |
| 112 | +|-----------------------|----------------------------------------------------------------| |
| 113 | +| `Dockerfile` | CUDA-devel image + actions runner + Rust toolchain + git-lfs | |
| 114 | +| `entrypoint.sh` | mint token → configure ephemeral runner → run → de-register | |
| 115 | +| `start-runner.sh` | (re)launch the container with `--gpus all`; idempotent | |
| 116 | +| `watchdog.sh` | cron health check + restart | |
| 117 | +| `runner.env.example` | template for `runner.env` (PAT + repo URL; gitignored) | |
| 118 | + |
| 119 | +## Notes & gotchas |
| 120 | + |
| 121 | +- **PAT scope**: repo-level runner needs `repo` (classic) or fine-grained |
| 122 | + "Administration: Read and write" on this repo. |
| 123 | +- **Warm builds across ephemeral restarts**: two named volumes persist between |
| 124 | + containers — `ceno-gpu-runner-cargo` (the cargo registry, so deps aren't |
| 125 | + re-downloaded) and `ceno-gpu-runner-target` (mounted at `/cache/target`, with |
| 126 | + `CARGO_TARGET_DIR` pointed at it so incremental compile artifacts survive). |
| 127 | + This keeps recompiles fast even though each job runs in a fresh container. |
| 128 | + Because of this, **GPU jobs can drop the `actions/cache` step for `target/`** — |
| 129 | + the host volume is bigger (no ~10 GB cache limit) and faster (no network |
| 130 | + restore) than GitHub's cache backend for a workspace this size. The single |
| 131 | + ephemeral runner serves one job at a time, so there's no concurrent writer on |
| 132 | + the shared target dir. (To reset: `docker volume rm ceno-gpu-runner-target`.) |
| 133 | +- **Alternative to cron**: `--restart unless-stopped` in `start-runner.sh` |
| 134 | + restarts on crash instantly, but won't catch a hung container or a GPU that |
| 135 | + silently went away — that's why the watchdog also probes `nvidia-smi`. |
| 136 | +- **Concurrency**: this is a single ephemeral runner = one job at a time. For N |
| 137 | + concurrent GPU jobs, run N containers (`CONTAINER_NAME=ceno-gpu-runner-2 …`) |
| 138 | + and one watchdog line each. |
0 commit comments