Funk-lab/admin/incidents-llm-gpu.md
ALI YESILKAYA 1b85d4ee65
feat(llama_server): heartbeat embed — garde chaud le slot nomic :1238 (#28)
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>
2026-06-19 22:17:23 +02:00

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

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.