EmbeddingGemma 2 lokal auf der CPU: Embeddings mit Ollama und llama.cpp, gemessen gegen bge-m3
EmbeddingGemma 2 von Google läuft als Textmodell mit 270 Millionen Parametern ohne Grafikkarte. So betreiben Sie es per Docker Compose mit Ollama 0.40.8 oder llama.cpp v0.7.0, messen Trefferqualität und Tempo auf deutschen Texten gegen bge-m3 und sichern Modelle samt Konfiguration.
Geprüft am 22.10.2026 · für Ollama 0.40.8, llama.cpp 0.7.0, EmbeddingGemma 2 270m-q8_0-text, GGUF Q8_0 Rev. 58faf909
Mit KI erstellt – redaktionelle Prüfung ausstehend
WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Google hat am 6. Oktober 2026 EmbeddingGemma 2 veröffentlicht, ein offenes Embedding-Modell für Text, Bilder, Audio und Video. Für ein RAG-System (Retrieval-Augmented Generation, Suche plus Sprachmodell) zählt der Textteil mit 270 Millionen Parametern, der ohne Grafikkarte läuft. Diese Anleitung zeigt beide gängigen Wege per Docker Compose, Ollama 0.40.8 und llama.cpp v0.7.0, und ein Messskript, das EmbeddingGemma 2 auf deutschen Texten gegen das verbreitete bge-m3 antreten lässt. Der Preis eines Wechsels: Alle vorhandenen Vektoren müssen neu berechnet werden.
Voraussetzungen
- Linux-Server mit x86-64-CPU und Docker Engine samt Docker Compose Plugin (Einrichtung: Docker Compose unter Linux). Eine Grafikkarte ist nicht nötig. Getestet mit 14 CPU-Threads eines Intel Core i7-12700T.
- Arbeitsspeicher: Im Test belegte Ollama mit beiden Modellen 2,459 GiB, llama.cpp 445,6 MiB. Planen Sie 4 GB frei ein.
- Speicher: Die Modelle belegten im Test 1,4 GiB (Ordner
ollama) und 296 MiB (Ordnermodels), dazu kommen die Docker-Images und Platz für die Sicherung. - Werkzeuge auf dem Host:
curl,jq,openssl,sha256sum. - Einmalig Internetzugang für Images und Modelle; Ihre Texte verlassen den Server nicht.
Modell und Lizenz im Überblick
| Eckdaten | Wert |
|---|---|
| Modell | EmbeddingGemma 2, Text 270M, 768 Dimensionen, kürzbar auf 512, 256, 128 |
| Lizenz | Apache 2.0 (Vorgänger EmbeddingGemma 1: Gemma Terms of Use) |
| Ollama | ollama/ollama:0.40.8, Modell embeddinggemma-2:270m-q8_0-text (304 MB) |
| llama.cpp | ghcr.io/ggml-org/llama.cpp:server-b11669 (Stand v0.7.0), GGUF Q8_0 (310 MB) |
| Vergleich | bge-m3:567m (1024 Dimensionen, 1,2 GB) |
| Ports | 11434 (Ollama), 8081 (llama.cpp), beide nur 127.0.0.1 |
Laut Modellkarte lassen sich die Vektoren dank Matryoshka-Training (das Wichtigste steht in den ersten Werten) kürzen, was Platz im Vektorindex spart.
Anders als der Vorgänger ist EmbeddingGemma 2 auf Hugging Face ohne Zustimmung abrufbar; die Modellkarte verlangt die Einhaltung der Gemma Prohibited Use Policy.
Wichtig: Das Modell erwartet Aufgaben-Präfixe. Für die Suche bekommt die Frage task: search result | query: vorangestellt, jedes Dokument title: {Titel} | text: {Inhalt} (ohne Titel title: none). Weder Ollama noch llama.cpp setzen diese Präfixe selbst, das erledigt Ihre Anwendung.
Schritt 1: Projektordner und compose.yaml anlegen
Legen Sie den Projektordner an und erzeugen Sie einen zufälligen API-Schlüssel für llama.cpp. Ollama nimmt Anfragen ohne Anmeldung an, deshalb lauschen beide Dienste nur auf 127.0.0.1.
mkdir -p ~/embeddings/ollama ~/embeddings/models ~/embeddings/bewertung
cd ~/embeddings
echo "LLAMA_API_KEY=$(openssl rand -hex 32)" > .env
chmod 600 .env
Die compose.yaml enthält Ollama, den llama.cpp-Server im Embedding-Modus und einen Python-Container nur für die Messung (Profil bewertung, startet nicht mit):
services:
ollama:
image: ollama/ollama:0.40.8
restart: unless-stopped
ports:
- "127.0.0.1:11434:11434"
volumes:
- ./ollama:/root/.ollama
environment:
OLLAMA_CONTEXT_LENGTH: "8192"
OLLAMA_KEEP_ALIVE: "1h"
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 30s
timeout: 10s
retries: 5
llama:
image: ghcr.io/ggml-org/llama.cpp:server-b11669
restart: unless-stopped
ports:
- "127.0.0.1:8081:8080"
volumes:
- ./models:/models:ro
environment:
LLAMA_ARG_MODEL: /models/embeddinggemma-2-Q8_0.gguf
LLAMA_ARG_EMBEDDINGS: "true"
LLAMA_ARG_CTX_SIZE: "8192"
LLAMA_ARG_N_PARALLEL: "2"
LLAMA_ARG_BATCH: "4096"
LLAMA_ARG_UBATCH: "4096"
LLAMA_API_KEY: ${LLAMA_API_KEY}
bewertung:
image: python:3.13.17-slim
profiles: ["bewertung"]
volumes:
- ./bewertung:/app:ro
environment:
LLAMA_API_KEY: ${LLAMA_API_KEY}
command: ["python", "/app/bewertung.py"]
LLAMA_ARG_CTX_SIZE gilt für alle Slots zusammen, mit zwei Slots hat jede Anfrage 4096 Token. Bei Embeddings verarbeitet llama.cpp jeden Text in einem Stück, deshalb muss LLAMA_ARG_UBATCH mindestens so groß sein wie der längste Text. Der Healthcheck für Ollama ruft ollama list auf, das llama.cpp-Image bringt einen eigenen mit. Weitere Härtung beschreibt die Anleitung Docker Compose absichern.
Verifizieren: docker compose config --quiet && echo ok gibt ok aus.
Schritt 2: Ollama starten und Modelle laden
docker compose up -d ollama
until curl -sfo /dev/null http://127.0.0.1:11434/; do sleep 2; done
docker compose exec ollama ollama --version
docker compose exec ollama ollama pull bge-m3:567m
docker compose exec ollama ollama pull embeddinggemma-2:270m-q8_0-text
docker compose exec ollama ollama list
Das Tag 270m-q8_0-text ist der reine Textteil in 8-Bit-Quantisierung. Laut Manifest im Ollama-Register setzt es Ollama 0.40.4 voraus; ältere Versionen lehnen den Download ab (siehe Troubleshooting).
Verifizieren: Die Version lautet ollama version is 0.40.8, die Liste zeigt beide Modelle:
NAME ID SIZE MODIFIED
embeddinggemma-2:270m-q8_0-text c21dd30c1e38 303 MB Less than a second ago
bge-m3:567m 790764642607 1.2 GB 26 seconds ago
Schritt 3: Embeddings über Ollama abrufen
Die Schnittstelle /api/embed nimmt einen Text oder eine Liste. Der erste Eintrag ist eine Suchanfrage, der zweite ein Dokument, jeweils mit Präfix:
curl -s http://127.0.0.1:11434/api/embed -d '{"model":"embeddinggemma-2:270m-q8_0-text","input":["task: search result | query: Wie beantrage ich Urlaub?","title: Urlaubsantrag stellen | text: Urlaub beantragen Sie im Personalportal."]}' | jq '{dim: (.embeddings[0]|length), anzahl: (.embeddings|length), token: .prompt_eval_count}'
{
"dim": 768,
"anzahl": 2,
"token": 36
}
Kürzere Vektoren fordern Sie mit "dimensions": 256 an. Laut Modellkarte müssen gekürzte Vektoren vor dem Kosinusvergleich neu normalisiert werden, und Fragen und Dokumente brauchen dieselbe Länge. Ollama lieferte die gekürzten Vektoren im Test bereits mit Länge 1.
Verifizieren: dim ist 768, mit "dimensions": 256 liefert dieselbe Abfrage 256 Werte.
Schritt 4: llama.cpp mit GGUF-Datei einrichten
llama.cpp unterstützt EmbeddingGemma 2 seit dem stabilen Release v0.7.0 vom 15. Oktober 2026. Für diesen Stand gibt es kein eigenes Image-Tag, aber den Build server-b11669 aus genau demselben Commit. Laden Sie die GGUF-Datei aus einer festen Revision und prüfen Sie die Prüfsumme:
curl -fsSL -o models/embeddinggemma-2-Q8_0.gguf https://huggingface.co/ggml-org/embeddinggemma-2-GGUF/resolve/58faf90940886df2d62be555c8765d00dac362c5/embeddinggemma-2-Q8_0.gguf
echo "2188ac1deca4b77dffefd603c2776a9d76d9d74ec01841392982ebb840b09135 models/embeddinggemma-2-Q8_0.gguf" | sha256sum -c
docker compose up -d llama
until curl -sf http://127.0.0.1:8081/health; do sleep 2; done
docker compose exec llama /app/llama-server --version
Das Repository bietet Q8_0 und BF16 an. Eine eigene float16-Umwandlung meiden Sie: Laut Modellkarte übersteigen die Werte im Modell den Bereich von float16.
Die OpenAI-kompatible Schnittstelle /v1/embeddings verlangt den Schlüssel aus der .env:
KEY=$(grep ^LLAMA_API_KEY= .env | cut -d= -f2)
curl -s http://127.0.0.1:8081/v1/embeddings -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"input":"task: search result | query: Wie beantrage ich Urlaub?"}' | jq '{dim: (.data[0].embedding|length), token: .usage.prompt_tokens}'
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8081/v1/embeddings -H "Content-Type: application/json" -d '{"input":"test"}'
Verifizieren: sha256sum meldet OK, die Version lautet version: 0.7.0-dev (build 11669, commit 620d2225c), die Abfrage mit Schlüssel liefert "dim": 768 und "token": 15, ohne Schlüssel kommt 401. Für denselben Satz lagen die Vektoren aus Ollama und llama.cpp im Test bei einer Kosinus-Ähnlichkeit von 0,9999.
Schritt 5: Trefferqualität und Tempo messen
Öffentliche Benchmarks sagen wenig über Ihre Dokumente. Legen Sie eine kleine Testmenge an: kurze Texte aus Ihrem Bestand und Fragen mit dem passenden Dokument als Ziel. Die Testmenge im Test umfasste 24 Texte einer fiktiven IT-Wissensdatenbank, 24 deutsche und 6 englische Fragen. Das Format von bewertung/testmenge.json (gekürzt):
{
"dokumente": [
{
"id": "urlaub",
"titel": "Urlaubsantrag stellen",
"text": "Urlaub beantragen Sie im Personalportal unter Abwesenheiten. Die Teamleitung erhält den Antrag automatisch und bestätigt ihn innerhalb von drei Arbeitstagen."
}
],
"fragen": [
{
"sprache": "de",
"frage": "Ich möchte im August zwei Wochen freinehmen. Wo trage ich das ein?",
"ziel": "urlaub"
}
]
}
Das Skript bewertung/bewertung.py braucht nur die Python-Standardbibliothek, ermittelt je Frage den Rang des richtigen Dokuments und misst die Zeiten:
"""Vergleicht Embedding-Modelle auf einer eigenen Testmenge (nur Standardbibliothek)."""
import json, math, os, time, urllib.request
EG2, BGE = "embeddinggemma-2:270m-q8_0-text", "bge-m3:567m"
T = json.load(open("/app/testmenge.json", encoding="utf-8"))
DOKS = T["dokumente"]
def post(url, body, key=None):
kopf = {"Content-Type": "application/json"}
if key:
kopf["Authorization"] = "Bearer " + key
with urllib.request.urlopen(urllib.request.Request(url, json.dumps(body).encode(), kopf), timeout=900) as r:
return json.load(r)
def ollama(modell, dim=None):
def f(texte):
extra = {"dimensions": dim} if dim else {}
d = post("http://ollama:11434/api/embed", {"model": modell, "input": texte, **extra})
return d["embeddings"], d["prompt_eval_count"]
return f
def llama(texte):
d = post("http://llama:8080/v1/embeddings", {"input": texte}, os.environ["LLAMA_API_KEY"])
return [x["embedding"] for x in d["data"]], d["usage"]["prompt_tokens"]
def norm(e): # auf Länge 1 bringen, nötig nach dem Kürzen
n = math.sqrt(sum(x * x for x in e))
return [x / n for x in e]
def einbetten(f, texte): # in Paketen zu acht
vek, tok = [], 0
for i in range(0, len(texte), 8):
v, t = f(texte[i:i + 8])
vek += [norm(e) for e in v]
tok += t
return vek, tok
def bewerte(name, f, praefix):
docs = [f"title: {d['titel']} | text: {d['text']}" if praefix else f"{d['titel']}. {d['text']}" for d in DOKS]
f(docs[:1]) # Aufwärmen: Modell laden, bevor die Zeit läuft
t0 = time.perf_counter()
dv, tok = einbetten(f, docs)
t_docs, werte, ms, fehl = time.perf_counter() - t0, {}, [], []
for q in T["fragen"]:
t0 = time.perf_counter()
qv = einbetten(f, [("task: search result | query: " if praefix else "") + q["frage"]])[0][0]
ms.append(1000 * (time.perf_counter() - t0))
score = [sum(a * b for a, b in zip(qv, v)) for v in dv]
ziel = score[[d["id"] for d in DOKS].index(q["ziel"])]
rang = 1 + sum(s > ziel for s in score)
w = werte.setdefault(q["sprache"], [0, 0, 0.0, 0])
w[0] += rang == 1; w[1] += rang <= 3; w[2] += 1 / rang; w[3] += 1
if rang > 1:
fehl.append(f"{q['ziel']}:{rang}")
aus = " | ".join(f"{s} Top1 {a}/{n} Top3 {b}/{n} MRR {c / n:.2f}" for s, (a, b, c, n) in sorted(werte.items()))
print(f"{name:<26} {len(dv[0])}d | {aus} | Doks {tok} Token {t_docs:.2f}s = {tok / t_docs:.0f} Token/s"
f" | Frage {sum(ms) / len(ms):.0f}ms | daneben {' '.join(fehl)}", flush=True)
for name, f, praefix in [("EG2 Ollama mit Präfix", ollama(EG2), True),
("EG2 Ollama ohne Präfix", ollama(EG2), False),
("EG2 Ollama 256d", ollama(EG2, 256), True),
("EG2 Ollama 128d", ollama(EG2, 128), True),
("EG2 llama.cpp mit Präfix", llama, True),
("bge-m3 Ollama", ollama(BGE), False)]:
try:
bewerte(name, f, praefix)
except Exception as e: # ein Fehler soll die übrigen Messungen nicht stoppen
print(f"{name:<26} FEHLER {e} {getattr(e, 'read', bytes)()[:200]}", flush=True)
docker compose run --rm bewertung
Top1 zählt, wie oft das richtige Dokument auf Platz 1 landet, Top3 die Plätze 1 bis 3. Der MRR (Mean Reciprocal Rank) bewertet auch die Position: Platz 1 zählt 1, Platz 2 zählt 0,5. Im Test ergab sich (Zeilen ohne Modellname: EmbeddingGemma 2):
| Variante | Top1 DE | Top3 DE | MRR DE | Top1 EN | Token/s | ms je Frage |
|---|---|---|---|---|---|---|
| Ollama, Präfix | 22/24 | 24/24 | 0,96 | 6/6 | 956 | 32 |
| Ollama, ohne Präfix | 21/24 | 22/24 | 0,91 | 6/6 | 868 | 26 |
| Ollama, 256d | 21/24 | 24/24 | 0,94 | 6/6 | 871 | 31 |
| Ollama, 128d | 20/24 | 22/24 | 0,90 | 5/6 | 897 | 30 |
| llama.cpp, Präfix | 22/24 | 24/24 | 0,96 | 6/6 | 932 | 29 |
| bge-m3 (Ollama) | 22/24 | 24/24 | 0,95 | 6/6 | 477 | 44 |
EmbeddingGemma 2 mit Präfixen und bge-m3 lagen gleichauf. Ohne Präfixe landete die Frage zur Kennwortregel erst auf Rang 15. Mit 256 Dimensionen blieben alle Ziele unter den ersten drei, bei 128 fielen Fragen zurück. Ein zweiter Messlauf ergab dieselben Trefferwerte, nur die Zeiten schwankten.
Für alle 24 Dokumente brauchte EmbeddingGemma 2 über Ollama 1,17 s, bge-m3 2,11 s. Die absoluten Zeiten gelten nur für die Test-CPU.
Verifizieren: Jede Zeile nennt Dimension, Top1, Top3 und MRR je Sprache, FEHLER heißt: Dienst oder Modell fehlt.
Schritt 6: Sichern und nach Totalverlust wiederherstellen
Vektoren im Index passen nur zum Modell, das sie erzeugt hat, und ein neuer Download liefert nicht zwingend dieselbe Fassung. Sichern Sie Modelle deshalb zusammen mit dem Vektorindex. Notieren Sie vorher einen Fingerabdruck, die ersten vier Werte eines festen Satzes:
curl -s http://127.0.0.1:11434/api/embed -d '{"model":"embeddinggemma-2:270m-q8_0-text","input":"title: Marker | text: Restore-Test 4711"}' | jq -c '.embeddings[0][:4]'
BK=/srv/backup/embeddings
sudo mkdir -p "$BK"
docker compose stop
sudo tar czf "$BK/embeddings-$(date +%F).tar.gz" compose.yaml .env ollama models bewertung
docker compose start
Die Sicherung war im Test 1,5 GiB groß. Wiederherstellung auf einem leeren System:
mkdir -p ~/embeddings && cd ~/embeddings
sudo tar xzf /srv/backup/embeddings/embeddings-JJJJ-MM-TT.tar.gz
docker compose up -d
until curl -sfo /dev/null http://127.0.0.1:11434/; do sleep 2; done
docker compose exec ollama ollama list
Verifizieren: ollama list zeigt beide Modelle ohne neuen Download, und der Fingerabdruck stimmt mit dem Wert vor der Sicherung überein. Im Test waren nach gelöschten Ordnern alle drei Fingerabdrücke (beide Wege und bge-m3) identisch. Auch ein frischer Download von EmbeddingGemma 2 ergab denselben Wert.
Schritt 7: Ollama aktualisieren
Tragen Sie in der compose.yaml die neue Version ein. Die Modelle im Ordner ollama bleiben erhalten.
docker compose pull ollama
docker compose up -d ollama
docker compose exec ollama ollama --version
Im Test lief das Update von Ollama 0.40.3 auf 0.40.8, der Fingerabdruck von bge-m3 blieb identisch. Ändert er sich nach einem Update, berechnen Sie den Index neu.
Ein Wechsel von bge-m3 auf EmbeddingGemma 2 ist ein Neuaufbau (768 statt 1024 Werte, anderer Vektorraum): Im RAG-System mit Qdrant legen Sie eine neue Collection an und stellen erst danach die Abfrage um.
Verifizieren: Die Version zeigt die neue Nummer, ollama list die bisherigen Modelle, der Fingerabdruck ist unverändert.
Troubleshooting
| Symptom | Ursache und Lösung |
|---|---|
ollama pull bricht mit Exit-Code 1 ab, die Meldung endet mit https://ollama.com/download | Ollama ist älter als 0.40.4. Image-Tag erhöhen, docker compose pull ollama, neu starten. |
{"error":"model 'embeddinggemma-2:270m' not found"} | Modellname oder Tag stimmt nicht mit ollama list überein. Exakt das geladene Tag verwenden. |
llama.cpp startet nicht, im Log unknown model architecture: 'gemma-embedding2' | Image vor v0.7.0. Auf server-b11669 oder neuer wechseln. |
"message":"Invalid API Key" | Header Authorization: Bearer mit dem Schlüssel aus der .env fehlt oder ist falsch. |
input (10403 tokens) is too large to process. increase the physical batch size (current batch size: 4096) | Text länger als LLAMA_ARG_UBATCH, zusätzlich begrenzt der Kontext je Slot (hier 4096 Token). Texte vorher in Abschnitte teilen. |
{"error":"the input length exceeds the context length"} | Ollama mit "truncate": false und zu langem Text. Ohne diese Option kürzt Ollama still: Im Test kamen vom selben Text, den llama.cpp mit 10403 Token zählte, nur 2048 Token an, obwohl der Kontext auf 8192 stand. |
Häufige Fragen
Lohnt sich der Umstieg von bge-m3?
Bei der Trefferqualität nicht zwingend: Im Test lagen beide Modelle gleichauf (MRR 0,96 gegen 0,95 bei deutschen Fragen). Für EmbeddingGemma 2 sprechen das rund 1,8-fache Tempo beim Einlesen, 528 MB statt 1,2 GB im Arbeitsspeicher laut ollama ps und kürzere Vektoren. Messen Sie mit eigenen Texten, bevor Sie einen großen Bestand neu einlesen.
Brauche ich die Präfixe wirklich?
Laut Modellkarte funktioniert das Modell auch ohne, aber ungenauer. Im Test sank der MRR bei deutschen Fragen ohne Präfixe von 0,96 auf 0,91, eine Frage rutschte auf Rang 15.
Ollama oder llama.cpp?
Ollama verwaltet Modelle selbst und kürzt Vektoren über dimensions. llama.cpp bietet eine OpenAI-kompatible Schnittstelle mit API-Schlüssel und eine per Prüfsumme festgelegte Datei. Im Test lieferten beide identische Trefferwerte bei fast gleichem Tempo.
Kann ich auch Bilder und Audio einbetten?
Laut Ollama-Dokumentation nimmt /api/embed auch Bilder und Audio an, Video noch nicht, mit den größeren Tags 440m (Bild), 570m (Audio) und 740m (beides). llama.cpp braucht die zusätzliche mmproj-Datei.
Fazit
EmbeddingGemma 2 ist klein genug für jeden Server ohne Grafikkarte und durch Apache 2.0 rechtlich unkompliziert. Im Test traf es auf deutschen Texten so gut wie bge-m3, las rund 1,8-mal so schnell ein und verlor bei 256 Dimensionen wenig (MRR 0,94 statt 0,96). Mit bge-m3 im Bestand entscheidet die eigene Testmenge, der Neuaufbau des Index gehört in die Planung.
Weiterführende Anleitungen und Quellen
- Lokales RAG-System mit Qdrant und Embeddings
- Hindsight mit Docker: Gedächtnis für KI-Agenten
- Ollama und Open WebUI mit Docker
- Docker Compose unter Linux einrichten
- Docker Compose absichern: Secrets, Healthchecks, Non-Root
- Modellkarte EmbeddingGemma 2 (Hugging Face)
- Google: EmbeddingGemma Dokumentation
- Ollama: embeddinggemma-2
- Ollama 0.40.8 Release
- llama.cpp v0.7.0 Release Notes
- GGUF-Dateien von ggml-org
- Ollama: bge-m3


