patdev/k3-a40-bootstrap
01.3k
1# Chien de garde du pont et du moteur.2#3# POURQUOI. Trois fois le 27/08, le pont a cesse de servir alors que le moteur4# allait parfaitement bien. Signature invariable :5#6# /v1/models 200 <- servi localement, sans appel amont7# /v1/messages 500 en ~10 s <- pool=10.0 de httpx8# 127.0.0.1:8000 200 en 0,1 s <- LE MOTEUR VA BIEN9#10# La cause n'est PAS un plafond de temps : avec VL_TIMEOUT=240 la panne11# revient, et au moment du blocage il y avait **zero socket** ouvert vers le12# port 8000. Ce ne sont donc pas des connexions vivantes qui saturent le13# bassin, ce sont des PLACES comptabilisees et jamais rendues -- des flux14# abandonnes par le client dont le slot httpx n'est pas libere. La fuite est15# definitive par flux, pas temporelle.16#17# Tant que le correctif de code n'est pas ecrit et eprouve, ce chien convertit18# une panne silencieuse de plusieurs heures en une coupure de quelques secondes.19#20# IL NE SONDE JAMAIS /v1/models : c'est precisement l'endpoint qui ment.21import json22import os23import subprocess24import time25import urllib.error26import urllib.request27 28PONT = "http://127.0.0.1:8080"29MOTEUR = "http://127.0.0.1:8000"30JOURNAL = "/travail/chien.log"31PERIODE = 6032ECHECS_AVANT_ACTION = 2 # deux passes de suite : on ne relance pas sur un hoquet33 34ENV_PONT = {35 "VL_UPSTREAM": "http://127.0.0.1:8000",36 "VL_SERVED_NAME": "flashnext",37 "VL_REAL_MODEL": "RadixArk/Qwen3.8-Flash-Next-NVFP4",38 "VL_MAX_OUTPUT": "32768",39 "VL_TIMEOUT": "240",40}41 42MOTEUR_CMD = [43 "vllm", "serve", "RadixArk/Qwen3.8-Flash-Next-NVFP4",44 "--served-model-name", "flashnext", "--host", "127.0.0.1", "--port", "8000",45 "--max-model-len", "262144", "--max-num-seqs", "16",46 "--gpu-memory-utilization", "0.93",47 "--kv-cache-memory-bytes", "15569256448", # 14,5 GiB : marge pour l'encodeur d'images48 "--distributed-executor-backend", "mp",49 "--reasoning-parser", "qwen3", "--tool-call-parser", "qwen3_coder",50 # Le plafond a 0 rendait un 400 sur toute capture collee dans Claude Code51 # ("At most 0 image(s) may be provided..."). Il doit rester identique a52 # celui de lancer_flashnext.sh : sinon le chien "repare" en remettant les53 # images hors service, et la panne revient sans que rien ne l'explique.54 "--limit-mm-per-prompt", '{"image":4,"video":0}',55 "--trust-remote-code", "--enable-prefix-caching", "--enable-auto-tool-choice",56 "--enable-flashinfer-autotune", "--enable-prompt-tokens-details",57]58 59 60def dire(m):61 l = "[%s] %s" % (time.strftime("%H:%M:%S"), m)62 print(l, flush=True)63 try:64 with open(JOURNAL, "a") as f:65 f.write(l + "\n")66 except Exception:67 pass68 69 70def poste(url, corps, delai):71 """Rend (ok, detail). Une VRAIE generation, jamais /v1/models."""72 try:73 r = urllib.request.Request(74 url, data=json.dumps(corps).encode(),75 headers={"content-type": "application/json",76 "anthropic-version": "2023-06-01", "x-api-key": "x"})77 t = time.time()78 d = json.loads(urllib.request.urlopen(r, timeout=delai).read().decode())79 return True, "%.2fs" % (time.time() - t)80 except urllib.error.HTTPError as e:81 return False, "HTTP %d" % e.code82 except Exception as e:83 return False, repr(e)[:90]84 85 86def pont_vivant():87 return poste(PONT + "/v1/messages",88 {"model": "flashnext", "max_tokens": 8,89 "thinking": {"type": "disabled"},90 "messages": [{"role": "user", "content": "ping"}]}, 45)91 92 93def moteur_vivant():94 return poste(MOTEUR + "/v1/completions",95 {"model": "flashnext", "prompt": "ping", "max_tokens": 4,96 "temperature": 0}, 45)97 98 99def tuer(motif):100 out = subprocess.run(["ps", "-eo", "pid,rss,comm", "--no-headers"],101 capture_output=True, text=True).stdout102 for l in out.splitlines():103 p = l.split(None, 2)104 if len(p) != 3:105 continue106 pid, rss, comm = int(p[0]), int(p[1]), p[2].strip()107 import re as _re108 if _re.match(motif, comm) or (rss > 10_000_000 and _re.match(r"^python3?$", comm)):109 try:110 os.kill(pid, 9)111 except Exception:112 pass113 114 115def relancer_pont():116 """Le pont ne se relance JAMAIS depuis /proc/PID/cmdline : l'environnement117 n'y figure pas, et le defaut de VL_UPSTREAM est le port du pont lui-meme --118 il se parlerait a lui-meme. L'env est donc explicite ici."""119 p = subprocess.run(["pgrep", "-f", "uvicorn anthropic_proxy"],120 capture_output=True, text=True).stdout.split()121 for pid in p:122 try:123 os.kill(int(pid), 9)124 except Exception:125 pass126 time.sleep(3)127 env = dict(os.environ)128 env.update(ENV_PONT)129 with open("/travail/pont.log", "a") as s:130 subprocess.Popen(["python3", "-m", "uvicorn", "anthropic_proxy:app",131 "--host", "0.0.0.0", "--port", "8080",132 "--log-level", "warning"],133 cwd="/travail", stdout=s, stderr=subprocess.STDOUT,134 stdin=subprocess.DEVNULL, start_new_session=True, env=env)135 for _ in range(20):136 time.sleep(3)137 ok, d = pont_vivant()138 if ok:139 return True, d140 return False, "pas de generation apres 60 s"141 142 143def relancer_moteur():144 tuer(r"^(vllm|VLLM|Ple|EngineCore)")145 time.sleep(12)146 if os.path.exists("/travail/vllm.log"):147 os.replace("/travail/vllm.log", "/travail/vllm-chien-precedent.log")148 with open("/travail/vllm.log", "w") as s:149 subprocess.Popen(MOTEUR_CMD, stdout=s, stderr=subprocess.STDOUT,150 stdin=subprocess.DEVNULL, start_new_session=True,151 env=dict(os.environ, VLLM_PLE_CPU_OFFLOAD="1"))152 for _ in range(45):153 time.sleep(20)154 ok, d = moteur_vivant()155 if ok:156 return True, d157 return False, "moteur pas pret en 15 min"158 159 160dire("chien de garde arme : generation reelle toutes les %d s, action apres "161 "%d echecs consecutifs" % (PERIODE, ECHECS_AVANT_ACTION))162echecs, reparations, tours = 0, 0, 0163while True:164 time.sleep(PERIODE)165 tours += 1166 ok, detail = pont_vivant()167 if ok:168 if echecs:169 dire("retabli tout seul apres %d echec(s)" % echecs)170 echecs = 0171 if tours % 15 == 0:172 dire("ok (%s) | %d reparation(s) depuis l'armement" % (detail, reparations))173 continue174 175 echecs += 1176 dire("pont KO (%s) -- echec %d/%d" % (detail, echecs, ECHECS_AVANT_ACTION))177 if echecs < ECHECS_AVANT_ACTION:178 continue179 180 # On distingue TOUJOURS les deux couches avant d'agir : trois fois cette181 # nuit, le moteur allait bien et j'ai failli le tuer pour rien.182 mok, mdetail = moteur_vivant()183 if mok:184 dire("le moteur va bien (%s) -> c'est le pont : relance du pont seul" % mdetail)185 r, d = relancer_pont()186 dire(" pont : %s (%s)" % ("REPARE" if r else "ECHEC", d))187 else:188 dire("le moteur aussi est KO (%s) -> relance moteur puis pont" % mdetail)189 r, d = relancer_moteur()190 dire(" moteur : %s (%s)" % ("REPARE" if r else "ECHEC", d))191 r2, d2 = relancer_pont()192 dire(" pont : %s (%s)" % ("REPARE" if r2 else "ECHEC", d2))193 reparations += 1194 echecs = 0195 