From fc7634077681c4131f82fb3be3843ee2980e08cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=83=81=EC=9C=A4?= Date: Wed, 1 Jul 2026 09:21:49 +0900 Subject: [PATCH 1/2] feat(container): add GPU (CUDA) serving image variant + image build CI Add a GPU variant of the serving container on an NVIDIA CUDA runtime base so a model's CUDA-enabled framework wheels (e.g. torch) can use the GPU at serve time. Like the CPU variant, torch is not baked; start_server.sh installs the model's requirements.txt via uv at runtime, so the CUDA torch wheel comes from the model. - container/Dockerfile.gpu: nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04 base, same uv + mlflow-skinny orchestrator toolchain and start_server.sh entrypoint. - .github/workflows/container-image.yml: matrix build (cpu/gpu) pushing to GHCR on release (and workflow_dispatch). CPU keeps the unsuffixed tag for backward compatibility; GPU uses the -gpu suffix. - container/README.md: document both variants, tag scheme, and runtime torch note. --- .github/workflows/container-image.yml | 87 +++++++++++++++++++++++++++ container/Dockerfile.gpu | 57 ++++++++++++++++++ container/README.md | 21 ++++++- 3 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/container-image.yml create mode 100644 container/Dockerfile.gpu diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml new file mode 100644 index 0000000..aba0b57 --- /dev/null +++ b/.github/workflows/container-image.yml @@ -0,0 +1,87 @@ +name: Build and Push Container Images + +# Builds the CPU and GPU serving images and pushes them to GHCR. +# - CPU keeps the unsuffixed tag (`:`, `:latest`) for backward compatibility +# with existing consumers (e.g. publish(image="ghcr.io/nubison/nubison-model:0.0.7")). +# - GPU is published under the `-gpu` suffix (`:-gpu`, `:latest-gpu`). +# Floating tags (:latest, :cpu, :gpu, :latest-gpu) are only moved on a published release, +# not on manual workflow_dispatch runs. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: "Image version tag (e.g. 0.0.8). Defaults to the short commit SHA." + required: false + type: string + +permissions: + contents: read + packages: write + +env: + IMAGE: ghcr.io/nubison/nubison-model + +jobs: + build: + name: Build ${{ matrix.variant }} image + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - variant: cpu + dockerfile: container/Dockerfile + - variant: gpu + dockerfile: container/Dockerfile.gpu + steps: + - uses: actions/checkout@v4 + + - name: Resolve version + id: ver + run: | + if [ "${{ github.event_name }}" = "release" ]; then + V="${{ github.event.release.tag_name }}" + elif [ -n "${{ inputs.version }}" ]; then + V="${{ inputs.version }}" + else + V="$(git rev-parse --short HEAD)" + fi + V="${V#v}" # strip a leading v (v0.0.8 -> 0.0.8) + echo "version=$V" >> "$GITHUB_OUTPUT" + + - name: Compute image tags + id: tags + run: | + V="${{ steps.ver.outputs.version }}" + IS_RELEASE="${{ github.event_name == 'release' }}" + if [ "${{ matrix.variant }}" = "cpu" ]; then + TAGS="${IMAGE}:${V},${IMAGE}:${V}-cpu" + [ "$IS_RELEASE" = "true" ] && TAGS="${TAGS},${IMAGE}:latest,${IMAGE}:cpu" + else + TAGS="${IMAGE}:${V}-gpu" + [ "$IS_RELEASE" = "true" ] && TAGS="${TAGS},${IMAGE}:gpu,${IMAGE}:latest-gpu" + fi + echo "tags=$TAGS" >> "$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 + uses: docker/build-push-action@v6 + with: + context: ./container + file: ${{ matrix.dockerfile }} + push: true + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=gha,scope=${{ matrix.variant }} + cache-to: type=gha,mode=max,scope=${{ matrix.variant }} diff --git a/container/Dockerfile.gpu b/container/Dockerfile.gpu new file mode 100644 index 0000000..a5b84a5 --- /dev/null +++ b/container/Dockerfile.gpu @@ -0,0 +1,57 @@ +# GPU (CUDA) variant of the nubison-model serving image. +# +# Mirrors container/Dockerfile (CPU) but sits on an NVIDIA CUDA runtime base so a +# model's CUDA-enabled framework wheels (e.g. torch) can use the GPU at serve time. +# +# Torch/TF are intentionally NOT baked here. Exactly like the CPU variant, +# start_server.sh downloads the model's requirements.txt from MLflow and builds the +# serving venv with uv at runtime, so the CUDA build of torch comes from the model's +# own requirements. This base only provides the CUDA runtime + cuDNN system libraries +# and the NVIDIA container-runtime hooks (NVIDIA_VISIBLE_DEVICES / _DRIVER_CAPABILITIES +# are preset by the nvidia/cuda base image) needed for those wheels to reach the GPU. +# +# CUDA 12.8 tracks the torch 2.7 line used by the notebook images. The host GPU driver +# must be new enough for this CUDA version; torch wheels ship their own CUDA libraries. +FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + python3 \ + python3-pip \ + python3-opencv \ + && rm -rf /var/lib/apt/lists/* + +# start_server.sh invokes a bare `python`; map it to the system python3. +RUN ln -sf /usr/bin/python3 /usr/local/bin/python + +SHELL ["bash", "-c"] +WORKDIR /app + +LABEL org.opencontainers.image.title="nubison-model-gpu" \ + org.opencontainers.image.description="GPU (CUDA) container image for nubison-model." \ + org.opencontainers.image.source="https://github.com/nubison/nubison-model" + +COPY start_server.sh /app/start_server.sh +RUN chmod +x /app/start_server.sh + +ENV MLFLOW_TRACKING_URI="" +ENV MLFLOW_MODEL_URI="" +ENV MLFLOW_MODEL_NAME="" +ENV MLFLOW_MODEL_VERSION="" +ENV DEBUG="" +ENV PORT=3000 +ENV NUM_WORKERS=2 + +EXPOSE ${PORT} + +# Same orchestrator toolchain as the CPU image: uv + mlflow-skinny drive dependency +# download and venv creation. pip is upgraded first because Ubuntu 22.04 ships an old +# pip; the base python only needs mlflow-skinny + pyyaml + uv. +RUN python3 -m pip install --no-cache-dir --upgrade pip && \ + python3 -m pip install --no-cache-dir uv==0.5.10 "mlflow-skinny>=3.12.0,<3.14.0" pyyaml && \ + uv python install 3.9 3.10 3.11 3.12 + +ENTRYPOINT [ "/app/start_server.sh" ] diff --git a/container/README.md b/container/README.md index dafea2d..0312bab 100644 --- a/container/README.md +++ b/container/README.md @@ -1,18 +1,33 @@ # Container for nubison-model -It is a container for the nubison-model. It is based on `python:3.12-slim-bookworm` with `uv` and `mlflow-skinny`. It will run the nubison-model server for a given model URI. +It is a container for the nubison-model. It runs the nubison-model server for a given model URI, using `uv` and `mlflow-skinny` to download the model's dependencies and build the serving venv at runtime. + +Two variants are published: + +| Variant | Dockerfile | Base image | Tags | +|---------|------------|------------|------| +| CPU (default) | `Dockerfile` | `python:3.12-slim-bookworm` | `:`, `:-cpu`, `:latest`, `:cpu` | +| GPU (CUDA) | `Dockerfile.gpu` | `nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04` | `:-gpu`, `:latest-gpu`, `:gpu` | + +CPU keeps the **unsuffixed** tag (`:`, `:latest`) for backward compatibility with existing consumers. The GPU variant is identical except for the CUDA base — torch/TF are **not** baked into either image; `start_server.sh` installs the model's own `requirements.txt` with `uv` at runtime. The GPU base only supplies the CUDA runtime + cuDNN system libraries and the NVIDIA container-runtime hooks so a CUDA-enabled torch wheel from the model's requirements can reach the GPU (the container must be scheduled with the NVIDIA runtime / a `nvidia.com/gpu` resource). ## Build ```bash -docker build -t ghcr.io/nubison/nubison-model:latest . +# CPU +docker build -t ghcr.io/nubison/nubison-model:latest -f Dockerfile . +# GPU +docker build -t ghcr.io/nubison/nubison-model:latest-gpu -f Dockerfile.gpu . ``` ## Push ```bash -docker push ghcr.io/nubison/nubison-model:latest +docker push ghcr.io/nubison/nubison-model:latest # CPU +docker push ghcr.io/nubison/nubison-model:latest-gpu # GPU ``` +Both variants are normally built and pushed by CI (`.github/workflows/container-image.yml`) on a published GitHub release (matrix over `cpu`/`gpu`), or on demand via `workflow_dispatch`. + ## Run ```bash From 06b037da057350e1fc536ef0a91b1fcf3c3611b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=83=81=EC=9C=A4?= Date: Wed, 1 Jul 2026 09:54:16 +0900 Subject: [PATCH 2/2] feat(container): broaden system deps for common ML library coverage The serving image installs each model's requirements.txt at runtime, so the base must carry the system deps that common ML wheels need. Add a build toolchain (build-essential, pkg-config), git (git+https requirements), ffmpeg + libsndfile1 (audio/video), and libsm6/libxext6 to both CPU and GPU images. Document remaining gaps in container/README.md: source-compiled CUDA extensions (need the -devel- base / nvcc) and CUDA driver vs torch-wheel compatibility on GPU (the base targets CUDA 12.8; a +cu130 torch wheel fails on a 12.8 node driver). --- container/Dockerfile | 10 ++++++++++ container/Dockerfile.gpu | 14 ++++++++++++++ container/README.md | 18 ++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/container/Dockerfile b/container/Dockerfile index 2128bcf..2f62445 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -1,7 +1,17 @@ FROM python:3.12.8-slim-bookworm +# System deps for the common ML stack so a model's requirements.txt installs cleanly: +# build tools (C/C++ ext without wheels), git (git+ requirements), ffmpeg/libsndfile1 +# (audio/video), libsm6/libxext6 (opencv/X runtime libs). Kept in parity with Dockerfile.gpu. RUN apt-get update && apt-get install -y \ bash \ + git \ + build-essential \ + pkg-config \ + ffmpeg \ + libsndfile1 \ + libsm6 \ + libxext6 \ python3-opencv \ && rm -rf /var/lib/apt/lists/* diff --git a/container/Dockerfile.gpu b/container/Dockerfile.gpu index a5b84a5..faaac65 100644 --- a/container/Dockerfile.gpu +++ b/container/Dockerfile.gpu @@ -16,9 +16,23 @@ FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04 ENV DEBIAN_FRONTEND=noninteractive +# System deps for the common ML stack so a model's requirements.txt installs cleanly: +# - build-essential/pkg-config: build C/C++ extension packages that ship no wheel +# - git: pip installs from `git+https://...` requirements +# - ffmpeg/libsndfile1: audio/video libs (torchaudio, librosa, moviepy, decord) +# - libsm6/libxext6: complete the opencv/X-linked runtime libraries +# NOTE: source-compiled CUDA extensions (nvcc) are still NOT covered — that needs the +# `-cudnn-devel-` base (~2x size). Prefer models that use prebuilt CUDA wheels. RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ ca-certificates \ + git \ + build-essential \ + pkg-config \ + ffmpeg \ + libsndfile1 \ + libsm6 \ + libxext6 \ python3 \ python3-pip \ python3-opencv \ diff --git a/container/README.md b/container/README.md index 0312bab..55eca00 100644 --- a/container/README.md +++ b/container/README.md @@ -28,6 +28,24 @@ docker push ghcr.io/nubison/nubison-model:latest-gpu # GPU Both variants are normally built and pushed by CI (`.github/workflows/container-image.yml`) on a published GitHub release (matrix over `cpu`/`gpu`), or on demand via `workflow_dispatch`. +## Library coverage + +The image does **not** bake ML frameworks; each model's `requirements.txt` is installed at +serve time. To let that install succeed for the common ML stack, both images include a +build toolchain (`build-essential`, `pkg-config`), `git` (for `git+https://` requirements), +`ffmpeg` + `libsndfile1` (audio/video), and the opencv/X runtime libraries. + +Known gaps (a model needing these must handle it itself): + +- **Source-compiled CUDA extensions** (e.g. building flash-attn / apex / mmcv from source) + need `nvcc` and CUDA headers, which are only in the `-cudnn-devel-` base (~2× size). The GPU + image uses the `-runtime-` base, so prefer models that install **prebuilt CUDA wheels**. +- **CUDA driver / torch wheel compatibility (GPU):** the GPU base targets CUDA 12.8 + (`NVIDIA_REQUIRE_CUDA=cuda>=12.8`). A model's CUDA framework wheel must match the **node + driver** — e.g. a torch wheel built for CUDA 13.0 (`+cu130`) fails on a node whose driver + only supports CUDA 12.8. Pin torch to a `cu126`/`cu128` build (or update the node driver). +- Exotic system libraries not listed above must be added to the image (or vendored by the model). + ## Run ```bash