Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ graph LR
Test["tests/test_image.cpp<br/>tests/test_dinov2.cpp<br/>tests/test_dinov2_c.cpp<br/>tests/test_cli.cpp"]
Tool["scripts/bench.sh<br/>scripts/dinov2-to-gguf.py<br/>scripts/publish-gguf.sh"]
Build["CMakeLists.txt<br/>CMakePresets.json<br/>src/stb_image.h<br/>src/stb_image_write.h<br/>src/doctest.h"]
Tier2["wasm/ (embind shim + demo)<br/>tools/server/ (dinov2-server)<br/>examples/ (dedup, ci-visual-regression)"]
Tier2["wasm/ (embind shim + demo)<br/>tools/server/ (dinov2-server)<br/>examples/ (dedup, ci-visual-regression, container)"]
CI[".github/workflows/release.yml<br/>.github/workflows/build.yml<br/>.github/workflows/extras.yml<br/>.github/workflows/convert-and-publish-gguf.yml"]
Doc["README.md<br/>CONTRIBUTING.md<br/>docs/cli.md<br/>docs/build.md<br/>docs/benchmarks.md<br/>docs/hf-publishing.md<br/>docs/stability.md + docs/tiers.md<br/>docs/wasm.md<br/>ARCHITECTURE.md<br/>CHANGELOG.md"]
Asset["assets/logo/<br/>assets/tench.jpg<br/>assets/logo.png"]
Expand Down Expand Up @@ -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. |
Expand Down
54 changes: 54 additions & 0 deletions examples/container/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
91 changes: 91 additions & 0 deletions examples/container/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading