diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5666c90..838279a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -84,7 +84,7 @@ graph LR Test["tests/test_image.cpp
tests/test_dinov2.cpp
tests/test_dinov2_c.cpp
tests/test_cli.cpp"] Tool["scripts/bench.sh
scripts/dinov2-to-gguf.py
scripts/publish-gguf.sh"] Build["CMakeLists.txt
CMakePresets.json
src/stb_image.h
src/stb_image_write.h
src/doctest.h"] - Tier2["wasm/ (embind shim + demo)
tools/server/ (dinov2-server)
examples/ (dedup, ci-visual-regression)"] + Tier2["wasm/ (embind shim + demo)
tools/server/ (dinov2-server)
examples/ (dedup, ci-visual-regression, container)"] CI[".github/workflows/release.yml
.github/workflows/build.yml
.github/workflows/extras.yml
.github/workflows/convert-and-publish-gguf.yml"] Doc["README.md
CONTRIBUTING.md
docs/cli.md
docs/build.md
docs/benchmarks.md
docs/hf-publishing.md
docs/stability.md + docs/tiers.md
docs/wasm.md
ARCHITECTURE.md
CHANGELOG.md"] Asset["assets/logo/
assets/tench.jpg
assets/logo.png"] @@ -117,7 +117,7 @@ Abridged table: | `CMakeLists.txt`, `CMakePresets.json`, vendored stb/doctest | Build | CMake build + vendored single-file deps. | | `wasm/` | Tier-2 | Emscripten embind shim over the C API + browser demo (`DINOV2_BUILD_WASM`). | | `tools/server/` | Tier-2 | `dinov2-server` HTTP embeddings service over the C API + vendored cpp-httplib (`DINOV2_BUILD_SERVER`). | -| `examples/` | Tier-2 | Copy-paste examples: `dedup/` (near-duplicate finder), `ci-visual-regression/` (GitHub Action). | +| `examples/` | Tier-2 | Copy-paste examples: `dedup/` (near-duplicate finder), `ci-visual-regression/` (GitHub Action), `container/` (FROM scratch demo image). | | `.github/workflows/*` | CI | build, release, convert-and-publish, parity, bench, changelog, extras (tier-2 advisory jobs). | | `README.md`, `CONTRIBUTING.md`, `docs/*`, `ARCHITECTURE.md`, `RELEASE_NOTES_*.md` | Doc | User + contributor-facing docs. | | `assets/*` | Asset | Logo + default input image. | diff --git a/examples/container/Dockerfile b/examples/container/Dockerfile new file mode 100644 index 0000000..89c0e6b --- /dev/null +++ b/examples/container/Dockerfile @@ -0,0 +1,54 @@ +# syntax=docker/dockerfile:1 +# +# dinov2.cpp container demo: a FROM scratch image carrying a statically +# linked dinov2-cli plus the pinned dinov2-small GGUF weight. The build +# context only needs this directory; the source tree and the model are +# fetched and verified inside the build stage. + +ARG DINOV2_REF=v0.4.0 +ARG MODEL_URL=https://huggingface.co/dinov2-cpp-core/dinov2-small-gguf/resolve/main/model.gguf +ARG MODEL_SHA256=b7ca009aa416f6be85ea95363f4ef13d3c139999f96ca3fd47226849afb712e6 + +FROM ubuntu:24.04 AS build +ARG DINOV2_REF MODEL_URL MODEL_SHA256 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential ca-certificates cmake curl git \ + && rm -rf /var/lib/apt/lists/* + +# GGUF weights, pinned by sha256 so a silently changed upstream file fails +# the build instead of shipping different embeddings. +RUN curl -fSL --retry 3 "$MODEL_URL" -o /model.gguf \ + && echo "$MODEL_SHA256 /model.gguf" | sha256sum -c - + +# dinov2-cli, fully static. BUILD_SHARED_LIBS=OFF makes libdinov2 and the +# vendored ggml build as .a archives (ggml defaults it ON on Linux), and +# GGML_BACKEND_DL defaults OFF so the CPU backend is compiled in rather than +# dlopen'd; -static then covers libc, libstdc++, libgomp and pthread. +# FindOpenMP records the shared libgomp.so path, which -static cannot use, +# so OpenMP_gomp_LIBRARY is preset to the gcc-shipped static archive. +# GGML_NATIVE=OFF avoids baking -march=native so the image stays portable +# across x86-64 and arm64 hosts. +RUN git clone --depth 1 --branch "$DINOV2_REF" \ + https://github.com/espetro/dinov2.cpp /src \ + && (git -C /src submodule update --init --depth 1 \ + || git -C /src submodule update --init) \ + && cmake -S /src -B /src/build -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF \ + -DDINOV2_BUILD_TESTS=OFF \ + -DGGML_NATIVE=OFF \ + -DOpenMP_gomp_LIBRARY="$(gcc -print-file-name=libgomp.a)" \ + -DCMAKE_EXE_LINKER_FLAGS="-static -static-libgcc -static-libstdc++" \ + && cmake --build /src/build --target dinov2-cli -j"$(nproc)" \ + && strip -o /dinov2-cli /src/build/bin/dinov2-cli \ + && if readelf -d /dinov2-cli | grep -q NEEDED; then \ + echo "error: dinov2-cli is not fully static" >&2; exit 1; \ + fi + +FROM scratch +COPY --from=build /dinov2-cli /dinov2-cli +COPY --from=build /model.gguf /model.gguf +ENTRYPOINT ["/dinov2-cli"] +CMD ["--help"] diff --git a/examples/container/README.md b/examples/container/README.md new file mode 100644 index 0000000..1c1de07 --- /dev/null +++ b/examples/container/README.md @@ -0,0 +1,91 @@ +# container: vision embeddings in a tiny scratch image + +A `FROM scratch` image that ships a statically linked `dinov2-cli` plus the +pinned `dinov2-small` GGUF weight. No shell, no libc, no package manager: +the whole runtime is one binary and one model file, roughly 50 MB total. + +Tier-2 example: not part of the shipped binary or CI gates. Build context +is this directory only; the Dockerfile clones the source at `DINOV2_REF` +(default `v0.4.0`) and downloads `model.gguf` from Hugging Face with a +pinned sha256, so the build fails loudly if either upstream changes. + +## Build + +```bash +docker build -t dinov2 examples/container/ +``` + +Multi-stage layout: + +1. `ubuntu:24.04` stage fetches `model.gguf` (sha256 verified), clones the + repo with the `ggml` submodule, and builds `dinov2-cli` with + `-DBUILD_SHARED_LIBS=OFF` and + `-DCMAKE_EXE_LINKER_FLAGS="-static -static-libgcc -static-libstdc++"`. + A `readelf` guard fails the build if the binary still has dynamic + dependencies. +2. `FROM scratch` copies only `/dinov2-cli` and `/model.gguf`. + +`GGML_NATIVE=OFF` keeps the binary portable, so the same Dockerfile +produces correct images on x86-64 and arm64 hosts (including emulated +`--platform` builds). + +## Run + +```bash +# version / help (no image needed) +docker run --rm dinov2 --version +docker run --rm dinov2 # prints usage via CMD ["--help"] + +# embed an image; mount a folder of inputs at /imgs +docker run --rm -v "$PWD/imgs:/imgs" dinov2 \ + -m /model.gguf -i /imgs/a.jpg --print-embeddings | jq . + +# every flag of dinov2-cli works the same way +docker run --rm -v "$PWD/imgs:/imgs" dinov2 \ + -m /model.gguf -i /imgs/a.jpg -i /imgs/b.jpg --batch 2 --print-embeddings +``` + +The image's `ENTRYPOINT` is `dinov2-cli`, so arguments after the image name +go straight to the CLI. `ENTRYPOINT ["/dinov2-cli"]` means there is no +shell inside; pass flags directly, and use `--entrypoint` only if you +bind-mount your own binary for debugging. + +## Size + +Measured on an arm64 host (x86-64 is within a few MB): + +| image | size | +| --- | --- | +| `dinov2` (this demo, scratch, arm64 build) | 45 MB | +| `gcr.io/distroless/cc-debian12` runtime alone | ~30 MB | +| `pytorch/pytorch` (CUDA + PyTorch + Python) | ~7 GB | + +Same embedding quality as the desktop CLI; the 100x+ difference vs the +PyTorch image is entirely the runtime: no Python, no CUDA stack, no OS +userland. If a fully static build ever regresses (for example a future +ggml backend that requires dlopen), the documented fallback is +`gcr.io/distroless/cc-debian12` plus the dynamically linked release +binary, which lands around 80 MB with the same model. + +## Build args + +| arg | default | purpose | +| --- | --- | --- | +| `DINOV2_REF` | `v0.4.0` | git tag/branch/commit cloned in the build stage | +| `MODEL_URL` | HF `dinov2-small-gguf` resolve URL | where `model.gguf` is fetched | +| `MODEL_SHA256` | pinned digest | build fails if the download does not match | + +```bash +docker build -t dinov2 --build-arg DINOV2_REF=main examples/container/ +``` + +## Notes + +- The model is baked in for zero config startup. For a slimmer image, + drop the `COPY` of `model.gguf` and mount it instead: + `docker run -v "$PWD/models:/models" dinov2 -m /models/model.gguf ...`. +- Static linking covers OpenMP (`libgomp`), pthread, libc and libstdc++. + `ggml` links `dl` on Linux, but with `GGML_BACKEND_DL=OFF` no `dlopen` + call survives into the binary, so a glibc static link is safe here. +- Networking is unnecessary at runtime; run with `--network none` for a + fully offline encode.