Funk-lab/admin/incidents-llm-gpu.md
ALI YESILKAYA 577d61ebaa
feat: anti-502 LLM — heartbeat (litellm) + watchdog auto-réparation (llama_server) (#23)
* feat(litellm): heartbeat anti-502 + request_timeout 20s

Cause racine du 502 « 1ʳᵉ demande après une pause » : la connexion keep-alive
LiteLLM↔llama-server devient inactive → llama-server la ferme → LiteLLM garde le
socket mort → la requête suivante part dans le vide → timeout → 502.

- llm-heartbeat : service systemd qui appelle hermes-default toutes les 15s
  (max_tokens:1, /no_think → ~10ms GPU) → la connexion n'est jamais inactive,
  jamais périmée. Logge les échecs → sert aussi de sonde (vraie génération).
- request_timeout 60→20s : un socket périmé échoue vite, dans la fenêtre où
  num_retries:2 peut rejouer sur une connexion neuve (sinon le client abandonnait
  avant le retry → 502 sec).
- Doc : admin/incidents-llm-gpu.md (fix racine) + README rôle.

⚠️ heartbeat appelle hermes-default en continu → si bascule sur Claude (facturé),
mettre llm_heartbeat_enabled: false. request_timeout global → remonter si Claude.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(llama_server): watchdog auto-réparation du wedge ROCm

Service systemd local sur gpu-01 qui sonde une vraie génération sur :1234
(pas juste /health, qui ment quand le slot d'inférence est figé). Sur N
échecs consécutifs → systemctl restart llama-server en local (root, sans
SSH/sudo distant). Gère le 503 "Loading model" post-restart sans le compter
comme échec.

Complète le llm-heartbeat (rôle litellm) : le heartbeat empêche la péremption
par inactivité de la connexion ; le watchdog répare le figeage du serveur lui-même.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:52:29 +02:00

8.7 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, mais POST /v1/ask502
  • llama-server :1234 : /health{"status":"ok"} et /v1/modelsqwen3-8b listé → l'apparence est « up », ce qui masque la panne
  • Mais une génération réelle ne renvoie jamais rien : curl /v1/chat/completions hang (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 deactivating le temps que le slot bloqué se libère) — systemctl restart peut dépasser un timeout SSH court, c'est normal ; vérifier avec systemctl is-active llama-server jusqu'à active.
  • Au redémarrage, /v1/chat/completions renvoie 503 {"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-bout POST http://stt.lab.local/v1/ask{"reply":"Bonjour.","model":"hermes-default"}.

Observations / suites (hors présent incident)

  • ⚠️ Piège du health-check : /health et /v1/models restent 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 en activating, 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 Timeout sur hermes-default, redémarrer automatiquement llama-server sur gpu-01.
  • Réduire le request_timeout LiteLLM (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

  1. 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.
  2. Côté LiteLLM (ansible/roles/litellm/templates/config.yaml.j2) :
    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 neuve
    
    Appliquer : ansible-playbook -i inventory.yml playbooks/storage-01.yml --tags litellm (⚠️ redémarre LiteLLM — service partagé Hermes/n8n/Open WebUI/Ghostfolio).
  3. Purge immédiate (sans attendre l'IaC) : sudo systemctl restart litellm vide 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) :

  1. llm-heartbeat (storage-01, rôle litellm) — garde la connexion chaude (anti-péremption).
  2. request_timeout: 20 + num_retries: 2 (LiteLLM) + retry STT-server — bornent le blip.
  3. llama-watchdog (gpu-01, rôle llama_server) — répare le wedge ROCm à la source.