chore(ci): build club vllm image
This commit is contained in:
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
||||
.git
|
||||
.github
|
||||
|
||||
models-cache
|
||||
results
|
||||
|
||||
**/cache
|
||||
**/.cache
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
|
||||
*.safetensors
|
||||
*.gguf
|
||||
*.bin
|
||||
*.pt
|
||||
*.pth
|
||||
|
||||
.env
|
||||
.env.*
|
||||
397
.github/workflows/build-vllm-image.yml
vendored
Normal file
397
.github/workflows/build-vllm-image.yml
vendored
Normal file
@@ -0,0 +1,397 @@
|
||||
name: Build vLLM Club3090 Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
upstream_image:
|
||||
description: "Upstream vLLM image to vendor overlays into"
|
||||
required: false
|
||||
default: "vllm/vllm-openai:nightly-1acd67a795ebccdf9b9db7697ae9082058301657"
|
||||
smoke:
|
||||
description: "GPU smoke behavior"
|
||||
required: false
|
||||
default: "auto"
|
||||
type: choice
|
||||
options:
|
||||
- auto
|
||||
- skip
|
||||
- required
|
||||
schedule:
|
||||
- cron: "0 0 * * 0"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v0.7.*"
|
||||
- "v0.[8-9].*"
|
||||
- "v[1-9]*"
|
||||
paths:
|
||||
- ".github/workflows/build-vllm-image.yml"
|
||||
- "docker/vllm-club3090/**"
|
||||
- "models/qwen3.6-27b/vllm/patches/**"
|
||||
- "models/gemma-4-31b/vllm/patches/**"
|
||||
|
||||
concurrency:
|
||||
group: build-vllm-club3090-image
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ghcr.io/noonghunna/vllm-club3090
|
||||
DEFAULT_VLLM_BASE_IMAGE: vllm/vllm-openai:nightly-1acd67a795ebccdf9b9db7697ae9082058301657
|
||||
CANONICAL_COMPOSE: models/qwen3.6-27b/vllm/compose/dual/docker-compose.yml
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and push dated image
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image_ref: ${{ steps.meta.outputs.image_ref }}
|
||||
image_tag: ${{ steps.meta.outputs.image_tag }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
upstream_image: ${{ steps.meta.outputs.upstream_image }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Compute image metadata
|
||||
id: meta
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_UPSTREAM_IMAGE: ${{ inputs.upstream_image }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
upstream_image="${INPUT_UPSTREAM_IMAGE:-${DEFAULT_VLLM_BASE_IMAGE}}"
|
||||
date_tag="$(date -u +%Y%m%d)"
|
||||
counter="$(printf "%04d" "${GITHUB_RUN_NUMBER}")"
|
||||
image_tag="nightly-${date_tag}-club${counter}"
|
||||
image_ref="${IMAGE_NAME}:${image_tag}"
|
||||
|
||||
{
|
||||
echo "upstream_image=${upstream_image}"
|
||||
echo "image_tag=${image_tag}"
|
||||
echo "image_ref=${image_ref}"
|
||||
echo "tags<<EOF"
|
||||
echo "${image_ref}"
|
||||
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
|
||||
echo "${IMAGE_NAME}:club-${GITHUB_REF_NAME}"
|
||||
fi
|
||||
echo "EOF"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push dated image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/vllm-club3090/Dockerfile
|
||||
platforms: linux/amd64
|
||||
pull: true
|
||||
push: true
|
||||
build-args: |
|
||||
VLLM_BASE_IMAGE=${{ steps.meta.outputs.upstream_image }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.version=${{ steps.meta.outputs.image_tag }}
|
||||
club3090.upstream_vllm_image=${{ steps.meta.outputs.upstream_image }}
|
||||
|
||||
detect-smoke-runner:
|
||||
name: Detect self-hosted GPU runner
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
available: ${{ steps.detect.outputs.available }}
|
||||
smoke_mode: ${{ steps.detect.outputs.smoke_mode }}
|
||||
steps:
|
||||
- name: Detect online gpu-labeled runner
|
||||
id: detect
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
SMOKE_MODE: ${{ inputs.smoke || 'auto' }}
|
||||
with:
|
||||
script: |
|
||||
const mode = process.env.SMOKE_MODE || "auto";
|
||||
core.setOutput("smoke_mode", mode);
|
||||
|
||||
if (mode === "skip") {
|
||||
core.notice("Smoke explicitly skipped. Dated image was pushed; aliases will not move.");
|
||||
core.setOutput("available", "false");
|
||||
return;
|
||||
}
|
||||
|
||||
const runners = await github.paginate(
|
||||
github.rest.actions.listSelfHostedRunnersForRepo,
|
||||
{
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100,
|
||||
},
|
||||
);
|
||||
|
||||
const available = runners.some((runner) => {
|
||||
const labels = runner.labels.map((label) => label.name.toLowerCase());
|
||||
return runner.status === "online" &&
|
||||
labels.includes("self-hosted") &&
|
||||
labels.includes("gpu");
|
||||
});
|
||||
|
||||
core.setOutput("available", available ? "true" : "false");
|
||||
if (!available) {
|
||||
const message = "No online self-hosted runner with label 'gpu' was found. Dated image was pushed; latest/nightly-stable were not moved.";
|
||||
if (mode === "required") {
|
||||
core.setFailed(message);
|
||||
} else {
|
||||
core.notice(message);
|
||||
}
|
||||
}
|
||||
|
||||
smoke:
|
||||
name: GPU smoke and alias promotion
|
||||
needs:
|
||||
- build
|
||||
- detect-smoke-runner
|
||||
if: needs.detect-smoke-runner.outputs.available == 'true'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- linux
|
||||
- x64
|
||||
- gpu
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
IMAGE_REF: ${{ needs.build.outputs.image_ref }}
|
||||
IMAGE_NAME: ghcr.io/noonghunna/vllm-club3090
|
||||
COMPOSE_PROJECT_NAME: club3090-ci-vllm-dual
|
||||
COMPOSE_OVERRIDE: /tmp/club3090-ci-vllm-image.override.yml
|
||||
URL: http://localhost:8010
|
||||
MODEL: qwen3.6-27b-autoround
|
||||
CONTAINER: vllm-qwen36-27b-dual
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Prepare canonical compose override
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker pull "${IMAGE_REF}"
|
||||
cat > "${COMPOSE_OVERRIDE}" <<EOF
|
||||
services:
|
||||
vllm-qwen36-27b-dual:
|
||||
image: ${IMAGE_REF}
|
||||
EOF
|
||||
|
||||
- name: Stop prior club-3090 estate if present
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -f "${HOME}/.club3090/estate.yml" ]]; then
|
||||
bash scripts/launch.sh --down-estate "${HOME}/.club3090/estate.yml" || true
|
||||
fi
|
||||
docker compose \
|
||||
-f "${CANONICAL_COMPOSE}" \
|
||||
-f "${COMPOSE_OVERRIDE}" \
|
||||
-p "${COMPOSE_PROJECT_NAME}" \
|
||||
down --remove-orphans || true
|
||||
|
||||
- name: Boot canonical dual vLLM compose
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker compose \
|
||||
-f "${CANONICAL_COMPOSE}" \
|
||||
-f "${COMPOSE_OVERRIDE}" \
|
||||
-p "${COMPOSE_PROJECT_NAME}" \
|
||||
up -d
|
||||
|
||||
- name: Wait for OpenAI endpoint
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for _ in {1..120}; do
|
||||
if curl -sf -m 5 "${URL}/v1/models" >/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
docker compose \
|
||||
-f "${CANONICAL_COMPOSE}" \
|
||||
-f "${COMPOSE_OVERRIDE}" \
|
||||
-p "${COMPOSE_PROJECT_NAME}" \
|
||||
logs --tail=200
|
||||
exit 1
|
||||
|
||||
- name: Run verify-full
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
URL="${URL}" MODEL="${MODEL}" CONTAINER="${CONTAINER}" bash scripts/verify-full.sh
|
||||
|
||||
- name: Run 3-prompt smoke bench
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
url = "http://localhost:8010"
|
||||
model = "qwen3.6-27b-autoround"
|
||||
prompts = [
|
||||
("narrative", "Write a concise paragraph explaining transformer attention.", 96),
|
||||
("code", "Write a small Python function that returns the nth Fibonacci number.", 96),
|
||||
("reasoning", "A train leaves at 08:00 traveling 60 km/h. Another leaves at 09:00 traveling 90 km/h. When does the second catch up?", 96),
|
||||
]
|
||||
|
||||
for label, prompt, max_tokens in prompts:
|
||||
body = json.dumps({
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": 0.3,
|
||||
"stream": False,
|
||||
"chat_template_kwargs": {"enable_thinking": False},
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{url}/v1/chat/completions",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
start = time.time()
|
||||
with urllib.request.urlopen(req, timeout=300) as response:
|
||||
data = json.load(response)
|
||||
wall = time.time() - start
|
||||
usage = data.get("usage") or {}
|
||||
tokens = usage.get("completion_tokens") or 0
|
||||
text = data["choices"][0]["message"].get("content") or ""
|
||||
if not text.strip():
|
||||
raise SystemExit(f"{label}: empty completion")
|
||||
tps = tokens / wall if wall > 0 else 0
|
||||
print(f"{label}: wall={wall:.2f}s completion_tokens={tokens} wall_TPS={tps:.2f}")
|
||||
PY
|
||||
|
||||
- name: Promote latest aliases
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker tag "${IMAGE_REF}" "${IMAGE_NAME}:latest"
|
||||
docker tag "${IMAGE_REF}" "${IMAGE_NAME}:nightly-stable"
|
||||
docker push "${IMAGE_NAME}:latest"
|
||||
docker push "${IMAGE_NAME}:nightly-stable"
|
||||
|
||||
- name: Cleanup canonical compose
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
docker compose \
|
||||
-f "${CANONICAL_COMPOSE}" \
|
||||
-f "${COMPOSE_OVERRIDE}" \
|
||||
-p "${COMPOSE_PROJECT_NAME}" \
|
||||
down --remove-orphans || true
|
||||
|
||||
retention:
|
||||
name: Retain four weeks of dated nightlies
|
||||
needs: build
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete old dated nightly package versions
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const packageType = "container";
|
||||
const packageName = "vllm-club3090";
|
||||
const cutoff = Date.now() - 28 * 24 * 60 * 60 * 1000;
|
||||
|
||||
async function listVersions(scope) {
|
||||
if (scope === "org") {
|
||||
return github.paginate("GET /orgs/{org}/packages/{package_type}/{package_name}/versions", {
|
||||
org: context.repo.owner,
|
||||
package_type: packageType,
|
||||
package_name: packageName,
|
||||
per_page: 100,
|
||||
});
|
||||
}
|
||||
return github.paginate("GET /users/{username}/packages/{package_type}/{package_name}/versions", {
|
||||
username: context.repo.owner,
|
||||
package_type: packageType,
|
||||
package_name: packageName,
|
||||
per_page: 100,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteVersion(scope, id) {
|
||||
if (scope === "org") {
|
||||
return github.request("DELETE /orgs/{org}/packages/{package_type}/{package_name}/versions/{package_version_id}", {
|
||||
org: context.repo.owner,
|
||||
package_type: packageType,
|
||||
package_name: packageName,
|
||||
package_version_id: id,
|
||||
});
|
||||
}
|
||||
return github.request("DELETE /users/{username}/packages/{package_type}/{package_name}/versions/{package_version_id}", {
|
||||
username: context.repo.owner,
|
||||
package_type: packageType,
|
||||
package_name: packageName,
|
||||
package_version_id: id,
|
||||
});
|
||||
}
|
||||
|
||||
let scope = "org";
|
||||
let versions = [];
|
||||
try {
|
||||
versions = await listVersions(scope);
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
scope = "user";
|
||||
try {
|
||||
versions = await listVersions(scope);
|
||||
} catch (userError) {
|
||||
if (userError.status === 404) {
|
||||
core.notice(`Package ${packageName} does not exist yet; retention skipped.`);
|
||||
return;
|
||||
}
|
||||
throw userError;
|
||||
}
|
||||
}
|
||||
|
||||
for (const version of versions) {
|
||||
const tags = version.metadata?.container?.tags || [];
|
||||
if (tags.length === 0) continue;
|
||||
|
||||
const protectedTag = tags.some((tag) =>
|
||||
tag === "latest" ||
|
||||
tag === "nightly-stable" ||
|
||||
tag.startsWith("club-v")
|
||||
);
|
||||
const datedNightly = tags.some((tag) => /^nightly-\d{8}-club\d{4,}$/.test(tag));
|
||||
const updatedAt = new Date(version.updated_at).getTime();
|
||||
|
||||
if (datedNightly && !protectedTag && updatedAt < cutoff) {
|
||||
core.notice(`Deleting old ${packageName} package version ${version.id}: ${tags.join(", ")}`);
|
||||
await deleteVersion(scope, version.id);
|
||||
}
|
||||
}
|
||||
46
docker/vllm-club3090/Dockerfile
Normal file
46
docker/vllm-club3090/Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
ARG VLLM_BASE_IMAGE=vllm/vllm-openai:nightly-1acd67a795ebccdf9b9db7697ae9082058301657
|
||||
FROM ${VLLM_BASE_IMAGE}
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
LABEL org.opencontainers.image.title="club-3090 vLLM"
|
||||
LABEL org.opencontainers.image.description="vLLM nightly with club-3090 vendored overlays baked into site-packages"
|
||||
LABEL org.opencontainers.image.source="https://github.com/noonghunna/club-3090"
|
||||
LABEL org.opencontainers.image.vendor="club-3090"
|
||||
|
||||
COPY models/qwen3.6-27b/vllm/patches/vllm-marlin-pad/marlin.py /tmp/club3090-overlays/qwen-marlin-pad/model_executor/kernels/linear/mixed_precision/marlin.py
|
||||
COPY models/qwen3.6-27b/vllm/patches/vllm-marlin-pad/MPLinearKernel.py /tmp/club3090-overlays/qwen-marlin-pad/model_executor/kernels/linear/mixed_precision/MPLinearKernel.py
|
||||
COPY models/qwen3.6-27b/vllm/patches/vllm-pr35936-required-fallback/vllm/ /tmp/club3090-overlays/qwen-pr35936/
|
||||
COPY models/gemma-4-31b/vllm/patches/vllm-gemma4-dflash-int8/ /tmp/club3090-overlays/gemma-dflash-int8/
|
||||
|
||||
RUN set -eux; \
|
||||
VLLM_SITE="$(python3 -c 'import importlib.util, pathlib; spec = importlib.util.find_spec("vllm"); print(pathlib.Path(spec.submodule_search_locations[0]) if spec and spec.submodule_search_locations else "")')"; \
|
||||
test -n "${VLLM_SITE}"; \
|
||||
test -d "${VLLM_SITE}"; \
|
||||
echo "Applying club-3090 overlays into ${VLLM_SITE}"; \
|
||||
find /tmp/club3090-overlays/qwen-marlin-pad -type f -name '*.py' -print0 \
|
||||
| while IFS= read -r -d '' src; do \
|
||||
rel="${src#/tmp/club3090-overlays/qwen-marlin-pad/}"; \
|
||||
install -D -m 0644 "$src" "${VLLM_SITE}/${rel}"; \
|
||||
done; \
|
||||
find /tmp/club3090-overlays/qwen-pr35936 -type f -name '*.py' -print0 \
|
||||
| while IFS= read -r -d '' src; do \
|
||||
rel="${src#/tmp/club3090-overlays/qwen-pr35936/}"; \
|
||||
install -D -m 0644 "$src" "${VLLM_SITE}/${rel}"; \
|
||||
done; \
|
||||
find /tmp/club3090-overlays/gemma-dflash-int8 -type f -name '*.py' ! -path '*/_pre-pr42102-historical/*' -print0 \
|
||||
| while IFS= read -r -d '' src; do \
|
||||
rel="${src#/tmp/club3090-overlays/gemma-dflash-int8/}"; \
|
||||
install -D -m 0644 "$src" "${VLLM_SITE}/${rel}"; \
|
||||
done; \
|
||||
mkdir -p /etc/club3090; \
|
||||
{ \
|
||||
echo "vllm base image: ${VLLM_BASE_IMAGE}"; \
|
||||
echo "vendored overlays:"; \
|
||||
echo " - vLLM PR #40361: Marlin pad-sub-tile-n for AutoRound INT4 TP shards"; \
|
||||
echo " - vLLM PR #35936: Qwen3-Coder required-tool fallback serving patch"; \
|
||||
echo " - vLLM PR #41703: Gemma 4 DFlash drafter"; \
|
||||
echo " - vLLM PR #42102: DFlash + INT8 per-token-head KV coexistence"; \
|
||||
echo "source repo: https://github.com/noonghunna/club-3090"; \
|
||||
} > /etc/club3090/vllm-overlays.txt; \
|
||||
rm -rf /tmp/club3090-overlays
|
||||
112
docs/CI_RUNNER_SETUP.md
Normal file
112
docs/CI_RUNNER_SETUP.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Club-3090 CI GPU Runner Setup
|
||||
|
||||
The vLLM image workflow builds on GitHub-hosted Ubuntu, then optionally smokes the
|
||||
fresh image on a self-hosted GPU runner. If no runner is registered, the workflow
|
||||
still pushes the dated `nightly-YYYYMMDD-clubXXXX` image and leaves `latest` /
|
||||
`nightly-stable` untouched.
|
||||
|
||||
## Runner Requirements
|
||||
|
||||
- Linux x86_64 host with Docker Engine and Docker Compose v2.
|
||||
- NVIDIA driver and NVIDIA Container Toolkit installed.
|
||||
- At least two 24 GB NVIDIA GPUs for the canonical `qwen3.6-27b/vllm/dual`
|
||||
smoke. The production validation target is 2x RTX 3090.
|
||||
- Enough local storage for the vLLM image, model cache, Docker layers, and
|
||||
compile caches. Plan for at least 250 GB free.
|
||||
- A dedicated runner host. Do not run untrusted pull-request jobs on this
|
||||
machine.
|
||||
|
||||
## Labels
|
||||
|
||||
Register the runner with the normal self-hosted labels plus `gpu`:
|
||||
|
||||
```text
|
||||
self-hosted
|
||||
linux
|
||||
x64
|
||||
gpu
|
||||
```
|
||||
|
||||
The workflow checks for an online runner with `self-hosted` and `gpu`; the smoke
|
||||
job itself targets `[self-hosted, linux, x64, gpu]`.
|
||||
|
||||
## Registration
|
||||
|
||||
1. Open the GitHub repository.
|
||||
2. Go to **Settings -> Actions -> Runners -> New self-hosted runner**.
|
||||
3. Choose Linux x64 and follow GitHub's generated commands.
|
||||
4. Add the `gpu` label during configuration, or add it later from the runner UI.
|
||||
5. Install the runner as a service:
|
||||
|
||||
```bash
|
||||
sudo ./svc.sh install
|
||||
sudo ./svc.sh start
|
||||
```
|
||||
|
||||
The runner user must be able to run Docker commands. On a typical Ubuntu host:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker "$USER"
|
||||
newgrp docker
|
||||
```
|
||||
|
||||
Restart the runner service after changing group membership.
|
||||
|
||||
## Host Preflight
|
||||
|
||||
Run these on the runner host before enabling the smoke job:
|
||||
|
||||
```bash
|
||||
nvidia-smi
|
||||
docker compose version
|
||||
docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi
|
||||
```
|
||||
|
||||
Then clone this repository at the path used by the runner workspace once and
|
||||
make sure the model cache is present or mounted at the compose default:
|
||||
|
||||
```bash
|
||||
ls -ld models-cache
|
||||
```
|
||||
|
||||
If your cache lives elsewhere, set `MODEL_DIR` in the runner service
|
||||
environment. The canonical compose reads `${MODEL_DIR:-../../../../../models-cache}`.
|
||||
|
||||
## What The Smoke Job Does
|
||||
|
||||
On a green build, the workflow:
|
||||
|
||||
1. Pulls `ghcr.io/noonghunna/vllm-club3090:nightly-YYYYMMDD-clubXXXX`.
|
||||
2. Boots `models/qwen3.6-27b/vllm/compose/dual/docker-compose.yml` with a
|
||||
temporary compose override that points at the dated image.
|
||||
3. Waits for `http://localhost:8010/v1/models`.
|
||||
4. Runs `bash scripts/verify-full.sh`.
|
||||
5. Runs a three-prompt OpenAI-compatible smoke bench.
|
||||
6. Only then retags the image as `latest` and `nightly-stable`.
|
||||
|
||||
If any smoke step fails, the dated image remains available for debugging and the
|
||||
rolling aliases do not move.
|
||||
|
||||
## Existing Containers
|
||||
|
||||
The runner should be dedicated to CI. Before booting the canonical compose, the
|
||||
workflow tears down the default club-3090 estate if `~/.club3090/estate.yml`
|
||||
exists, then runs `docker compose down` for the CI project name. Avoid running
|
||||
manual workloads on the same host while the workflow is active.
|
||||
|
||||
## Registry Permissions
|
||||
|
||||
The workflow uses `GITHUB_TOKEN` with `packages: write` to push GHCR images and
|
||||
move aliases. No personal access token is required for the repository-owned
|
||||
package.
|
||||
|
||||
## Retention
|
||||
|
||||
The scheduled workflow keeps:
|
||||
|
||||
- `latest`
|
||||
- `nightly-stable`
|
||||
- every `club-v*` release tag
|
||||
- dated `nightly-YYYYMMDD-clubXXXX` tags from the last four weeks
|
||||
|
||||
Older dated nightly package versions are deleted by the retention job.
|
||||
Reference in New Issue
Block a user