Funk-lab/admin/incidents-llm-gpu.md
alkatrazz 6e6dd45ac5 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>
2026-06-19 16:29:17 +02:00

7.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.

Reste à faire (durci, mais pas zéro-risque)

Le heartbeat empêche la péremption par inactivité. Si llama-server se fige carrément (wedge ROCm), le heartbeat le détecte (échecs loggués) mais ne le répare pas encore. Évolution possible : sur N échecs consécutifs du heartbeat → restart auto de llama-server et litellm (watchdog actif). Pour l'instant : détection + alerte manuelle.