# 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}"]