File size: 5,238 Bytes
5e84645
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# 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}"]