4.5 KiB
Switching Immich Facial Recognition Models
How to switch between
antelopev2,buffalo_l, andbuffalo_sfacial recognition models — and the critical gotcha that WILL bite you.
When to switch
- antelopev2 → buffalo_l: Family members with similar faces are getting merged/confused. buffalo_l is significantly better at distinguishing similar faces (larger model, better embeddings).
- buffalo_l → buffalo_s: Running low on VRAM (buffalo_s is ~50MB vs buffalo_l's ~182MB). Trade quality for memory.
- Reverse (experimental): If buffalo_l somehow performs worse for your specific dataset (unlikely but possible).
Procedure
1. Set the model in .env
# /opt/immich/.env
MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL_NAME=buffalo_l
Valid values: antelopev2, buffalo_l, buffalo_s.
2. Restart the ML container
cd /opt/immich
sudo docker compose up -d immich-machine-learning
The new model downloads automatically on first use (~182 MB for buffalo_l).
To pre-cache and avoid the download-on-first-request delay, see
references/pre-caching-ml-models.md.
3. Check the model is in place
sudo docker exec immich_machine_learning find /cache/facial-recognition -name '*.onnx'
# Should show: /cache/facial-recognition/buffalo_l/detection/model.onnx
# /cache/facial-recognition/buffalo_l/recognition/model.onnx
🚨 CRITICAL PITFALL: Model switch does NOT re-process existing faces
Changing MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL_NAME in .env and
restarting the ML container only changes which model is loaded for FUTURE
face detection. It does NOT touch the existing face embeddings already
stored in the database.
Old antelopev2 embeddings remain and Immich will keep using them — meaning
face confusion persists until you explicitly re-run face detection.
The fix: Re-run face detection
From the UI (easiest):
- Go to Immich admin → Administration → Jobs → Face Detection
- Click All (NOT "Missing" — "Missing" only processes photos without ANY embedding, and your old antelopev2 photos already have embeddings)
- Wait — on GPU this takes 1-2 hours for ~23K photos
Via the API:
# Login
LOGIN=$(curl -s -X POST http://localhost:2283/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"your-password"}')
TOK=*** -e "console.log(JSON.parse(process.argv[1]).accessToken)" -- "$LOGIN")
# Trigger face detection with force=true to re-process all assets
curl -s -X PUT "http://localhost:2283/api/jobs/faceDetection" \
-H "Authorization: Bearer $TOK" \
-H "Content-Type: application/json" \
-d '{"command":"start","force":true}'
# Trigger facial recognition too
curl -s -X PUT "http://localhost:2283/api/jobs/facialRecognition" \
-H "Authorization: Bearer $TOK" \
-H "Content-Type: application/json" \
-d '{"command":"start","force":true}'
What happens after re-processing
- Old face clusters (People) remain but get new embeddings
- Faces that were incorrectly merged under
antelopev2will get separate clusters underbuffalo_l - You may need to manually clean up: remove wrong photos from a person's page in the UI (buffalo_l won't auto-unmerge what antelopev2 merged — it just won't merge new ones incorrectly)
How to know it worked
# ML container should show facial recognition activity
sudo docker logs immich_machine_learning --tail 20 | head
# GPU should be actively processing
nvidia-smi
# Expect: ~1200 MiB VRAM, 10-40% utilization
# Server logs should show face detection activity
sudo docker logs immich_server --tail 50 | grep -i 'detect\|face\|person'
Model comparison
| Model | Size | Quality | VRAM (loaded) | Best for |
|---|---|---|---|---|
| antelopev2 | ~100 MB | Moderate | ~100 MB | Low-resource, not picky about false merges |
| buffalo_l | ~182 MB | High | ~182 MB | Default choice — best accuracy, distinguishing similar faces |
| buffalo_s | ~50 MB | Lower | ~50 MB | Extreme VRAM constraints |
Internal ML URL
The ML container runs as immich_machine_learning on the Docker network.
The default URL Immich uses is:
http://immich-machine-learning:3003
This is auto-discovered via Docker DNS. If the "Machine Learning URL" field in Immich admin settings is blank, it's using this default — which is correct when ML runs on the same Docker host. Only fill it in when running ML on a separate machine.