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}47# --enable-flashinfer-autotune fait desormais partie du socle : mesure du48# 26/08, +8,2 % solo et +7,4 % agrege, et -1,01 ms sur le temps par pas.49FANIONS = ["--trust-remote-code", "--enable-prefix-caching",50 "--enable-auto-tool-choice", "--enable-flashinfer-autotune"]51 52CG = {"cudagraph_mode": "FULL_AND_PIECEWISE"}53 54 55def cc(**extra):56 d = dict(CG)57 d.update(extra)58 return json.dumps(d)59 60 61CONFIGS = [62 # Les deux gains connus s'additionnent-ils ? customops valait +1,7 % seul.63 ("custom_auto", {"--compilation-config": cc(64 custom_ops=["+rms_norm", "+fused_add_rms_norm"])}, [], []),65 # Le noyau MoE de TensorRT-LLM, atteignable depuis vLLM. Prediction posee66 # AVANT mesure : aucun effet, le noyau MoE n'etant pas le goulot (marlin vs67 # humming : 0,9 % d'ecart sur trois architectures). Risque : refus, ces68 # noyaux etant verrouilles sur sm_100 dans nos mesures precedentes.69 ("moe_trtllm", {"--moe-backend": "flashinfer_trtllm"}, [], []),70 ("tout", {"--compilation-config": cc(71 custom_ops=["+rms_norm", "+fused_add_rms_norm"]),72 "--moe-backend": "flashinfer_trtllm"}, [], []),73]74 75 76def dire(msg):77 ligne = "[%5.1f min] %s" % ((time.time() - DEBUT) / 60, msg)78 print(ligne, flush=True)79 with open(JOURNAL, "a") as f:80 f.write(ligne + "\n")81 82 83def metrique(nom):84 try:85 t = urllib.request.urlopen(BASE + "/metrics", timeout=5).read().decode()86 m = re.search(r"^vllm:%s\S*\s+([0-9.eE+-]+)$" % nom, t, re.M)87 return float(m.group(1)) if m else None88 except Exception:89 return None90 91 92def attendre_vide(limite=180):93 t0, libre = time.time(), 094 while time.time() - t0 < limite:95 r, w = metrique("num_requests_running"), metrique("num_requests_waiting")96 libre = libre + 1 if (r == 0 and (w or 0) == 0) else 097 if libre >= 2:98 return True99 time.sleep(5)100 return False101 102 103def arreter():104 out = subprocess.run(["ps", "-eo", "pid,rss,comm", "--no-headers"],105 capture_output=True, text=True).stdout106 for l in out.splitlines():107 p = l.split(None, 2)108 if len(p) != 3:109 continue110 pid, rss, comm = int(p[0]), int(p[1]), p[2].strip()111 if re.match(r"^(vllm|VLLM|Ple|EngineCore)", comm) or \112 (rss > 10_000_000 and re.match(r"^python3?$", comm)):113 try:114 os.kill(pid, 9)115 except Exception:116 pass117 time.sleep(12)118 119 120# Seuls ces trois motifs sont sans ambiguite. Le balayage 2 abandonnait sur121# "is not supported" (un WARNING de repli des graphes CUDA) et la premiere122# correction abandonnait sur "ERROR" (un lint de docstring de transformers).123# Un vrai echec produit TOUJOURS "Engine core initialization failed" -- verifie124# sur le plantage MTP et sur les trois refus de quantification de KV.125FATAL = re.compile(r"Engine core initialization failed|EngineDeadError|"126 r"Traceback \(most recent call last\)")127 128 129def raison(txt):130 m = re.findall(r"^.*(?:Error|Exception|not supported|NotImplemented).*$",131 txt, re.M)132 return " || ".join(x.strip()[:200] for x in m[-3:]) or "cause non identifiee"133 134 135def lancer(nom, remplace, ajout, retire):136 if os.path.exists(LOG):137 os.replace(LOG, "/travail/vllm-%s-precedent.log" % nom)138 opts = dict(SOCLE)139 opts.update(remplace)140 cmd = ["vllm", "serve", MODELE]141 for k, v in opts.items():142 cmd += [k, v]143 cmd += [f for f in FANIONS if f not in retire] + ajout144 env = dict(os.environ, VLLM_PLE_CPU_OFFLOAD="1")145 with open(LOG, "w") as sortie:146 subprocess.Popen(cmd, stdout=sortie, stderr=subprocess.STDOUT,147 stdin=subprocess.DEVNULL, start_new_session=True, env=env)148 for i in range(45):149 time.sleep(20)150 try:151 urllib.request.urlopen(BASE + "/v1/models", timeout=5)152 return True, ""153 except Exception:154 pass155 try:156 txt = open(LOG, errors="replace").read()157 except Exception:158 txt = ""159 if FATAL.search(txt):160 return False, raison(txt)161 return False, "jamais pret en 15 min"162 163 164def kv_annonce():165 try:166 txt = open(LOG, errors="replace").read()167 m = re.findall(r"GPU KV cache size: ([\d,]+) tokens.*?concurrency "168 r"for [\d,]+ tokens per request: ([\d.]+)x", txt)169 if m:170 return int(m[-1][0].replace(",", "")), float(m[-1][1])171 except Exception:172 pass173 return None, None174 175 176def preuve():177 """Le levier a-t-il MORDU ? Sans ca, un reglage ignore ressemble a un178 reglage sans effet -- exactement le piege d'--async-scheduling."""179 try:180 txt = open(LOG, errors="replace").read()181 out = []182 for motif in (r"cudagraph_mode[^,}\n]{0,40}",183 r"enabled_custom_ops[^\n]{0,120}",184 r"custom_ops'?: ?\[[^\]]{0,80}\]",185 r"'fuse_norm_quant': ?\w+",186 r"Capturing CUDA graphs \(\w+\)"):187 t = re.findall(motif, txt)188 if t:189 out.append(t[-1][:120])190 return " ; ".join(out)[:300]191 except Exception:192 return ""193 194 195def appel(prompt, n, ignore=True):196 corps = json.dumps({"model": "flashnext", "prompt": prompt, "max_tokens": n,197 "ignore_eos": ignore, "temperature": 0.7}).encode()198 t = time.time()199 r = json.loads(urllib.request.urlopen(urllib.request.Request(200 BASE + "/v1/completions", data=corps,201 headers={"content-type": "application/json"}), timeout=400).read().decode())202 u = r["usage"]203 return (u["completion_tokens"], u["prompt_tokens"], time.time() - t,204 r["choices"][0]["text"])205 206 207def div4(texte):208 mots = texte.split()209 if len(mots) < 40:210 return None211 q = [tuple(mots[i:i + 4]) for i in range(len(mots) - 3)]212 return len(set(q)) / len(q)213 214 215def banc(nom):216 r = {"nom": nom}217 r["kv"], r["conc"] = kv_annonce()218 r["preuve"] = preuve()219 if not attendre_vide():220 r["erreur"] = "moteur jamais libre"221 return r222 try:223 appel("Bonjour.", 64)224 n, _, d, _ = appel("Ecris un texte long et detaille sur la mer.", 300)225 r["solo"] = round(n / d, 1)226 r["ms_pas"] = round(d * 1000 / n, 2)227 t = time.time()228 with ThreadPoolExecutor(max_workers=8) as ex:229 res = list(ex.map(lambda i: appel(230 "Ecris un texte long et detaille sur la mer numero %d." % i, 200),231 range(8)))232 r["agg8"] = round(sum(x[0] for x in res) / (time.time() - t), 1)233 long = " ".join("mot%d" % i for i in range(24000))234 _, p1, d1, _ = appel(long, 1)235 r["prefill"] = round(p1 / d1)236 _, _, _, txt = appel("Explique en detail comment fonctionne une maree, "237 "en francais.", 220, ignore=False)238 r["div4"] = round(div4(txt), 3) if div4(txt) else None239 except Exception as e:240 r["erreur"] = repr(e)[:200]241 return r242 243 244resultats = []245dire("=== REFERENCE (sans redemarrage)")246ref = banc("base")247resultats.append(ref)248dire(" base : %s" % json.dumps(ref, ensure_ascii=False))249 250for nom, remplace, ajout, retire in CONFIGS:251 if time.time() - DEBUT > PLAFOND:252 dire("!!! plafond de temps, on passe a la config finale")253 break254 dire("=== %s" % nom)255 arreter()256 ok, motif = lancer(nom, remplace, ajout, retire)257 if not ok:258 dire(" %s : NE DEMARRE PAS -- %s" % (nom, motif))259 resultats.append({"nom": nom, "echec": motif})260 continue261 r = banc(nom)262 resultats.append(r)263 dire(" %s : %s" % (nom, json.dumps(r, ensure_ascii=False)))264 265with open("/travail/balayage5.json", "w") as f:266 json.dump(resultats, f, indent=1, ensure_ascii=False)267 268 269def trouve(nom):270 for r in resultats:271 if r["nom"] == nom and "echec" not in r and "erreur" not in r:272 return r273 return None274 275 276base = trouve("base") or {}277opts, ajout, retire, raisons, meilleur = {}, [], [], [], None278for nom, remplace, aj, re_ in CONFIGS:279 r = trouve(nom)280 if not r or not r.get("solo") or not r.get("agg8"):281 continue282 if (r["solo"] > (base.get("solo") or 0) * 1.01283 and r["agg8"] >= (base.get("agg8") or 0) * 0.98284 and (r.get("div4") or 0) >= 0.8):285 if not meilleur or r["solo"] > meilleur["solo"]:286 meilleur, opts, ajout, retire = r, remplace, aj, re_287 raisons = ["%s : solo %s vs %s, agg8 %s vs %s" %288 (nom, r["solo"], base.get("solo"),289 r["agg8"], base.get("agg8"))]290 291dire("=== CONFIG FINALE : %s" % (", ".join(raisons) or "reference telle quelle"))292arreter()293ok, motif = lancer("final", opts, ajout, retire)294if not ok:295 dire(" finale KO (%s) -- repli sur la reference" % motif)296 arreter()297 ok, motif = lancer("repli", {}, [], [])298 dire(" repli : %s %s" % ("OK" if ok else "ECHEC", motif))299kv, conc = kv_annonce()300dire("=== SERVEUR RENDU : kv=%s, concurrence %sx, options=%s %s" % (kv, conc, opts, ajout))301 302print("\n\n########## TABLEAU ##########")303print("%-11s %9s %6s %6s %8s %7s %6s" %304 ("config", "kv", "solo", "ms/pas", "agg8", "prefill", "div4"))305for r in resultats:306 if "echec" in r:307 print("%-11s NE DEMARRE PAS : %s" % (r["nom"], r["echec"][:100]))308 elif "erreur" in r:309 print("%-11s MESURE REFUSEE : %s" % (r["nom"], r["erreur"][:100]))310 else:311 print("%-11s %9s %6s %6s %8s %7s %6s" %312 (r["nom"], r.get("kv"), r.get("solo"), r.get("ms_pas"),313 r.get("agg8"), r.get("prefill"), r.get("div4")))314print("\n########## PREUVE QUE LE LEVIER A MORDU ##########")315for r in resultats:316 if r.get("preuve"):317 print(" %-11s %s" % (r["nom"], r["preuve"]))318 