runpod: make the capability images actually buildable, and cheap to ship

Three fixes the first real build exposed.

The import check caught what a lazy-import survey had missed: app.py
mounts every router, and a few touch libraries at module level rather
than lazily — soundfile (voice_convert), cv2/PIL (faceswap), litellm
(parser), websockets (broker client). A static walk of the import graph
from codai.api.app found the full set; they are now in core.txt with a
note saying why. torch and transformers are NOT among them, so they stay
per-profile.

torch moved into core so it becomes ONE shared layer. Nine images each
building their own copy would duplicate ~5 GB nine times — on disk and on
every push. Core is now its own RUN, identical across profiles, so all
nine share it.

A BuildKit pip cache mount means building all nine downloads torch once
rather than nine times, which matters when the link is also pulling
models.

build_all_capability_images.sh drives the set. Sequential on purpose:
they share the cache and the core layer, so parallel builds would race on
both. Each image must answer /healthz before the next starts.

Validated: coderai-embeddings built, imported and answered /healthz
(7259 MB) before this restructuring.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182DhR1eFNPQyGmDrpHbedY
parent 9fd72543
......@@ -21,7 +21,6 @@ ARG PROFILE=images
ARG TORCH_INDEX=https://download.pytorch.org/whl/cu124
ENV PROFILE=${PROFILE} \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
HF_HOME=/cache/huggingface \
CODERAI_HOME=/config
......@@ -35,9 +34,17 @@ WORKDIR /opt/coderai/app
# Dependencies first so a code change doesn't re-resolve the ML stack.
COPY packaging/runpod/profiles/ /tmp/profiles/
RUN python -m pip install --upgrade pip \
&& python -m pip install -r /tmp/profiles/core.txt \
&& python -m pip install --extra-index-url ${TORCH_INDEX} -r /tmp/profiles/${PROFILE}.txt \
# A BuildKit cache mount, so building all nine profiles downloads torch ONCE
# instead of nine times. The cache lives on the builder, never in the image.
# Core in its OWN layer: identical for every profile, so all nine images share
# it and it is pushed and pulled once.
RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install --upgrade pip \
&& python -m pip install --extra-index-url ${TORCH_INDEX} -r /tmp/profiles/core.txt
# Then only what this capability adds.
RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install --extra-index-url ${TORCH_INDEX} -r /tmp/profiles/${PROFILE}.txt \
&& rm -rf /tmp/profiles
COPY . /opt/coderai/app
......
#!/usr/bin/env bash
# Build every capability image, then optionally push them.
#
# ./packaging/runpod/build_all_capability_images.sh ghcr.io/nextime
# PUSH=1 ./packaging/runpod/build_all_capability_images.sh ghcr.io/nextime
#
# Builds are sequential on purpose: they share a pip cache and a core layer, so
# running them in parallel would race on both and download torch several times.
# Each image is smoke-tested (it must answer /healthz) before the next starts.
set -uo pipefail
cd "$(dirname "$0")/../.."
NS="${1:-}"
VERSION="${VERSION:-$(python3 -c 'import re,pathlib; print(re.search(r"__version__ = \"([^\"]+)\"", pathlib.Path("codai/__init__.py").read_text()).group(1))')}"
PROFILES="${PROFILES:-$(ls packaging/runpod/profiles/*.txt | xargs -n1 basename | sed 's/\.txt$//' | grep -v '^core$' | tr '\n' ' ')}"
if [[ -z "$NS" ]]; then
echo "usage: $0 <registry/namespace> e.g. ghcr.io/nextime" >&2
exit 2
fi
echo "== building ${VERSION} images for: ${PROFILES}"
ok=(); failed=()
for p in $PROFILES; do
tag="${NS}/coderai-${p}:${VERSION}"
echo
echo "================ $p -> $tag ================"
if PUSH=0 ./packaging/runpod/build_capability_image.sh "$p" "$tag"; then
docker tag "$tag" "${NS}/coderai-${p}:latest"
ok+=("$tag")
else
failed+=("$p")
fi
done
echo
echo "== built: ${#ok[@]} failed: ${#failed[@]} ${failed[*]:-}"
for t in "${ok[@]}"; do
printf ' %-52s %s MB\n' "$t" "$(( $(docker image inspect "$t" --format '{{.Size}}') / 1000000 ))"
done
if [[ "${PUSH:-0}" == "1" && ${#ok[@]} -gt 0 ]]; then
echo
echo "== pushing ${#ok[@]} images to ${NS}"
for t in "${ok[@]}"; do
docker push "$t" || failed+=("push:$t")
docker push "${t%:*}:latest" || failed+=("push:${t%:*}:latest")
done
fi
[[ ${#failed[@]} -eq 0 ]] || { echo "FAILURES: ${failed[*]}" >&2; exit 1; }
......@@ -27,7 +27,8 @@ if [[ ! -f "$PROFILES_DIR/$PROFILE.txt" ]]; then
fi
echo "==> building $TAG from profile '$PROFILE'"
docker build -f packaging/runpod/Dockerfile.capability \
# BuildKit for the pip cache mount: nine profiles, one torch download.
DOCKER_BUILDKIT=1 docker build -f packaging/runpod/Dockerfile.capability \
--build-arg "PROFILE=$PROFILE" \
-t "$TAG" .
......
# /v1/audio/generate, /v1/audio/stems, /v1/audio/cleanup
torch>=2.5.0
torchaudio
transformers>=4.35.0
diffusers>=0.25.0
......
# Core — what every capability pod needs to boot coderai and answer /healthz.
# The routers import lazily, so a pod only needs the libraries its own capability
# actually touches; this file is the floor under all of them.
# torch lives here rather than in each profile so it becomes ONE shared layer:
# nine images that each built their own copy would duplicate ~5 GB nine times,
# on disk and on every push. Profiles add torchvision/torchaudio as they need.
torch>=2.5.0
fastapi>=0.104.0
uvicorn[standard]>=0.24.0
pydantic>=2.5.0
......@@ -12,3 +17,18 @@ Pillow>=10.0.0
numpy>=1.24.0
safetensors>=0.4.0
huggingface_hub>=0.25.0
# Imported at MODULE level somewhere in the graph `import codai.api.app` walks,
# so every pod needs them regardless of profile — app.py mounts every router, and
# a few of those touch these at import time rather than lazily:
# cv2, PIL codai/api/faceswap.py
# soundfile codai/api/voice_convert.py
# litellm codai/models/parser.py
# websockets codai/broker/client.py
# Verified by a static walk of the import graph, and enforced by the Dockerfile's
# `import codai.api.app` build step. torch/transformers are NOT here: those are
# imported lazily, so they stay per-profile.
opencv-python-headless>=4.8.0
soundfile>=0.12.0
litellm>=1.40.0
websockets>=12.0
# /v1/embeddings and /v1/rerank
torch>=2.5.0
transformers>=4.35.0
sentence-transformers>=2.7.0
geoclip>=1.2.0
......
# /v1/images/faceswap
torch>=2.5.0
insightface>=0.7.3
onnxruntime-gpu>=1.20.0
opencv-python-headless>=4.8.0
# /v1/images/* — generation, edits, inpaint, outfit, upscale, deblur, segment, depth
torch>=2.5.0
torchvision
transformers>=4.35.0
accelerate>=0.24.0
......
# /v1/ocr — see also requirements-ocr.txt / requirements-surya.txt for the
# engine-specific extras a given OCR backend needs.
torch>=2.5.0
transformers>=4.35.0
pypdfium2>=4.20.0
# /v1/audio/transcriptions + diarization/speaker endpoints
torch>=2.5.0
torchaudio
transformers>=4.35.0
faster-whisper>=0.10.0
......
# /v1/audio/speech
torch>=2.5.0
torchaudio
transformers>=4.35.0
soundfile>=0.12.0
......
# /v1/video/* — generation, interpolation, upscale. The heaviest profile.
torch>=2.5.0
torchvision
torchaudio
transformers>=4.35.0
......
# /v1/audio/clone, /v1/audio/voices, /v1/audio/convert, watermarking
torch>=2.5.0
torchaudio
transformers>=4.35.0
f5-tts>=1.1.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment