# syntax=docker/dockerfile:1
# Three named build contexts feed this image. docker-compose.yml passes all three by itself:
#
#   ops       the compiled kernels from build.sh step 8 (this repo's artifacts/). The image then
#             installs them instead of compiling: ~2 min.
#   uvcache   $HOME/.cache/uv   ─┐ two wheel caches, so the build does not re-download the
#   pipcache  $HOME/.cache/pip  ─┘ torch + CUDA wheel set (~6 GB). Read only.
#
# Plain `docker build -t vllm:latest .` passes none of them: the ops context is empty, so the
# image compiles every kernel from source, and the wheel caches start empty. BuildKit keeps the
# caches in its own volumes after the first build, so a rebuild downloads nothing again.
#
#   docker compose up -d                                                          # kernels installed
#   docker build -t vllm:latest .                                                 # compiles
#   docker build --build-context ops="$PWD/artifacts" -t vllm:latest .            # no compile
#
# A cache mount copies its context into the BuildKit cache volume the first time and reuses it
# after that. The caches on the host are only read: the build never writes to them.
#
# build.sh compares the tree object, the arch, CPython, CUDA and torch in the manifest with the
# tree it installs into, and hashes every unpacked file. A tarball that does not fit this tree
# stops the build. The image build runs with PACK=0: an image layer must not carry a 340 MB tarball.
FROM scratch AS uvcache
FROM scratch AS pipcache
FROM scratch AS ops
# trixie, not bookworm: the kernels of a host build on Debian 13 need glibc 2.38, bookworm has 2.36,
# so the loader would refuse them at serve time. trixie has 2.41.
FROM python:3.13-slim-trixie
# vllm: Qwen3.8-Flash-Next-W4A16 at 262K on 4x RTX 3090 (vLLM 0.30.0 + patches/).
#
#   docker build -t vllm:latest .
#   docker run -d --name vllm --gpus all --stop-timeout 20 --shm-size=8g \
#     --ulimit memlock=-1 -p 8080:8000 \
#     -v /data/models/halt95/Swift1.5-Qwen3.8-Flash-Next-W4A16-Merlin:/models/model:ro \
#     -v "$PWD/.vllm-cache":/cache \
#     -e MODEL_NAME=Hinata vllm:latest
#
# The checkpoint mount must contain qsa_kv_scales.json (the FP8 KV scales sidecar).
#
# The build fetches vLLM v0.30.0 and applies patches/ (build.sh step 2; needs network), then compiles
# every kernel of this tree from source (build.sh: cmake + ninja + nvcc 13.0 from the venv's
# CUDA wheels, arch 8.6). No prebuilt binaries go in. Budget 20-40 min and ~15 GB of build disk.
# Pass the ops context (see above) to install prebuilt kernels instead: ~2 min, same tree.
# trixie ships cmake 3.25, which is too old, so cmake and ninja come from pip.
#
# Layers are split so a rebuild stays cheap. The long compile only re-runs when a file it reads
# changes (build.sh, scripts/, patches/, start.sh). Docs, README, compose and
# the example env land in a later COPY, so editing them costs seconds. The apt, pip, uv and ccache
# caches are BuildKit cache mounts: they live outside the image, so a rebuild downloads nothing
# again and recompiles only the kernels whose source changed (build.sh prints the ccache stats).
ARG MAX_JOBS=32
ARG ARCH=8.6
ENV DEBIAN_FRONTEND=noninteractive PIP_NO_CACHE_DIR=1
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates tar gzip procps \
       build-essential ccache binutils
