Problem Description
During a Kubernetes rolling update of a TorchServe‑hosted Retrieval‑Augmented Generation (RAG) service, the RAGRetriever began returning an empty list of documents for every query. The symptom manifested as log entries such as:
INFO 2026-09-21 12:34:56,789 RAGRetriever: No documents retrieved for query
ERROR 2026-09-21 12:34:57,102 faiss index not found at path /model/index.faiss
WARNING 2026-09-21 12:34:58,210 torchserve model version 3 loaded but index file missing, falling back to empty retrieval
Clients observed a sudden drop in answer relevance, while the generation head of the RAG pipeline continued to produce text (often hallucinating) because the retrieval step yielded no context.
Root Cause Analysis
The empty retrieval results stem from a combination of three intertwined factors that become visible during a rolling update:
- FAISS index not available at model start‑up – TorchServe loads the model and its associated
RAGRetrieverbefore the persistent volume that holdsindex.faissis mounted. This mirrors the incident where a Kubernetes rolling update restarted pods before the FAISS index volume was attached, causing a 15‑minute outage. - Retriever cache cleared on model hot‑reload – According to the Hugging Face RAG tutorial, the
RAGRetrievercaches the loaded index in memory. When TorchServe performs a hot‑swap of model weights (as described in the TorchServe model versioning guide), the in‑memory cache is invalidated. If the index cannot be reloaded, the retriever falls back to an empty list. - Mismatch between tokenizer and index embeddings – In a separate production incident on AWS ECS, a new model version shipped with a different tokenizer version, producing out‑of‑range query embeddings that FAISS could not match. While this was not the primary cause in the current scenario, the same symptom (empty results) appears, and the logs do not differentiate the underlying mismatch.
- Prometheus metric
rag_retriever_documents_totalshould increase with each query. - Latency histogram
rag_inference_secondsshould show a modest increase (≈10 ms) for the retrieval step, confirming the index is being consulted. - Mount readiness probes: Use a
readinessProbethat checks for/model/index.faissexistence before marking the pod ready. - Version‑locked index artifacts: Store the FAISS index in a version‑specific sub‑directory (e.g.,
/model/v3/index.faiss) and reference it via an environment variable. This prevents accidental reuse of an outdated index. - Graceful rolling update strategy: Set
maxUnavailable: 0andmaxSurge: 1to ensure at least one healthy replica serves traffic while a new pod initializes its index. - Health check endpoint: Expose
/healthzthat runs a lightweight retrieval against a known query and returns HTTP 200 only iflen(results) > 0. - Monitoring alerts: Trigger an alert when the log pattern “No documents retrieved for query” exceeds a threshold (e.g., >5 per minute) or when
rag_retriever_documents_totalplateaus. - Why does the RAG pipeline work locally but return empty results after a hot‑reload?
Because the hot‑reload clears the in‑memory FAISS index. If the index file is not yet mounted or fails to load (e.g., due to OOM), the retriever falls back to an empty list. - Can I reuse the same FAISS index across different model versions?
Only if the embedding dimension and tokenizer vocabulary are identical. A mismatch leads to out‑of‑range vectors and zero matches. - What should I do if I see “CUDA out of memory while loading retriever index”?
Load the index on CPU first (setdevice='cpu'in the retriever constructor), then move the model to GPU after the index is built, or increase the pod’s GPU memory limit. - Is there a way to force TorchServe to wait for the index before loading the model?
Yes, use aninitContainerthat blocks until the index file appears, as shown in the resolution section. - How can I verify which FAISS index file a running pod is using?
Executecat /model/index_path.txt(if you store the path in a file) or inspect the environment variableRAG_RETRIEVER_CACHEinside the container.
Official PyTorch Distributed Data Parallel (DDP) documentation emphasizes that model state loading must be coordinated with external resources during rolling updates (DDP guidelines). Failure to synchronize index availability violates that recommendation.
Investigation and Debugging Steps
1. Verify index presence on pod start‑up
kubectl exec -it rag-service-5c9d7b9f6f-abcde -- ls -l /model/
total 8240
-rw-r--r-- 1 root root 8423424 Jan 12 08:15 index.faiss
-rw-r--r-- 1 root root 123456 Jan 12 08:15 config.json
If the file is missing, the retriever will log “faiss index not found”.
2. Inspect TorchServe model loading logs
2026-09-21 12:34:55,876 INFO [ModelLoader] Loading model version 3 from /model/
2026-09-21 12:34:56,001 INFO [RAGRetriever] Initializing FAISS index from /model/index.faiss
2026-09-21 12:34:56,102 ERROR [RAGRetriever] faiss index not found at path /model/index.faiss
2026-09-21 12:34:56,103 INFO [RAGRetriever] Falling back to empty retrieval
3. Check volume mount lifecycle
kubectl describe pod rag-service-5c9d7b9f6f-abcde
...
Volumes:
rag-index:
Type: PersistentVolumeClaim (claim "rag-index-pvc")
Capacity: 10Gi
Access Modes: RWO
Mounted By: rag-service
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulling 2m kubelet, node1 Pulling image "pytorch/torchserve:latest"
Normal Pulled 2m kubelet, node1 Successfully pulled image
Normal Created 2m kubelet, node1 Created container rag-service
Normal Started 2m kubelet, node1 Started container rag-service
Warning VolumeMountFailed 2m kubelet, node1 Volume rag-index not mounted: mount failed: permission denied
The warning indicates the mount failure that leads to the missing index.
4. Reproduce the empty‑retrieval condition locally
python -c "
from transformers import RagTokenizer, RagRetriever, RagSequenceForGeneration
tokenizer = RagTokenizer.from_pretrained('facebook/rag-token-nq')
retriever = RagRetriever.from_pretrained('facebook/rag-token-nq', index_name='custom', passages_path='/tmp/missing')
print(retriever.retrieve('What is the capital of France?'))
"
# Expected output: list of passages
# Actual output: []
5. Confirm tokenizer‑index compatibility
python - <<'PY'
from transformers import RagTokenizer, RagRetriever
tok_v1 = RagTokenizer.from_pretrained('facebook/rag-token-nq')
retriever_v1 = RagRetriever.from_pretrained('facebook/rag-token-nq', index_name='custom')
emb_v1 = tok_v1.encode('test query', return_tensors='pt')
print('Embedding dim:', emb_v1.shape[-1]) # e.g., 768
PY
If the new model version reports a different embedding dimension (e.g., 1024), FAISS built with 768‑dim vectors will reject the query, yielding empty results.
Resolution
Step 1 – Ensure index volume is ready before model loading
Modify the deployment manifest to add an initContainer that blocks until the index file is present.
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-service
spec:
replicas: 3
template:
spec:
initContainers:
- name: wait-for-index
image: busybox
command: ['sh', '-c', 'while [ ! -f /model/index.faiss ]; do echo "Waiting for FAISS index..."; sleep 2; done']
volumeMounts:
- name: rag-index
mountPath: /model
containers:
- name: rag-service
image: pytorch/torchserve:latest
env:
- name: TORCHSERVE_MODEL_DIR
value: /model
volumeMounts:
- name: rag-index
mountPath: /model
volumes:
- name: rag-index
persistentVolumeClaim:
claimName: rag-index-pvc
This guarantees that TorchServe’s model loader sees a valid index.faiss before invoking RAGRetriever.
Step 2 – Persist the retriever cache across hot‑reloads
Configure TorchServe to reuse the same model_store directory for the retriever index and disable automatic cache clearing:
# torchserve.properties
model_store=/model_store
default_workers_per_model=2
load_models_on_startup=true
# Custom environment variable read by the RAG wrapper
RAG_RETRIEVER_CACHE=/model_store/rag_cache
In the custom model handler, add:
if os.path.isdir(os.getenv('RAG_RETRIEVER_CACHE')):
retriever = RagRetriever.from_pretrained(
model_name_or_path,
index_name='custom',
passages_path=os.getenv('RAG_RETRIEVER_CACHE')
)
else:
# Fallback to loading from /model/index.faiss
retriever = RagRetriever.from_pretrained(...)
Step 3 – Align tokenizer and index embedding dimensions
When a new model version is built, regenerate the FAISS index with the same embedding size:
python - <<'PY'
from transformers import RagTokenizer, RagRetriever
import faiss, numpy as np
tokenizer = RagTokenizer.from_pretrained('facebook/rag-token-nq')
retriever = RagRetriever.from_pretrained('facebook/rag-token-nq')
emb_dim = retriever.question_encoder.config.hidden_size # e.g., 768
# Assume `documents` is a list of raw texts
doc_embeddings = []
for doc in documents:
inputs = tokenizer(doc, return_tensors='pt')
with torch.no_grad():
emb = retriever.question_encoder(**inputs).pooler_output
doc_embeddings.append(emb.squeeze().cpu().numpy())
index = faiss.IndexFlatIP(emb_dim)
index.add(np.stack(doc_embeddings))
faiss.write_index(index, '/model/index.faiss')
PY
Deploy the regenerated index alongside the new model version.
Verification
Functional test after rollout
curl -X POST http://rag-service.example.com/predictions/rag -d '{"inputs":"Who invented the telephone?"}'
Expected response snippet (excerpt):
{
"generated_text": "Alexander Graham Bell invented the telephone..."
}
Check the logs for a successful retrieval entry:
INFO 2026-09-21 13:02:10,453 RAGRetriever: Retrieved 5 documents for query
Metrics validation
Operational Best Practices & Prevention
Related Topic Hub: Model Serving Troubleshooting Hub