Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ updates:
schedule:
interval: "weekly"

- package-ecosystem: "docker"
directory: "/vllm"
schedule:
interval: "weekly"
labels:
- docker
- dependencies
commit-message:
prefix: "chore(vllm)"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
Expand Down
151 changes: 151 additions & 0 deletions .github/workflows/vllm-gpu-container.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: vLLM GPU Container

on:
push:
branches: [main]
paths:
- "vllm/Containerfile"
- ".github/workflows/vllm-gpu-container.yaml"
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
paths:
- "vllm/Containerfile"
- ".github/workflows/vllm-gpu-container.yaml"
merge_group:
branches: [main]
workflow_dispatch:
inputs:
inference_model:
description: "Inference model (HuggingFace ID) — default is Qwen/Qwen3-0.6B"
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

permissions: {}

env:
REGISTRY: ghcr.io
IMAGE_NAME: ghcr.io/praxis-proxy/vllm-gpu
INFERENCE_MODEL: ${{ inputs.inference_model || 'Qwen/Qwen3-0.6B' }}

jobs:
changes:
if: github.event_name == 'merge_group'
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
relevant: ${{ steps.filter.outputs.relevant }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Detect relevant path changes
id: filter
run: |
CHANGED=$(git diff --name-only "${{ github.event.merge_group.base_sha }}" \
"${{ github.event.merge_group.head_sha }}" -- \
'vllm/Containerfile' \
'.github/workflows/vllm-gpu-container.yaml')
if [ -n "$CHANGED" ]; then
echo "relevant=true" >> "$GITHUB_OUTPUT"
else
echo "relevant=false" >> "$GITHUB_OUTPUT"
fi

build-test:
needs: [changes]
if: |
always() &&
(needs.changes.result == 'skipped' || needs.changes.outputs.relevant == 'true')
runs-on: TODO-GPU-RUNNER-LABEL
timeout-minutes: 45
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set image tag
run: echo "IMAGE_TAG=${INFERENCE_MODEL#*/}" >> "$GITHUB_ENV"

- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL 2>/dev/null || true
docker system prune -af

- name: Build image
env:
DOCKER_BUILDKIT: "1"
HF_TOKEN: ${{ secrets.HF_TOKEN }}
run: |
BUILD_ARGS=(
--build-arg "INFERENCE_MODEL=${INFERENCE_MODEL}"
--tag "${IMAGE_NAME}:${IMAGE_TAG}"
--file vllm/Containerfile
)
if [ -n "${HF_TOKEN}" ]; then
BUILD_ARGS+=(--secret "id=hf_token,env=HF_TOKEN")
fi
docker build "${BUILD_ARGS[@]}" .

- name: Start vLLM
run: |
docker run -d --name vllm-inference \
--gpus all \
-p 8000:8000 \
"${IMAGE_NAME}:${IMAGE_TAG}" \
--model "/opt/vllm/models/${INFERENCE_MODEL}" \
--max-model-len 4096 \
--served-model-name "${INFERENCE_MODEL#*/}" \
--enable-auto-tool-choice \
--tool-call-parser hermes

- name: Wait for vLLM readiness
run: |
echo "Waiting for vLLM health endpoint..."
timeout 300 bash -c 'until curl -sf http://127.0.0.1:8000/health > /dev/null 2>&1; do
sleep 5
done'
echo "vLLM is ready"

- name: Smoke test inference
run: |
response=$(curl -sf http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "'"${INFERENCE_MODEL#*/}"'",
"messages": [{"role": "user", "content": "Say hello in one word."}],
"max_tokens": 16
}')
echo "$response" | jq .
echo "$response" | jq -e '.choices[0].message.content' > /dev/null

- name: vLLM logs
if: failure()
run: docker logs vllm-inference

- name: Stop vLLM
if: always()
run: docker rm -f vllm-inference || true

- name: Log in to GHCR
if: contains(fromJSON('["push", "workflow_dispatch"]'), github.event_name)
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Push image
if: contains(fromJSON('["push", "workflow_dispatch"]'), github.event_name)
run: |
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
if [ "${{ github.event_name }}" = "push" ]; then
docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:latest"
docker push "${IMAGE_NAME}:latest"
fi
35 changes: 35 additions & 0 deletions vllm/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# syntax=docker/dockerfile:1

# GPU-accelerated vLLM image with a model baked in at build time.
#
# Reference: https://hub.docker.com/r/vllm/vllm-openai

FROM vllm/vllm-openai:v0.26.0

RUN pip install "huggingface-hub[cli]"

ARG INFERENCE_MODEL=""

ENV INFERENCE_MODEL="${INFERENCE_MODEL}"
ENV MODEL_CACHE_DIR="/opt/vllm/models"

RUN if [ -z "${INFERENCE_MODEL}" ]; then \
echo "ERROR: INFERENCE_MODEL build argument is required" >&2 && exit 1; \
fi

RUN --mount=type=secret,id=hf_token \
set -e && \
model_path="${MODEL_CACHE_DIR}/${INFERENCE_MODEL}" && \
mkdir -p "${model_path}" && \
if [ -f /run/secrets/hf_token ]; then \
HF_TOKEN=$(cat /run/secrets/hf_token) && \
hf download "${INFERENCE_MODEL}" --local-dir "${model_path}" --token "${HF_TOKEN}"; \
else \
hf download "${INFERENCE_MODEL}" --local-dir "${model_path}"; \
fi && \
rm -rf "${MODEL_CACHE_DIR}/.huggingface" "${model_path}/original"

RUN useradd -r -u 1001 -m vllm
USER 1001

ENTRYPOINT ["vllm", "serve"]
85 changes: 85 additions & 0 deletions vllm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# vLLM GPU Container

Pre-built [vLLM](https://docs.vllm.ai/) GPU container image with an
inference model baked in at build time, eliminating runtime download
latency.

Published to `ghcr.io/praxis-proxy/vllm-gpu`.

## Requirements

- Docker with [BuildKit](https://docs.docker.com/build/buildkit/) enabled
(default in Docker 23+); required for the `# syntax=` directive and
`--secret` mounts used below.
- [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
on the host, so `docker run --gpus all` can expose the GPU to the
container.

## Building

```console
docker build \
--build-arg INFERENCE_MODEL=Qwen/Qwen3-0.6B \
--tag vllm-gpu:Qwen3-0.6B \
--file vllm/Containerfile \
.
```

For gated models that require a HuggingFace token:

```console
docker build \
--build-arg INFERENCE_MODEL=meta-llama/Llama-3.1-8B \
--secret id=hf_token,env=HF_TOKEN \
--tag vllm-gpu:Llama-3.1-8B \
--file vllm/Containerfile \
.
```

## Running

```console
docker run --gpus all -p 8000:8000 vllm-gpu:Qwen3-0.6B \
--model /opt/vllm/models/Qwen/Qwen3-0.6B \
--max-model-len 4096 \
--served-model-name Qwen3-0.6B \
--enable-auto-tool-choice \
--tool-call-parser hermes
```

Additional flags for larger GPUs:

```console
--gpu-memory-utilization 0.92
--enable-chunked-prefill
--enable-prefix-caching
```

## Health check

```console
curl http://localhost:8000/health
```

## CI

The `.github/workflows/vllm-gpu-container.yaml` workflow builds, smoke
tests, and publishes the image on a GPU runner:

- **Push to `main`** (when `Containerfile` or the workflow changes) —

Check warning on line 69 in vllm/README.md

View workflow job for this annotation

GitHub Actions / Detect hidden unicode characters

Unicode Safety [non-ascii-identifier]

U+2014 <unnamed U+2014> -- Non-ASCII U+2014 <unnamed U+2014> in identifier '—' (policy: ascii-only)
builds, tests, and pushes both `:<model>` (e.g. `:Qwen3-0.6B`) and
`:latest`.
- **Pull request** / **merge queue** — builds and tests only; nothing is

Check warning on line 72 in vllm/README.md

View workflow job for this annotation

GitHub Actions / Detect hidden unicode characters

Unicode Safety [non-ascii-identifier]

U+2014 <unnamed U+2014> -- Non-ASCII U+2014 <unnamed U+2014> in identifier '—' (policy: ascii-only)
pushed.
- **Manual (`workflow_dispatch`)** — accepts an `inference_model` input to

Check warning on line 74 in vllm/README.md

View workflow job for this annotation

GitHub Actions / Detect hidden unicode characters

Unicode Safety [non-ascii-identifier]

U+2014 <unnamed U+2014> -- Non-ASCII U+2014 <unnamed U+2014> in identifier '—' (policy: ascii-only)
build an arbitrary HuggingFace model; pushes `:<model>` but not
`:latest`. Defaults to `Qwen/Qwen3-0.6B`.

The image tag is derived from the model ID with the org prefix stripped
(`Qwen/Qwen3-0.6B` → `Qwen3-0.6B`). Gated models are supported via the

Check warning on line 79 in vllm/README.md

View workflow job for this annotation

GitHub Actions / Detect hidden unicode characters

Unicode Safety [non-ascii-identifier]

U+2192 <unnamed U+2192> -- Non-ASCII U+2192 <unnamed U+2192> in identifier '→' (policy: ascii-only)
`HF_TOKEN` repository secret.

## Dependabot

The `FROM` directive in `Containerfile` is monitored by Dependabot for
weekly base image updates (see `.github/dependabot.yaml`).
Loading