77 description : " Upstream vLLM image to vendor overlays into"
88 required : false
99 default : " vllm/vllm-openai:nightly-1acd67a795ebccdf9b9db7697ae9082058301657"
10- smoke :
11- description : " GPU smoke behavior"
12- required : false
13- default : " auto"
14- type : choice
15- options :
16- - auto
17- - skip
18- - required
1910 schedule :
2011 - cron : " 0 0 * * 0"
2112 push :
@@ -36,14 +27,12 @@ concurrency:
3627 cancel-in-progress : false
3728
3829permissions :
39- actions : read
4030 contents : read
4131 packages : write
4232
4333env :
4434 IMAGE_NAME : ghcr.io/noonghunna/vllm-club3090
4535 DEFAULT_VLLM_BASE_IMAGE : vllm/vllm-openai:nightly-1acd67a795ebccdf9b9db7697ae9082058301657
46- CANONICAL_COMPOSE : models/qwen3.6-27b/vllm/compose/dual/docker-compose.yml
4736
4837jobs :
4938 build :
@@ -111,206 +100,28 @@ jobs:
111100 org.opencontainers.image.version=${{ steps.meta.outputs.image_tag }}
112101 club3090.upstream_vllm_image=${{ steps.meta.outputs.upstream_image }}
113102
114- detect-smoke-runner :
115- name : Detect self-hosted GPU runner
103+ promote-aliases :
104+ name : Promote latest and nightly-stable
116105 needs : build
117106 runs-on : ubuntu-latest
118- outputs :
119- available : ${{ steps.detect.outputs.available }}
120- smoke_mode : ${{ steps.detect.outputs.smoke_mode }}
121107 steps :
122- - name : Detect online gpu-labeled runner
123- id : detect
124- uses : actions/github-script@v7
125- env :
126- SMOKE_MODE : ${{ inputs.smoke || 'auto' }}
127- with :
128- script : |
129- const mode = process.env.SMOKE_MODE || "auto";
130- core.setOutput("smoke_mode", mode);
131-
132- if (mode === "skip") {
133- core.notice("Smoke explicitly skipped. Dated image was pushed; aliases will not move.");
134- core.setOutput("available", "false");
135- return;
136- }
137-
138- const runners = await github.paginate(
139- github.rest.actions.listSelfHostedRunnersForRepo,
140- {
141- owner: context.repo.owner,
142- repo: context.repo.repo,
143- per_page: 100,
144- },
145- );
146-
147- const available = runners.some((runner) => {
148- const labels = runner.labels.map((label) => label.name.toLowerCase());
149- return runner.status === "online" &&
150- labels.includes("self-hosted") &&
151- labels.includes("gpu");
152- });
153-
154- core.setOutput("available", available ? "true" : "false");
155- if (!available) {
156- const message = "No online self-hosted runner with label 'gpu' was found. Dated image was pushed; latest/nightly-stable were not moved.";
157- if (mode === "required") {
158- core.setFailed(message);
159- } else {
160- core.notice(message);
161- }
162- }
163-
164- smoke :
165- name : GPU smoke and alias promotion
166- needs :
167- - build
168- - detect-smoke-runner
169- if : needs.detect-smoke-runner.outputs.available == 'true'
170- runs-on :
171- - self-hosted
172- - linux
173- - x64
174- - gpu
175- timeout-minutes : 120
176- env :
177- IMAGE_REF : ${{ needs.build.outputs.image_ref }}
178- IMAGE_NAME : ghcr.io/noonghunna/vllm-club3090
179- COMPOSE_PROJECT_NAME : club3090-ci-vllm-dual
180- COMPOSE_OVERRIDE : /tmp/club3090-ci-vllm-image.override.yml
181- URL : http://localhost:8010
182- MODEL : qwen3.6-27b-autoround
183- CONTAINER : vllm-qwen36-27b-dual
184- steps :
185- - name : Checkout
186- uses : actions/checkout@v4
187-
188108 - name : Log in to GHCR
189109 uses : docker/login-action@v3
190110 with :
191111 registry : ghcr.io
192112 username : ${{ github.actor }}
193113 password : ${{ secrets.GITHUB_TOKEN }}
194114
195- - name : Prepare canonical compose override
196- shell : bash
197- run : |
198- set -euo pipefail
199- docker pull "${IMAGE_REF}"
200- cat > "${COMPOSE_OVERRIDE}" <<EOF
201- services:
202- vllm-qwen36-27b-dual:
203- image: ${IMAGE_REF}
204- EOF
205-
206- - name : Stop prior club-3090 estate if present
207- shell : bash
208- run : |
209- set -euo pipefail
210- if [[ -f "${HOME}/.club3090/estate.yml" ]]; then
211- bash scripts/launch.sh --down-estate "${HOME}/.club3090/estate.yml" || true
212- fi
213- docker compose \
214- -f "${CANONICAL_COMPOSE}" \
215- -f "${COMPOSE_OVERRIDE}" \
216- -p "${COMPOSE_PROJECT_NAME}" \
217- down --remove-orphans || true
218-
219- - name : Boot canonical dual vLLM compose
220- shell : bash
221- run : |
222- set -euo pipefail
223- docker compose \
224- -f "${CANONICAL_COMPOSE}" \
225- -f "${COMPOSE_OVERRIDE}" \
226- -p "${COMPOSE_PROJECT_NAME}" \
227- up -d
228-
229- - name : Wait for OpenAI endpoint
230- shell : bash
231- run : |
232- set -euo pipefail
233- for _ in {1..120}; do
234- if curl -sf -m 5 "${URL}/v1/models" >/dev/null; then
235- exit 0
236- fi
237- sleep 10
238- done
239- docker compose \
240- -f "${CANONICAL_COMPOSE}" \
241- -f "${COMPOSE_OVERRIDE}" \
242- -p "${COMPOSE_PROJECT_NAME}" \
243- logs --tail=200
244- exit 1
245-
246- - name : Run verify-full
247- shell : bash
248- run : |
249- set -euo pipefail
250- URL="${URL}" MODEL="${MODEL}" CONTAINER="${CONTAINER}" bash scripts/verify-full.sh
251-
252- - name : Run 3-prompt smoke bench
253- shell : bash
254- run : |
255- set -euo pipefail
256- python3 - <<'PY'
257- import json
258- import time
259- import urllib.request
260-
261- url = "http://localhost:8010"
262- model = "qwen3.6-27b-autoround"
263- prompts = [
264- ("narrative", "Write a concise paragraph explaining transformer attention.", 96),
265- ("code", "Write a small Python function that returns the nth Fibonacci number.", 96),
266- ("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),
267- ]
268-
269- for label, prompt, max_tokens in prompts:
270- body = json.dumps({
271- "model": model,
272- "messages": [{"role": "user", "content": prompt}],
273- "max_tokens": max_tokens,
274- "temperature": 0.3,
275- "stream": False,
276- "chat_template_kwargs": {"enable_thinking": False},
277- }).encode()
278- req = urllib.request.Request(
279- f"{url}/v1/chat/completions",
280- data=body,
281- headers={"Content-Type": "application/json"},
282- )
283- start = time.time()
284- with urllib.request.urlopen(req, timeout=300) as response:
285- data = json.load(response)
286- wall = time.time() - start
287- usage = data.get("usage") or {}
288- tokens = usage.get("completion_tokens") or 0
289- text = data["choices"][0]["message"].get("content") or ""
290- if not text.strip():
291- raise SystemExit(f"{label}: empty completion")
292- tps = tokens / wall if wall > 0 else 0
293- print(f"{label}: wall={wall:.2f}s completion_tokens={tokens} wall_TPS={tps:.2f}")
294- PY
295-
296- - name : Promote latest aliases
115+ - name : Promote :latest and :nightly-stable to dated tag
297116 shell : bash
117+ env :
118+ IMAGE_REF : ${{ needs.build.outputs.image_ref }}
298119 run : |
299120 set -euo pipefail
300- docker tag "${IMAGE_REF}" "${IMAGE_NAME}:latest"
301- docker tag "${IMAGE_REF}" "${IMAGE_NAME}:nightly-stable"
302- docker push "${IMAGE_NAME}:latest"
303- docker push "${IMAGE_NAME}:nightly-stable"
304-
305- - name : Cleanup canonical compose
306- if : always()
307- shell : bash
308- run : |
309- docker compose \
310- -f "${CANONICAL_COMPOSE}" \
311- -f "${COMPOSE_OVERRIDE}" \
312- -p "${COMPOSE_PROJECT_NAME}" \
313- down --remove-orphans || true
121+ docker buildx imagetools create \
122+ -t "${IMAGE_NAME}:latest" \
123+ -t "${IMAGE_NAME}:nightly-stable" \
124+ "${IMAGE_REF}"
314125
315126 retention :
316127 name : Retain four weeks of dated nightlies
0 commit comments