Le grounding RAG du STT ratait par intermittence (docs=0 → réponses génériques) parce que l'instance d'embedding :1238 partait à froid après une pause : la 1ʳᵉ requête mettait 5-8 s et dépassait le timeout 4 s du STT-server (recall + RAG doc). llama-embed-heartbeat : service systemd local sur gpu-01 qui envoie un petit embed à :1238 toutes les 20 s → le slot reste résident/chaud → la 1ʳᵉ vraie requête est rapide → docs>0 fiable. Pendant du llm-heartbeat (slot chat :1234), pour le slot embed. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
Incidents LLM / GPU — llama-server (gpu-01)
Journal dédié des incidents d'inférence : llama-server (ROCm / RX 6700XT sur
gpu-01) et chaîne LLM STT-server / Hermes → LiteLLM → llama-server. Voir aussi
incidents.md (matériel & services storage-01/k8s) et admin/ia/llama_server.md.
2026-06-19 — llama-server gelé : /health OK mais inférence bloquée → 502 sur tout le LLM
Symptômes
- STT (HUD, composer texte) :
502 Server Error: Bad Gateway for url: http://stt.lab.local/v1/ask - Petite activité GPU visible sur gpu-01 (le slot d'inférence tourne en rond)
- STT-server :
GET /healthz→ 200, maisPOST /v1/ask→ 502 - llama-server
:1234:/health→{"status":"ok"}et/v1/models→qwen3-8blisté → l'apparence est « up », ce qui masque la panne - Mais une génération réelle ne renvoie jamais rien :
curl /v1/chat/completionshang (45 s sans un seul token) - LiteLLM (storage-01) :
litellm.Timeout: APITimeoutError - Request timed out. timeout value=600.0, time taken=600.0s. Received Model Group=hermes-default(après 2 retries)
Cause
Le llama-server (gpu-01, :1234, qwen3-8b) était dans un état wedgé après
~2 jours d'uptime (démarré le 2026-06-17 21:04). Les endpoints de contrôle
(/health, /v1/models) répondaient encore, mais le slot d'inférence ROCm était
bloqué (vraisemblablement une requête avortée laissée dans le slot). Chaîne du 502 :
HUD composer → STT-server (/v1/ask) → LiteLLM :4000 (hermes-default) → llama-server gpu-01 :1234 ← GELÉ
LiteLLM attend request_timeout = 600 s, échoue, et le STT-server renvoie 502.
Diagnostic (commandes utiles)
# 1) STT-server : confirmer que le 502 vient de lui (et pas de Traefik)
kubectl -n ai logs <pod stt-server> --tail=50 # → "POST /v1/ask HTTP/1.1" 502
# 2) LiteLLM : voir le timeout
ssh storage-01 'journalctl -u litellm -n 40 --no-pager --output=cat'
# → litellm.Timeout ... timeout value=600.0 ... Model Group=hermes-default
# 3) llama-server : /health MENT — tester une VRAIE génération
ssh gpu-01 'curl -s -m 30 http://127.0.0.1:1234/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{\"model\":\"qwen3-8b\",\"messages\":[{\"role\":\"user\",\"content\":\"Dis bonjour. /no_think\"}],\"max_tokens\":30}"'
# gelé (aucune sortie) = wedgé | réponse JSON = sain
Résolution
Redémarrer le service d'inférence sur gpu-01 :
ssh gpu-01 'sudo systemctl restart llama-server'
- L'ancienne instance gelée met ~40 s à mourir (état
deactivatingle temps que le slot bloqué se libère) —systemctl restartpeut dépasser un timeout SSH court, c'est normal ; vérifier avecsystemctl is-active llama-serverjusqu'àactive. - Au redémarrage,
/v1/chat/completionsrenvoie503 {"error":"Loading model"}pendant ~30 s (rechargement du modèle en VRAM) — normal, attendre. - Validation : génération directe (
"Bonjour."en ~80 ms) puis bout-en-boutPOST http://stt.lab.local/v1/ask→{"reply":"Bonjour.","model":"hermes-default"}.
Observations / suites (hors présent incident)
- ⚠️ Piège du health-check :
/healthet/v1/modelsrestent verts quand le serveur est wedgé → un check qui ne fait pas une vraie génération ne voit pas la panne (même angle mort que le crash-loop Qdrant enactivating, cf.incidents.md). - Latence cerveau : post-restart, 30-45 s pour une réponse triviale via STT-server
alors que le llama-server direct est à ~80 ms → le surcoût vient du pipeline du
cerveau (mémoire/embeddings Qdrant
stt-memory+ LiteLLM). À investiguer séparément. - Bruit LiteLLM :
Invalid model name model=qwen3-1.7b-monitor(profil monitor Hermes) dans les logs — pré-existant, non lié à cet incident.
Prévention (pistes)
- Probe applicative : sonde qui fait une vraie génération
/v1/chat/completions(pas seulement/health) → alerte si le LLM ne produit plus de tokens. - Watchdog : si LiteLLM accumule des
Timeoutsurhermes-default, redémarrer automatiquementllama-serversur gpu-01. - Réduire le
request_timeoutLiteLLM (600 s est très long → un 502 tardif et opaque).
2026-06-19 (suite) — burst de 502 « cold-start » : connexions keep-alive périmées (LiteLLM)
Symptôme
Après le déploiement du STT-server résilient (latence ramenée à ~340 ms en régime), un nouveau pod STT (ou une reprise de trafic après inactivité) voyait ses 1-2 premières requêtes hanger ~60 s puis 502, ensuite tout repartait. Reproductible à chaque rollout.
Cause
Connexions keep-alive périmées dans le pool LiteLLM → llama-server. Les redémarrages de
llama-server (cf. incident ci-dessus) tuent ses connexions, mais LiteLLM garde les
sockets morts dans son pool. La requête suivante part sur un socket mort → attend dans le
vide jusqu'au timeout. Or litellm_settings.request_timeout valait 600 s → LiteLLM
pouvait hanger 10 min sans rien logguer (d'où « LiteLLM ne logge aucune erreur » pendant
la fenêtre de 502 : il attendait encore). Le STT, lui, abandonne avant (timeout par
tentative) et renvoie 502.
Diagnostic clé : llama-server direct est stable (10/10 à 0,32 s) ; le hang est dans la couche LiteLLM↔llama-server, pas dans llama-server ni le code STT.
Correctifs
- Côté STT-server (déployé, v0.2.1) : 2 tentatives avec fermeture du client httpx entre les essais (connexion neuve) + timeout 30 s/tentative. Tolère un blip, mais ne couvre pas un hang > 60 s.
- Côté LiteLLM (
ansible/roles/litellm/templates/config.yaml.j2) :
Appliquer :litellm_settings: request_timeout: 60 # était 600 — borne le hang d'une connexion morte num_retries: 2 # rejoue → évince le socket périmé, repart sur une connexion neuveansible-playbook -i inventory.yml playbooks/storage-01.yml --tags litellm(⚠️ redémarre LiteLLM — service partagé Hermes/n8n/Open WebUI/Ghostfolio). - Purge immédiate (sans attendre l'IaC) :
sudo systemctl restart litellmvide le pool.
Fix racine — heartbeat (déployé)
Le timeout/retry ne fait que borner la casse : la 1ʳᵉ demande après une pause restait lente (la connexion keep-alive devient inactive → llama-server la ferme → LiteLLM garde le socket mort). La vraie cause = la connexion qui devient périmée pendant l'inactivité.
Solution : llm-heartbeat (rôle litellm) — un service systemd qui appelle hermes-default
toutes les llm_heartbeat_interval s (défaut 15 s, max_tokens:1, /no_think → ~10 ms GPU).
La connexion LiteLLM↔llama-server n'est jamais inactive → jamais périmée → la 1ʳᵉ vraie
demande tombe toujours sur un tuyau vivant. Logge les échecs (journalctl -u llm-heartbeat),
donc sert aussi de sonde (génération réelle, pas juste /health).
ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags litellm # déploie heartbeat + config
systemctl status llm-heartbeat ; journalctl -u llm-heartbeat -f # vérifier
⚠️ Le heartbeat appelle hermes-default en continu → si on bascule cet alias sur Claude
(API facturée), mettre llm_heartbeat_enabled: false.
Fix racine #2 — watchdog gpu-01 (déployé)
Le heartbeat empêche la péremption par inactivité, mais si llama-server se fige
carrément (wedge ROCm), il le détecte sans le réparer. C'est désormais couvert par le
llama-watchdog (rôle llama_server) : un service systemd local sur gpu-01 qui sonde une
vraie génération sur :1234 toutes les llama_watchdog_interval s (défaut 20 s,
max_tokens:1, /no_think). Sur llama_watchdog_failures échecs consécutifs (défaut 3) →
systemctl restart llama-server en local (tourne en root, ni SSH ni sudo distant), puis
cooldown jusqu'à ce que la génération remarche (gère le 503 "Loading model" du rechargement
VRAM sans le compter comme échec).
ansible-playbook -i inventory.yml playbooks/gpu-01.yml --tags llama_server # déploie le watchdog
systemctl status llama-watchdog ; journalctl -u llama-watchdog -f # vérifier
Pourquoi sur gpu-01 (et pas piloté depuis storage-01 via le heartbeat) : le restart est local → pas de SSH/sudo inter-hôtes, répare même si LiteLLM ou le réseau cluster est en vrac, et sonde au plus près du serveur figé.
Défense en profondeur (3 couches) :
llm-heartbeat(storage-01, rôlelitellm) — garde la connexion chaude (anti-péremption).request_timeout: 20+num_retries: 2(LiteLLM) + retry STT-server — bornent le blip.llama-watchdog(gpu-01, rôlellama_server) — répare le wedge ROCm à la source.
2026-06-19 (suite) — slot d'embedding :1238 froid → RAG du STT raté (docs=0)
Symptôme
Après déploiement du grounding RAG du STT-server (le cerveau interroge funk-docs), les
réponses sur le homelab étaient intermittentes : parfois ancrées dans la doc, parfois
génériques (« serveur XMPP », « worker nodes »…). Log du pod stt-server :
ask … recall=5471ms … docs=6 ← embed OK → 6 extraits doc injectés → réponse ancrée
ask … recall=8330ms … docs=0 ← embed à 8 s → timeout → 0 extrait → réponse générique
Cause
L'instance d'embedding dédiée (nomic-embed-text, gpu-01 :1238) part à froid après une
pause : le slot GPU devient inactif, la 1ʳᵉ requête met 5-8 s (réchauffe + VRAM). Or le
STT-server borne le recall mémoire et la recherche doc à 4 s (chemin de réponse). Au-delà :
recall renvoie ([], None) → knowledge.search ré-embed (2ᵉ timeout) → docs=0 → le cerveau
répond sans contexte. Même famille que le wedge chat, sur l'autre slot GPU — mais ici pas de
freeze, juste une latence à froid. Le slot chat :1234 est gardé chaud par llm-heartbeat,
l'embed :1238 ne l'était pas.
Correctif — heartbeat embed (rôle llama_server)
llama-embed-heartbeat : service systemd local sur gpu-01 qui envoie un petit embed à :1238
toutes les llama_embed_heartbeat_interval s (défaut 20). Le slot reste résident/chaud → la
1ʳᵉ vraie requête est rapide → recall + RAG doc tiennent dans les 4 s → docs>0 fiable.
ansible-playbook -i inventory.yml playbooks/gpu-01.yml --tags llama_server # déploie le heartbeat embed
systemctl status llama-embed-heartbeat ; journalctl -u llama-embed-heartbeat -f
Variante moins bonne envisagée : remonter
STT_DOCS_TIMEOUT/recallà ~8 s → couvre le froid mais ajoute la latence à chaque requête froide. Le warming l'élimine à la source.