| # VectorHD β CPU-only service container. | |
| # | |
| # docker build -t vectorhd . | |
| # docker run -p 7860:7860 -e VECTORHD_API_TOKEN=secret vectorhd | |
| # | |
| # CPU-only is the whole story now, not a slim variant of a GPU build. The retrofit replaced | |
| # DiffVG (the only CUDA-relevant path) with the numpy/scipy analytic optimizer, the Rust kernel | |
| # accelerates that on CPU, and the perception net β the sole remaining torch consumer β is gated | |
| # off by default because every head measurably harmed geometry. So there is one target, and it is | |
| # this one. (The old `cuda` target was removed for exactly that reason; see git history.) | |
| # | |
| # Stage 1 compiles the optional `vectorhd_core` Rust extension (β2.1Γ on analytic refine); stage 2 | |
| # is a slim runtime with no compilers. The extension is optional by design β if stage 1 were ever | |
| # dropped, `refine_backend=auto` silently serves the pure-Python kernel and the service still runs. | |
| ARG PYTHON_VERSION=3.11 | |
| # --------------------------------------------------------------------- stage 1: Rust extension | |
| # Must share PYTHON_VERSION with the runtime: the wheel is built for a specific CPython ABI | |
| # (cp311), so a mismatch here yields a wheel the runtime cannot import. | |
| FROM python:${PYTHON_VERSION}-slim AS rust-builder | |
| ENV PIP_NO_CACHE_DIR=1 \ | |
| PIP_DISABLE_PIP_VERSION_CHECK=1 \ | |
| CARGO_HOME=/usr/local/cargo \ | |
| RUSTUP_HOME=/usr/local/rustup \ | |
| PATH=/usr/local/cargo/bin:$PATH | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| curl build-essential ca-certificates \ | |
| && rm -rf /var/lib/apt/lists/* \ | |
| && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | |
| | sh -s -- -y --profile minimal --default-toolchain stable \ | |
| && pip install "maturin>=1.5,<2" | |
| WORKDIR /build | |
| # Only the crate manifest + sources: the 46 MB of test vectors and the wasm harness are not | |
| # build inputs (and are excluded from the context by .dockerignore anyway). | |
| COPY rust/vectorhd-core/Cargo.toml rust/vectorhd-core/pyproject.toml ./ | |
| COPY rust/vectorhd-core/src ./src | |
| # `--features python` comes from the crate's [tool.maturin]; release for the optimised kernel. | |
| RUN maturin build --release --out /wheels | |
| # --------------------------------------------------------------------------- stage 2: runtime | |
| FROM python:${PYTHON_VERSION}-slim AS runtime | |
| ENV PYTHONUNBUFFERED=1 \ | |
| PYTHONDONTWRITEBYTECODE=1 \ | |
| PIP_NO_CACHE_DIR=1 \ | |
| PIP_DISABLE_PIP_VERSION_CHECK=1 \ | |
| VIRTUAL_ENV=/opt/venv \ | |
| PATH=/opt/venv/bin:$PATH | |
| # opencv-python-headless still wants libGL/glib at import time even in its headless build. | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| libgl1 libglib2.0-0 \ | |
| && rm -rf /var/lib/apt/lists/* | |
| RUN python -m venv "$VIRTUAL_ENV" | |
| WORKDIR /app | |
| # CPU torch FIRST, from the CPU index: this satisfies the `torch>=2.2` core dependency so the | |
| # subsequent `pip install .` does not pull the multi-GB CUDA wheel from PyPI. | |
| RUN pip install --index-url https://download.pytorch.org/whl/cpu "torch>=2.2" | |
| COPY pyproject.toml README.md ./ | |
| COPY src ./src | |
| RUN pip install . | |
| # The optional Rust kernel, built in stage 1. `refine_backend=auto` picks it up automatically. | |
| COPY --from=rust-builder /wheels/*.whl /tmp/wheels/ | |
| RUN pip install /tmp/wheels/*.whl && rm -rf /tmp/wheels | |
| # --- Runtime configuration --------------------------------------------------------------- | |
| # Tuned for a small shared-CPU host (Hugging Face free tier = 2 vCPU / 16 GB). | |
| # | |
| # WORKER_POOL_SIZE=1: each worker runs the whole CPU-bound pipeline and parallelises internally | |
| # through BLAS. On 2 vCPU a second worker would contend with the first rather than add throughput; | |
| # one worker + capped BLAS threads keeps a single hd job from fighting itself. Raise this on a | |
| # bigger box (rule of thumb: β cores), and bound concurrency at the edge either way. | |
| # | |
| # The *_NUM_THREADS caps are read by numpy/scipy/OpenCV at import. They are set here (container | |
| # env) rather than in code so they are inherited by the spawned pool workers, which re-import | |
| # numpy fresh in a new interpreter. | |
| ENV VECTORHD_DEVICE=cpu \ | |
| VECTORHD_WORKER_POOL_SIZE=1 \ | |
| VECTORHD_WEIGHTS_DIR=/weights \ | |
| OMP_NUM_THREADS=2 \ | |
| OPENBLAS_NUM_THREADS=2 \ | |
| MKL_NUM_THREADS=2 \ | |
| NUMEXPR_NUM_THREADS=2 \ | |
| VECLIB_MAXIMUM_THREADS=2 \ | |
| PORT=7860 | |
| # Non-root. /app holds no writable state (nothing is ever persisted β see the privacy note), so | |
| # the runtime user only needs read access. | |
| RUN useradd --create-home --uid 1000 appuser && chown -R appuser:appuser /app | |
| USER appuser | |
| # 7860 is the Hugging Face Spaces convention; $PORT overrides it for other hosts. | |
| EXPOSE 7860 | |
| # No curl in the image β urllib is already present and keeps the runtime layer lean. | |
| HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ | |
| CMD python -c "import os,urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:'+os.environ.get('PORT','7860')+'/healthz', timeout=4).status==200 else 1)" | |
| # Shell form so ${PORT} expands. One uvicorn process: request concurrency is the worker pool's | |
| # job, and a second uvicorn worker would double the pool. | |
| CMD ["sh", "-c", "exec uvicorn vectorhd.api.app:app --host 0.0.0.0 --port ${PORT:-7860}"] | |