patdev/k3-a40-bootstrap
01.3k
1# Balayage 3 : reprise du 2, avec DEUX corrections.2#3# CORRECTION 1 -- le detecteur d'echec. Le balayage 2 declarait morte toute4# configuration dont le journal contenait "is not supported", et il a mordu sur5# un AVERTISSEMENT benin :6# WARNING [compilation.py:1414] CUDAGraphMode.FULL is not supported with7# Qwen3_8FlashNextQSAFlashAttentionBackend (support: UNIFORM_BATCH)8# Le moteur repliait gracieusement en PIECEWISE et demarrait normalement ; la9# preuve, l'abandon tombait 5,2 min apres le lancement, soit APRES le10# chargement des poids et la compilation. Desormais on n'abandonne que sur une11# ligne ERROR / Traceback / EngineDead, et JAMAIS sur une ligne WARNING.12#13# CORRECTION 2 -- une seule variable a la fois, pour de vrai. Passer14# --compilation-config remet `cudagraph_mode` a son defaut FULL au lieu du15# FULL_AND_PIECEWISE que vLLM choisit tout seul pour ce modele. Le balayage 216# changeait donc DEUX choses a la fois sans que je le voie. On restate donc17# cudagraph_mode dans chaque JSON, et la premiere configuration est un TEMOIN18# qui ne change que ca : s'il retrouve les chiffres de la reference, la syntaxe19# est bonne et tout ecart ulterieur est imputable au vrai levier.20import json21import os22import re23import subprocess24import time25import urllib.request26from concurrent.futures import ThreadPoolExecutor27 28BASE = "http://127.0.0.1:8000"29MODELE = "RadixArk/Qwen3.8-Flash-Next-NVFP4"30LOG = "/travail/vllm.log"31JOURNAL = "/travail/banc.log"32DEBUT = time.time()33PLAFOND = 330034 35SOCLE = {36 "--served-model-name": "flashnext",37 "--host": "127.0.0.1",38 "--port": "8000",39 "--max-model-len": "262144",40 "--max-num-seqs": "16",41 "--gpu-memory-utilization": "0.93",42 "--distributed-executor-backend": "mp",43 "--reasoning-parser": "qwen3",44 "--tool-call-parser": "qwen3_coder",45 "--limit-mm-per-prompt": '{"image":0,"video":0}',46}47FANIONS = ["--trust-remote-code", "--enable-prefix-caching",48 "--enable-auto-tool-choice", "--no-enable-flashinfer-autotune"]49 50CG = {"cudagraph_mode": "FULL_AND_PIECEWISE"}51 52 53def cc(**extra):54 d = dict(CG)55 d.update(extra)56 return json.dumps(d)57 58 59CONFIGS = [60 ("ctrl_cg", {"--compilation-config": cc()}, [], []),61 ("fusions", {"--compilation-config": cc(pass_config={62 "fuse_norm_quant": True, "fuse_attn_quant": True,63 "enable_qk_norm_rope_fusion": True,64 "fuse_qk_norm_rope_kvcache": True})}, [], []),65 ("customops", {"--compilation-config": cc(66 custom_ops=["+rms_norm", "+fused_add_rms_norm"])}, [], []),67 ("autotune", {}, ["--enable-flashinfer-autotune"],68 ["--no-enable-flashinfer-autotune"]),69]70 71 72def dire(msg):73 ligne = "[%5.1f min] %s" % ((time.time() - DEBUT) / 60, msg)74 print(ligne, flush=True)75 with open(JOURNAL, "a") as f:76 f.write(ligne + "\n")77 78 79def metrique(nom):80 try:81 t = urllib.request.urlopen(BASE + "/metrics", timeout=5).read().decode()82 m = re.search(r"^vllm:%s\S*\s+([0-9.eE+-]+)$" % nom, t, re.M)83 return float(m.group(1)) if m else None84 except Exception:85 return None86 87 88def attendre_vide(limite=180):89 t0, libre = time.time(), 090 while time.time() - t0 < limite:91 r, w = metrique("num_requests_running"), metrique("num_requests_waiting")92 libre = libre + 1 if (r == 0 and (w or 0) == 0) else 093 if libre >= 2:94 return True95 time.sleep(5)96 return False97 98 99def arreter():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 if re.match(r"^(vllm|VLLM|Ple|EngineCore)", comm) or \108 (rss > 10_000_000 and re.match(r"^python3?$", comm)):109 try:110 os.kill(pid, 9)111 except Exception:112 pass113 time.sleep(12)114 115 116# Seuls ces trois motifs sont sans ambiguite. Le balayage 2 abandonnait sur117# "is not supported" (un WARNING de repli des graphes CUDA) et la premiere118# correction abandonnait sur "ERROR" (un lint de docstring de transformers).119# Un vrai echec produit TOUJOURS "Engine core initialization failed" -- verifie120# sur le plantage MTP et sur les trois refus de quantification de KV.121FATAL = re.compile(r"Engine core initialization failed|EngineDeadError|"122 r"Traceback \(most recent call last\)")123 124 125def raison(txt):126 m = re.findall(r"^.*(?:Error|Exception|not supported|NotImplemented).*$",127 txt, re.M)128 return " || ".join(x.strip()[:200] for x in m[-3:]) or "cause non identifiee"129 130 131def lancer(nom, remplace, ajout, retire):132 if os.path.exists(LOG):133 os.replace(LOG, "/travail/vllm-%s-precedent.log" % nom)134 opts = dict(SOCLE)135 opts.update(remplace)136 cmd = ["vllm", "serve", MODELE]137 for k, v in opts.items():138 cmd += [k, v]139 cmd += [f for f in FANIONS if f not in retire] + ajout140 env = dict(os.environ, VLLM_PLE_CPU_OFFLOAD="1")141 with open(LOG, "w") as sortie:142 subprocess.Popen(cmd, stdout=sortie, stderr=subprocess.STDOUT,143 stdin=subprocess.DEVNULL, start_new_session=True, env=env)144 for i in range(45):145 time.sleep(20)146 try:147 urllib.request.urlopen(BASE + "/v1/models", timeout=5)148 return True, ""149 except Exception:150 pass151 try:152 txt = open(LOG, errors="replace").read()153 except Exception:154 txt = ""155 if FATAL.search(txt):156 return False, raison(txt)157 return False, "jamais pret en 15 min"158 159 160def kv_annonce():161 try:162 txt = open(LOG, errors="replace").read()163 m = re.findall(r"GPU KV cache size: ([\d,]+) tokens.*?concurrency "164 r"for [\d,]+ tokens per request: ([\d.]+)x", txt)165 if m:166 return int(m[-1][0].replace(",", "")), float(m[-1][1])167 except Exception:168 pass169 return None, None170 171 172def preuve():173 """Le levier a-t-il MORDU ? Sans ca, un reglage ignore ressemble a un174 reglage sans effet -- exactement le piege d'--async-scheduling."""175 try:176 txt = open(LOG, errors="replace").read()177 out = []178 for motif in (r"cudagraph_mode[^,}\n]{0,40}",179 r"enabled_custom_ops[^\n]{0,120}",180 r"custom_ops'?: ?\[[^\]]{0,80}\]",181 r"'fuse_norm_quant': ?\w+",182 r"Capturing CUDA graphs \(\w+\)"):183 t = re.findall(motif, txt)184 if t:185 out.append(t[-1][:120])186 return " ; ".join(out)[:300]187 except Exception:188 return ""189 190 191def appel(prompt, n, ignore=True):192 corps = json.dumps({"model": "flashnext", "prompt": prompt, "max_tokens": n,193 "ignore_eos": ignore, "temperature": 0.7}).encode()194 t = time.time()195 r = json.loads(urllib.request.urlopen(urllib.request.Request(196 BASE + "/v1/completions", data=corps,197 headers={"content-type": "application/json"}), timeout=400).read().decode())198 u = r["usage"]199 return (u["completion_tokens"], u["prompt_tokens"], time.time() - t,200 r["choices"][0]["text"])201 202 203def div4(texte):204 mots = texte.split()205 if len(mots) < 40:206 return None207 q = [tuple(mots[i:i + 4]) for i in range(len(mots) - 3)]208 return len(set(q)) / len(q)209 210 211def banc(nom):212 r = {"nom": nom}213 r["kv"], r["conc"] = kv_annonce()214 r["preuve"] = preuve()215 if not attendre_vide():216 r["erreur"] = "moteur jamais libre"217 return r218 try:219 appel("Bonjour.", 64)220 n, _, d, _ = appel("Ecris un texte long et detaille sur la mer.", 300)221 r["solo"] = round(n / d, 1)222 r["ms_pas"] = round(d * 1000 / n, 2)223 t = time.time()224 with ThreadPoolExecutor(max_workers=8) as ex:225 res = list(ex.map(lambda i: appel(226 "Ecris un texte long et detaille sur la mer numero %d." % i, 200),227 range(8)))228 r["agg8"] = round(sum(x[0] for x in res) / (time.time() - t), 1)229 long = " ".join("mot%d" % i for i in range(24000))230 _, p1, d1, _ = appel(long, 1)231 r["prefill"] = round(p1 / d1)232 _, _, _, txt = appel("Explique en detail comment fonctionne une maree, "233 "en francais.", 220, ignore=False)234 r["div4"] = round(div4(txt), 3) if div4(txt) else None235 except Exception as e:236 r["erreur"] = repr(e)[:200]237 return r238 239 240resultats = []241dire("=== REFERENCE")242arreter()243ok, motif = lancer("base", {}, [], [])244if ok:245 ref = banc("base")246 resultats.append(ref)247 dire(" base : %s" % json.dumps(ref, ensure_ascii=False))248else:249 dire(" base NE DEMARRE PAS -- %s" % motif)250 resultats.append({"nom": "base", "echec": motif})251 252for nom, remplace, ajout, retire in CONFIGS:253 if time.time() - DEBUT > PLAFOND:254 dire("!!! plafond de temps, on passe a la config finale")255 break256 dire("=== %s" % nom)257 arreter()258 ok, motif = lancer(nom, remplace, ajout, retire)259 if not ok:260 dire(" %s : NE DEMARRE PAS -- %s" % (nom, motif))261 resultats.append({"nom": nom, "echec": motif})262 continue263 r = banc(nom)264 resultats.append(r)265 dire(" %s : %s" % (nom, json.dumps(r, ensure_ascii=False)))266 267with open("/travail/balayage3.json", "w") as f:268 json.dump(resultats, f, indent=1, ensure_ascii=False)269 270 271def trouve(nom):272 for r in resultats:273 if r["nom"] == nom and "echec" not in r and "erreur" not in r:274 return r275 return None276 277 278base = trouve("base") or {}279opts, ajout, retire, raisons, meilleur = {}, [], [], [], None280for nom, remplace, aj, re_ in CONFIGS:281 r = trouve(nom)282 if not r or not r.get("solo") or not r.get("agg8"):283 continue284 if (r["solo"] > (base.get("solo") or 0) * 1.02285 and r["agg8"] >= (base.get("agg8") or 0) * 0.98286 and (r.get("div4") or 0) >= 0.8):287 if not meilleur or r["solo"] > meilleur["solo"]:288 meilleur, opts, ajout, retire = r, remplace, aj, re_289 raisons = ["%s : solo %s vs %s, agg8 %s vs %s" %290 (nom, r["solo"], base.get("solo"),291 r["agg8"], base.get("agg8"))]292 293dire("=== CONFIG FINALE : %s" % (", ".join(raisons) or "reference telle quelle"))294arreter()295ok, motif = lancer("final", opts, ajout, retire)296if not ok:297 dire(" finale KO (%s) -- repli sur la reference" % motif)298 arreter()299 ok, motif = lancer("repli", {}, [], [])300 dire(" repli : %s %s" % ("OK" if ok else "ECHEC", motif))301kv, conc = kv_annonce()302dire("=== SERVEUR RENDU : kv=%s, concurrence %sx, options=%s %s" % (kv, conc, opts, ajout))303 304print("\n\n########## TABLEAU ##########")305print("%-11s %9s %6s %6s %8s %7s %6s" %306 ("config", "kv", "solo", "ms/pas", "agg8", "prefill", "div4"))307for r in resultats:308 if "echec" in r:309 print("%-11s NE DEMARRE PAS : %s" % (r["nom"], r["echec"][:100]))310 elif "erreur" in r:311 print("%-11s MESURE REFUSEE : %s" % (r["nom"], r["erreur"][:100]))312 else:313 print("%-11s %9s %6s %6s %8s %7s %6s" %314 (r["nom"], r.get("kv"), r.get("solo"), r.get("ms_pas"),315 r.get("agg8"), r.get("prefill"), r.get("div4")))316print("\n########## PREUVE QUE LE LEVIER A MORDU ##########")317for r in resultats:318 if r.get("preuve"):319 print(" %-11s %s" % (r["nom"], r["preuve"]))320 