k3-a40-bootstrap / chien.py
patdev's picture
chien: images 4 + KV 14,5 GiB, identique au bootstrap
d04d9dd verified
Raw History Blame Contribute Delete
7.43 kB
# Chien de garde du pont et du moteur.
#
# POURQUOI. Trois fois le 27/08, le pont a cesse de servir alors que le moteur
# allait parfaitement bien. Signature invariable :
#
# /v1/models 200 <- servi localement, sans appel amont
# /v1/messages 500 en ~10 s <- pool=10.0 de httpx
# 127.0.0.1:8000 200 en 0,1 s <- LE MOTEUR VA BIEN
#
# La cause n'est PAS un plafond de temps : avec VL_TIMEOUT=240 la panne
# revient, et au moment du blocage il y avait **zero socket** ouvert vers le
# port 8000. Ce ne sont donc pas des connexions vivantes qui saturent le
# bassin, ce sont des PLACES comptabilisees et jamais rendues -- des flux
# abandonnes par le client dont le slot httpx n'est pas libere. La fuite est
# definitive par flux, pas temporelle.
#
# Tant que le correctif de code n'est pas ecrit et eprouve, ce chien convertit
# une panne silencieuse de plusieurs heures en une coupure de quelques secondes.
#
# IL NE SONDE JAMAIS /v1/models : c'est precisement l'endpoint qui ment.
import json
import os
import subprocess
import time
import urllib.error
import urllib.request
PONT = "http://127.0.0.1:8080"
MOTEUR = "http://127.0.0.1:8000"
JOURNAL = "/travail/chien.log"
PERIODE = 60
ECHECS_AVANT_ACTION = 2 # deux passes de suite : on ne relance pas sur un hoquet
ENV_PONT = {
"VL_UPSTREAM": "http://127.0.0.1:8000",
"VL_SERVED_NAME": "flashnext",
"VL_REAL_MODEL": "RadixArk/Qwen3.8-Flash-Next-NVFP4",
"VL_MAX_OUTPUT": "32768",
"VL_TIMEOUT": "240",
}
MOTEUR_CMD = [
"vllm", "serve", "RadixArk/Qwen3.8-Flash-Next-NVFP4",
"--served-model-name", "flashnext", "--host", "127.0.0.1", "--port", "8000",
"--max-model-len", "262144", "--max-num-seqs", "16",
"--gpu-memory-utilization", "0.93",
"--kv-cache-memory-bytes", "15569256448", # 14,5 GiB : marge pour l'encodeur d'images
"--distributed-executor-backend", "mp",
"--reasoning-parser", "qwen3", "--tool-call-parser", "qwen3_coder",
# Le plafond a 0 rendait un 400 sur toute capture collee dans Claude Code
# ("At most 0 image(s) may be provided..."). Il doit rester identique a
# celui de lancer_flashnext.sh : sinon le chien "repare" en remettant les
# images hors service, et la panne revient sans que rien ne l'explique.
"--limit-mm-per-prompt", '{"image":4,"video":0}',
"--trust-remote-code", "--enable-prefix-caching", "--enable-auto-tool-choice",
"--enable-flashinfer-autotune", "--enable-prompt-tokens-details",
]
def dire(m):
l = "[%s] %s" % (time.strftime("%H:%M:%S"), m)
print(l, flush=True)
try:
with open(JOURNAL, "a") as f:
f.write(l + "\n")
except Exception:
pass
def poste(url, corps, delai):
"""Rend (ok, detail). Une VRAIE generation, jamais /v1/models."""
try:
r = urllib.request.Request(
url, data=json.dumps(corps).encode(),
headers={"content-type": "application/json",
"anthropic-version": "2023-06-01", "x-api-key": "x"})
t = time.time()
d = json.loads(urllib.request.urlopen(r, timeout=delai).read().decode())
return True, "%.2fs" % (time.time() - t)
except urllib.error.HTTPError as e:
return False, "HTTP %d" % e.code
except Exception as e:
return False, repr(e)[:90]
def pont_vivant():
return poste(PONT + "/v1/messages",
{"model": "flashnext", "max_tokens": 8,
"thinking": {"type": "disabled"},
"messages": [{"role": "user", "content": "ping"}]}, 45)
def moteur_vivant():
return poste(MOTEUR + "/v1/completions",
{"model": "flashnext", "prompt": "ping", "max_tokens": 4,
"temperature": 0}, 45)
def tuer(motif):
out = subprocess.run(["ps", "-eo", "pid,rss,comm", "--no-headers"],
capture_output=True, text=True).stdout
for l in out.splitlines():
p = l.split(None, 2)
if len(p) != 3:
continue
pid, rss, comm = int(p[0]), int(p[1]), p[2].strip()
import re as _re
if _re.match(motif, comm) or (rss > 10_000_000 and _re.match(r"^python3?$", comm)):
try:
os.kill(pid, 9)
except Exception:
pass
def relancer_pont():
"""Le pont ne se relance JAMAIS depuis /proc/PID/cmdline : l'environnement
n'y figure pas, et le defaut de VL_UPSTREAM est le port du pont lui-meme --
il se parlerait a lui-meme. L'env est donc explicite ici."""
p = subprocess.run(["pgrep", "-f", "uvicorn anthropic_proxy"],
capture_output=True, text=True).stdout.split()
for pid in p:
try:
os.kill(int(pid), 9)
except Exception:
pass
time.sleep(3)
env = dict(os.environ)
env.update(ENV_PONT)
with open("/travail/pont.log", "a") as s:
subprocess.Popen(["python3", "-m", "uvicorn", "anthropic_proxy:app",
"--host", "0.0.0.0", "--port", "8080",
"--log-level", "warning"],
cwd="/travail", stdout=s, stderr=subprocess.STDOUT,
stdin=subprocess.DEVNULL, start_new_session=True, env=env)
for _ in range(20):
time.sleep(3)
ok, d = pont_vivant()
if ok:
return True, d
return False, "pas de generation apres 60 s"
def relancer_moteur():
tuer(r"^(vllm|VLLM|Ple|EngineCore)")
time.sleep(12)
if os.path.exists("/travail/vllm.log"):
os.replace("/travail/vllm.log", "/travail/vllm-chien-precedent.log")
with open("/travail/vllm.log", "w") as s:
subprocess.Popen(MOTEUR_CMD, stdout=s, stderr=subprocess.STDOUT,
stdin=subprocess.DEVNULL, start_new_session=True,
env=dict(os.environ, VLLM_PLE_CPU_OFFLOAD="1"))
for _ in range(45):
time.sleep(20)
ok, d = moteur_vivant()
if ok:
return True, d
return False, "moteur pas pret en 15 min"
dire("chien de garde arme : generation reelle toutes les %d s, action apres "
"%d echecs consecutifs" % (PERIODE, ECHECS_AVANT_ACTION))
echecs, reparations, tours = 0, 0, 0
while True:
time.sleep(PERIODE)
tours += 1
ok, detail = pont_vivant()
if ok:
if echecs:
dire("retabli tout seul apres %d echec(s)" % echecs)
echecs = 0
if tours % 15 == 0:
dire("ok (%s) | %d reparation(s) depuis l'armement" % (detail, reparations))
continue
echecs += 1
dire("pont KO (%s) -- echec %d/%d" % (detail, echecs, ECHECS_AVANT_ACTION))
if echecs < ECHECS_AVANT_ACTION:
continue
# On distingue TOUJOURS les deux couches avant d'agir : trois fois cette
# nuit, le moteur allait bien et j'ai failli le tuer pour rien.
mok, mdetail = moteur_vivant()
if mok:
dire("le moteur va bien (%s) -> c'est le pont : relance du pont seul" % mdetail)
r, d = relancer_pont()
dire(" pont : %s (%s)" % ("REPARE" if r else "ECHEC", d))
else:
dire("le moteur aussi est KO (%s) -> relance moteur puis pont" % mdetail)
r, d = relancer_moteur()
dire(" moteur : %s (%s)" % ("REPARE" if r else "ECHEC", d))
r2, d2 = relancer_pont()
dire(" pont : %s (%s)" % ("REPARE" if r2 else "ECHEC", d2))
reparations += 1
echecs = 0