WORKDIR /opt/vllm
# The build tools. These two mounts have their own ids: a cache mount with the same target path
# shares one volume, and a from= seed applies only when the volume is first created. Without the
# ids, this layer would create the pip/uv volumes empty and the seeds of the compile layer below
# would never be read. (PIP_NO_CACHE_DIR= (empty) makes pip use the cache.)
# build.sh uses uv for the venv when it is on PATH; it makes the 196-pin install several times faster.
# cmake<4: pip ships 4.x, the tree and its vendored projects are built and tested on 3.x.
RUN --mount=type=cache,id=tools-pip,target=/root/.cache/pip,sharing=locked \
    --mount=type=cache,id=tools-uv,target=/root/.cache/uv,sharing=locked \
    PIP_NO_CACHE_DIR= python -m pip install uv 'cmake<4' ninja
# The build inputs, in one layer. Nothing here changes on a docs edit.
COPY build.sh start.sh ./
COPY scripts/ scripts/
COPY patches/ patches/
RUN --mount=type=cache,id=compile-uv,target=/root/.cache/uv,from=uvcache,sharing=locked \
    --mount=type=cache,id=compile-pip,target=/root/.cache/pip,from=pipcache,sharing=locked \
    --mount=type=cache,id=compile-ccache,target=/root/.cache/ccache \
    --mount=type=bind,from=ops,target=/ops,ro \
    chmod +x start.sh build.sh scripts/*.sh scripts/*.py \
    && echo "==> caches at the start of the compile:" && (du -sh /root/.cache/uv /root/.cache/pip /root/.cache/ccache 2>/dev/null || true) \
    && ops_tar="$(ls -1 /ops/build-artifacts-*.tar.gz 2>/dev/null | head -1 || true)" \
    && if [ -n "$ops_tar" ]; then echo "==> ops source: $ops_tar (prebuilt, no compile)"; else echo "==> ops source: none passed, compiling from source"; fi \
    && PIP_NO_CACHE_DIR= MAX_JOBS="$MAX_JOBS" ARCH="$ARCH" PACK=0 ARTIFACTS="$ops_tar" bash build.sh \
    && rm -rf /opt/vllm/vllm/.deps /opt/vllm/vllm/CMakeUserPresets.json \
       /opt/vllm/build-*.log
# The record build.sh writes (vllm/.build-products, vllm/.build-products.sha256) is KEPT on
# purpose: start.sh re-hashes every kernel against it before it starts the engine.
# Everything else: README, Dockerfile, compose, .env.example, systemd/. A copy, so it is cheap.
COPY . /opt/vllm/
RUN chmod +x start.sh build.sh scripts/*.sh scripts/*.py \
    && rm -rf /root/.cache
ENV TREE=/opt/vllm/vllm VENV=/opt/vllm/.venv HOST=0.0.0.0 PORT=8000 CACHE_ROOT=/cache \
    MODEL_NAME=Hinata MODEL_MOUNT_PATH=/models/model LOG= \
    CUDA_HOME=/opt/vllm/.venv/lib/python3.13/site-packages/nvidia/cu13 \
    FLASHINFER_WORKSPACE_BASE=/cache/flashinfer TRITON_CACHE_DIR=/cache/triton
ENV PATH="/opt/vllm/.venv/lib/python3.13/site-packages/nvidia/cu13/bin:$PATH"
VOLUME /cache
EXPOSE 8000
# /v1/models answers 200 even when the engine is dead: probe a real one-token generation.
HEALTHCHECK --interval=60s --timeout=90s --start-period=30m --retries=3 \
  CMD m="${MODEL_NAME%% *}"; m="${m:-Hinata}"; curl -fs -m 85 -X POST "http://127.0.0.1:${PORT}/v1/chat/completions" \
      -H "Content-Type: application/json" ${VLLM_API_KEY:+-H "Authorization: Bearer $VLLM_API_KEY"} \
      -d "{\"model\":\"$m\",\"messages\":[{\"role\":\"user\",\"content\":\"ok\"}],\"max_tokens\":1,\"chat_template_kwargs\":{\"enable_thinking\":false}}" \
      | grep -q '"choices"' || exit 1
ENTRYPOINT ["/opt/vllm/start.sh"]
CMD ["/models/model"]
