Problem Description
The RAG (Retrieval‑Augmented Generation) service deployed on a Kubernetes cluster began returning vector dimension mismatch errors after a routine rolling update. The symptoms observed across multiple pods were:
- FAISS index load failures:
ValueError: Expected embedding dimension 768 but got 1024 - Cosine‑similarity runtime errors:
RuntimeError: shape mismatch for dot product: (batch, 768) vs (batch, 1024) - Intermittent
faiss.IndexFlatL2: dimension mismatchlogs during batch query processing - Overall retrieval precision dropped from ~92 % to < 60 % as measured by downstream QA tests
All pods are stateless inference containers that mount a persistent volume holding the vector database (FAISS, Chroma, or Milvus). The deployment controller uses the imagePullPolicy: Always setting and the Docker image tag myorg/embedding-service:latest.
Root Cause Analysis
The mismatch originates from three tightly coupled factors:
- Unpinned model image tag – Docker’s Tagging and versioning images guidance recommends pinning immutable tags to avoid accidental drift. Using
:latestallowed the CI pipeline to push a new checkpoint (a 1024‑dimensional BERT model) while the existing index was built with a 768‑dimensional SBERT checkpoint. - Rolling update without index migration – Kubernetes performed a rolling update, pulling the new image on a subset of pods. Because the vector store resides on a persistent volume, the newly started pods began serving queries with the 1024‑dim model against an index still containing 768‑dim vectors, triggering the FAISS and HNSW errors reported in the logs.
- Image build non‑determinism – The Dockerfile relied on
ADDto copy the model directory from a remote URL at build time. According to Dockerfile reference,COPYshould be used for deterministic artifacts; otherwise the image can embed a different checkpoint each build, making it impossible to guarantee dimensionality consistency.
These points are echoed in community reports: the LangChain issue #8421 and Milvus issue #12634 both describe identical failures after a model upgrade, and the Haystack discussion #3849 highlights mixed‑version pods causing cosine‑similarity failures.
Investigation and Debugging
Below is a reproducible debugging workflow that was used to pinpoint the root cause.
1. Gather pod logs
kubectl logs -l app=rag-embedding -c inference --tail=100 | grep -i "dimension"
Sample output:
2026-07-28T14:12:03.112Z INFO Loading model from /app/model.pt – unexpected hidden size 1024 (expected 768)
2026-07-28T14:12:04.237Z ERROR faiss.IndexFlatL2: dimension mismatch (expected 768, got 1024)
2026-07-28T14:12:04.239Z TRACE Query embedding shape: torch.Size([32, 1024])
2026-07-28T14:12:04.240Z TRACE Index embedding shape: torch.Size([?, 768])
2. Inspect the Docker image tag used by each pod
kubectl get pods -l app=rag-embedding -o jsonpath="{range .items[*]}{.metadata.name}{'\\t'}{.spec.containers[0].image}{'\\n'}{end}"
Result showed a mix of myorg/embedding-service:latest and myorg/embedding-service:2024.07.15 after the update.
3. Verify model dimensions directly inside a running container
kubectl exec -it $(kubectl get pod -l app=rag-embedding -o name | head -n1) -- bash -c "
python - <<'PY'
import torch, transformers
model = torch.load('/app/model.pt')
print('hidden size:', model.config.hidden_size)
PY"
Output:
hidden size: 1024
4. Check the index metadata on the persistent volume
kubectl exec -it $(kubectl get pod -l app=vector-db -o name | head -n1) -- bash -c "
python - <<'PY'
import faiss, json, os
index_path = '/data/faiss.index'
if os.path.exists(index_path):
idx = faiss.read_index(index_path)
print('index dimension:', idx.d)
else:
print('index not found')
PY"
Output:
index dimension: 768
5. Confirm image build process
cat Dockerfile
# Bad example from the broken image
FROM python:3.11-slim
ADD https://model-repo.com/checkpoints/bert_latest.pt /app/model.pt
RUN pip install -r requirements.txt
The ADD instruction pulls the latest checkpoint at build time, violating deterministic builds.
Resolution
The fix consists of three coordinated actions: enforce immutable image tags, make the Docker build deterministic, and re‑index the vector store to match the new model (or downgrade the model to the previous dimension).
1. Pin the model image tag
Update the deployment manifest to reference a specific tag and set imagePullPolicy: IfNotPresent to avoid pulling unintended versions.
# Before (broken)
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-embedding
spec:
replicas: 3
selector:
matchLabels:
app: rag-embedding
template:
metadata:
labels:
app: rag-embedding
spec:
containers:
- name: inference
image: myorg/embedding-service:latest
imagePullPolicy: Always
volumeMounts:
- name: vectordb
mountPath: /data
volumes:
- name: vectordb
persistentVolumeClaim:
claimName: vectordb-pvc
# After (fixed)
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-embedding
spec:
replicas: 3
selector:
matchLabels:
app: rag-embedding
template:
metadata:
labels:
app: rag-embedding
spec:
containers:
- name: inference
image: myorg/embedding-service:2024.07.15 # immutable tag
imagePullPolicy: IfNotPresent
volumeMounts:
- name: vectordb
mountPath: /data
volumes:
- name: vectordb
persistentVolumeClaim:
claimName: vectordb-pvc
2. Make the Docker build deterministic
Replace ADD with COPY and bake the exact checkpoint into the image during CI.
# Dockerfile (recommended)
FROM python:3.11-slim
# Copy a specific, version‑controlled model artifact
COPY models/bert_sbert_768.pt /app/model.pt
RUN pip install -r requirements.txt
The models/bert_sbert_768.pt file should be stored in the repository (or a version‑controlled artifact store) and referenced by a hash in CI to guarantee consistency.
3. Re‑index or migrate the vector database
If the new model is intentionally larger (e.g., 1024‑dim), the existing index must be rebuilt. Otherwise, revert to the previous model and keep the existing index.
- Option A – Re‑index with new dimensions
# Re‑index script (run once) python index_documents.py \ --model /app/model.pt \ --output /data/faiss.index \ --dimension 1024 - Option B – Downgrade model to match existing index
Replace the model artifact with the 768‑dim checkpoint and redeploy without rebuilding the index.
Verification
After applying the fixes, perform the following checks:
- Pod image consistency
kubectl get pods -l app=rag-embedding -o jsonpath="{.items[*].spec.containers[0].image}" | tr ' ' '\n' | sort | uniqExpected output: a single line with
myorg/embedding-service:2024.07.15. - Index load test
kubectl exec -it $(kubectl get pod -l app=vector-db -o name | head -n1) -- bash -c " python - <<'PY' import faiss, numpy as np idx = faiss.read_index('/data/faiss.index') print('Loaded index dim:', idx.d) # Dummy query vector matching the model dimension q = np.random.random((1, idx.d)).astype('float32') D, I = idx.search(q, k=5) print('Search succeeded, distances:', D) PY"Should print
Loaded index dim: 1024(or 768, depending on chosen option) and no dimension‑mismatch errors. - End‑to‑end query test
curl -X POST http://rag-embedding.default.svc.cluster.local/query \ -H "Content-Type: application/json" \ -d '{"question":"What is the capital of France?"}' | jq .answerAnswer should be correct and latency within expected SLA.
- Monitoring alerts
Confirm that the alert
VectorDimensionMismatch(if configured) is in aOKstate in Prometheus/Grafana.
Operational Best Practices and Prevention
| Practice | Why it matters | Implementation tip |
|---|---|---|
| Immutable image tags | Prevents accidental drift when latest moves. |
Use CI to generate tags like v2024.07.15‑sha1abc123 and reference them in manifests. |
| Deterministic Docker builds | Ensures the same checkpoint is baked every time. | Prefer COPY from a version‑controlled directory; avoid ADD from remote URLs. |
| Schema versioning for vector stores | Detects dimension mismatches before runtime. | Store dimension as a metadata field; reject queries if mismatch detected. |
| Rolling‑update coordination | Guarantees all pods run the same model before traffic resumes. | Use maxUnavailable: 0 and a pre‑hook that validates the new model dimension against the stored index. |
| Automated re‑indexing pipeline | Reduces manual effort when model dimensions change. | Trigger a re‑index job in CI when a model version with a new hidden_size is published. |
FAQ
- Why does the error appear only after a rolling update and not during the initial deployment?
Because the initial deployment used the same model version that built the index. The rolling update introduced a newer model image while the persistent index remained at the old dimension, causing the mismatch only when the new pods started handling queries. - Can I keep using
imagePullPolicy: Alwayssafely?
Yes, but only if the tag you pull is immutable (e.g.,v2024.07.15) and the CI pipeline guarantees that the artifact behind that tag never changes. Otherwise,IfNotPresentwith pinned tags is the safer default. - How do I detect a dimension mismatch before it crashes the service?
Add a startup health check that loads the vector store, readsindex.d, and compares it tomodel.config.hidden_size. Fail the pod if they differ, allowing the orchestrator to roll back. - Is there a way to migrate an existing FAISS index to a new dimension without full re‑indexing?
FAISS does not support changing the dimensionality of an existing index. The only reliable path is to rebuild the index with embeddings generated from the new model, optionally using a background re‑index job to avoid downtime. - What monitoring metric should I alert on to catch this early?
Expose a gauge such asvector_store_dimension{model="bert_sbert_768"} = 768and another gauge for the loaded model dimension. Alert when the two differ, e.g.,vector_store_dimension != model_embedding_dimension.
Related Topic Hub: Distributed Systems Troubleshooting Hub