CoolFace
Modelpublic

patdev/k3-a40-bootstrap

sourceHugging Faceotherupdated 17d agoView on Hugging Face
0likes1.3kdownloads
balayage_cache.py436 linesDownload Raw Back to root
1# Balayage capacite / cache / prefill sur le pod de production.2#3# Socle = la meilleure config connue : 0.93 + --enable-flashinfer-autotune.4# UNE variable change par configuration ; la reference est mesuree d'abord.5#6# TROIS NOUVEAUTES par rapport aux balayages precedents :7#8# 1. --kv-cache-memory-bytes. On est a 12,26 Gio de KV avec 0.93. vLLM annonce9#    19 243 520 512 (17,92 Gio) comme "fully utilize" -- et cette valeur-la a10#    tue le serveur. L'intervalle entre les deux n'a JAMAIS ete essaye.11#    C'est le seul levier qui attaque le goulot demontre : la capacite.12#13# 2. Le prefill FROID APRES EVICTION, pas seulement le froid initial. C'est le14#    seul cas que le dechargement KV peut ameliorer. Les prompts sont SALES en15#    TETE : sans ca le cache de prefixe fausse le "froid" d'un facteur 20.16#17# 3. Une sonde de FUITE CJK. Le 26/08 le modele a ecrit un ideogramme au18#    milieu d'un mot francais -- un glissement de jeton vers le chinois.19#    C'est la signature d'une marge de logit ecrasee. On la mesure a20#    temperature 1,0 (le regime de Claude Code) et a 0,7 (celui des bancs).21#    Un debit ne vaut rien si le texte deraille.22#23# Le script rend TOUJOURS un serveur : la meilleure config qui a demarre, ou24# la reference en repli. Ce pod sert les sessions de l'utilisateur.25import glob26import json27import os28import re29import subprocess30import time31import urllib.request32from concurrent.futures import ThreadPoolExecutor33 34BASE = "http://127.0.0.1:8000"35MODELE = "RadixArk/Qwen3.8-Flash-Next-NVFP4"36LOG = "/travail/vllm.log"37JOURNAL = "/travail/banc.log"38DEBUT = time.time()39PLAFOND = 4200          # 70 min : au-dela on assemble et on rend le serveur.40 41SOCLE = {42    "--served-model-name": "flashnext",43    "--host": "127.0.0.1",44    "--port": "8000",45    "--max-model-len": "262144",46    "--max-num-seqs": "16",47    "--gpu-memory-utilization": "0.93",48    "--distributed-executor-backend": "mp",49    "--reasoning-parser": "qwen3",50    "--tool-call-parser": "qwen3_coder",51    "--limit-mm-per-prompt": '{"image":0,"video":0}',52}53FANIONS = ["--trust-remote-code", "--enable-prefix-caching",54           "--enable-auto-tool-choice", "--enable-flashinfer-autotune",55           "--enable-prompt-tokens-details"]56 57GIO = 1024 ** 358 59 60def dire(msg):61    ligne = "[%5.1f min] %s" % ((time.time() - DEBUT) / 60, msg)62    print(ligne, flush=True)63    with open(JOURNAL, "a") as f:64        f.write(ligne + "\n")65 66 67# ---------------------------------------------------------------- indexer68def cherche_config():69    """Le seul levier qui reduise le TRAVAIL de prefill au lieu de70    l'ordonnancer : le budget de l'attention eparse QSA. On ne devine pas sa71    valeur, on la lit -- et on note ou elle se trouve dans l'arbre, parce que72    --hf-overrides doit reproduire exactement ce chemin."""73    motifs = [74        "/root/.cache/huggingface/hub/models--RadixArk--*Flash-Next*/snapshots/*/config.json",75        "/travail/**/models--RadixArk--*Flash-Next*/snapshots/*/config.json",76        os.path.expanduser(77            "~/.cache/huggingface/hub/models--RadixArk--*Flash-Next*/snapshots/*/config.json"),78    ]79    for m in motifs:80        for p in glob.glob(m, recursive=True):81            try:82                return json.load(open(p)), p83            except Exception:84                pass85    return None, None86 87 88def surcharge_indexer(facteur=0.5):89    cfg, chemin = cherche_config()90    if not cfg:91        return None, "config.json introuvable"92    if "indexer_budget" in cfg:93        v = cfg["indexer_budget"]94        return (json.dumps({"indexer_budget": int(v * facteur)}),95                "racine %s -> %d" % (v, int(v * facteur)))96    tc = cfg.get("text_config") or {}97    if "indexer_budget" in tc:98        v = tc["indexer_budget"]99        return (json.dumps({"text_config": {"indexer_budget": int(v * facteur)}}),100                "text_config %s -> %d (%s)" % (v, int(v * facteur), chemin))101    return None, "pas d'indexer_budget dans %s" % chemin102 103 104# ------------------------------------------------------------- lancement105def arreter():106    out = subprocess.run(["ps", "-eo", "pid,rss,comm", "--no-headers"],107                         capture_output=True, text=True).stdout108    for l in out.splitlines():109        p = l.split(None, 2)110        if len(p) != 3:111            continue112        pid, rss, comm = int(p[0]), int(p[1]), p[2].strip()113        # Filtrer sur le NOM, jamais sur la ligne de commande : ce script114        # contient lui-meme la commande de lancement.115        if re.match(r"^(vllm|VLLM|Ple|EngineCore)", comm) or \116           (rss > 10_000_000 and re.match(r"^python3?$", comm)):117            try:118                os.kill(pid, 9)119            except Exception:120                pass121    time.sleep(12)122 123 124FATAL = re.compile(r"Engine core initialization failed|EngineDeadError|"125                   r"Traceback \(most recent call last\)")126 127 128def raison(txt):129    m = re.findall(r"^.*(?:Error|Exception|not supported|NotImplemented).*$",130                   txt, re.M)131    return " || ".join(x.strip()[:200] for x in m[-3:]) or "cause non identifiee"132 133 134def lancer(nom, remplace, ajout, retire):135    if os.path.exists(LOG):136        os.replace(LOG, "/travail/vllm-%s-precedent.log" % nom)137    opts = dict(SOCLE)138    opts.update(remplace)139    cmd = ["vllm", "serve", MODELE]140    for k, v in opts.items():141        cmd += [k, str(v)]142    cmd += [f for f in FANIONS if f not in retire] + list(ajout)143    env = dict(os.environ, VLLM_PLE_CPU_OFFLOAD="1")144    with open(LOG, "w") as sortie:145        subprocess.Popen(cmd, stdout=sortie, stderr=subprocess.STDOUT,146                         stdin=subprocess.DEVNULL, start_new_session=True, env=env)147    for i in range(45):148        time.sleep(20)149        try:150            urllib.request.urlopen(BASE + "/v1/models", timeout=5)151            return True, ""152        except Exception:153            pass154        try:155            txt = open(LOG, errors="replace").read()156        except Exception:157            txt = ""158        if FATAL.search(txt):159            return False, raison(txt)160    return False, "jamais pret en 15 min"161 162 163def kv_annonce():164    try:165        txt = open(LOG, errors="replace").read()166        k = re.findall(r"GPU KV cache size: ([\d,]+) tokens", txt)167        c = re.findall(r"concurrency for [\d,]+ tokens per request: ([\d.]+)x", txt)168        return (int(k[-1].replace(",", "")) if k else None,169                float(c[-1]) if c else None)170    except Exception:171        return None, None172 173 174# ------------------------------------------------------------------ banc175def metrique(nom):176    try:177        t = urllib.request.urlopen(BASE + "/metrics", timeout=5).read().decode()178        m = re.search(r"^vllm:%s\S*\s+([0-9.eE+-]+)$" % nom, t, re.M)179        return float(m.group(1)) if m else None180    except Exception:181        return None182 183 184def attendre_vide(limite=90):185    """Trois mesures de debit solo ont deja ete polluees par le trafic de186    l'utilisateur. On refuse de mesurer plutot que de publier un faux."""187    t0 = time.time()188    while time.time() - t0 < limite:189        n = metrique("num_requests_running")190        if n is not None and n < 0.5:191            return True192        time.sleep(5)193    return False194 195 196def appel(prompt, n, ignore=True, temp=0.7):197    corps = json.dumps({"model": "flashnext", "prompt": prompt, "max_tokens": n,198                        "ignore_eos": ignore, "temperature": temp}).encode()199    t = time.time()200    r = json.loads(urllib.request.urlopen(urllib.request.Request(201        BASE + "/v1/completions", data=corps,202        headers={"content-type": "application/json"}), timeout=600).read().decode())203    u = r["usage"]204    return (u["completion_tokens"], u["prompt_tokens"], time.time() - t,205            r["choices"][0]["text"])206 207 208def prompt_sale(graine, mots=24000):209    """Le sel est en TETE : rien ne peut correspondre dans le cache."""210    return ("sel%08d " % graine) + " ".join("mot%d" % (i + graine)211                                            for i in range(mots))212 213 214CJK = re.compile(r"[぀-ヿ㐀-䶿一-鿿가-힯]")215 216Q_PROMPT = ("Redige en francais, en trois paragraphes techniques et precis, "217            "comment on compile un projet Zig avec build.zig, ce que fait "218            "l'etape de verification des types, et pourquoi il faut relancer "219            "la compilation apres avoir modifie un module partage.\n\n")220 221 222def div4(texte):223    mots = texte.split()224    if len(mots) < 40:225        return None226    q = [tuple(mots[i:i + 4]) for i in range(len(mots) - 3)]227    return round(len(set(q)) / len(q), 3)228 229 230def qualite(r):231    """La sonde du bug du 26/08 : des ideogrammes au milieu du francais.232    Trois tirages, dont deux a temperature 1,0 -- c'est ce que Claude Code233    envoie, et c'est la que le glissement a ete observe."""234    total, echantillons, pire = 0, [], ""235    for temp in (1.0, 1.0, 0.7):236        try:237            _, _, _, txt = appel(Q_PROMPT, 420, ignore=False, temp=temp)238        except Exception:239            continue240        n = len(CJK.findall(txt))241        total += n242        echantillons.append({"temp": temp, "cjk": n, "div4": div4(txt)})243        if n and not pire:244            i = CJK.search(txt).start()245            pire = txt[max(0, i - 45):i + 25].replace("\n", " ")246    r["cjk"] = total247    r["qualite"] = echantillons248    r["div4"] = echantillons[0]["div4"] if echantillons else None249    if pire:250        r["fuite_cjk"] = pire251 252 253def banc(nom, eviction):254    r = {"nom": nom}255    r["kv"], r["conc"] = kv_annonce()256    try:257        appel("Bonjour.", 64)                        # rodage des noyaux258        if not attendre_vide():259            r["erreur"] = "serveur non oisif : mesure refusee"260            return r261        n, _, d, _ = appel("Ecris un texte long et detaille sur la mer.", 300)262        r["solo"] = round(n / d, 1)263        r["ms_pas"] = round(d * 1000 / n, 2)264 265        t = time.time()266        with ThreadPoolExecutor(max_workers=8) as ex:267            res = list(ex.map(lambda i: appel(268                "Ecris un texte long sur la mer numero %d." % i, 200), range(8)))269        r["agg8"] = round(sum(x[0] for x in res) / (time.time() - t), 1)270 271        A = prompt_sale(1)272        _, pA, d1, _ = appel(A, 1)273        r["prefill_froid"] = round(pA / d1)274        r["jetons"] = pA275        _, _, d2, _ = appel(A, 1)276        r["prefill_chaud"] = round(pA / d2)277 278        if eviction:279            # Chasser A du cache GPU : le KV tient ~500 k jetons, on pousse280            # 5 x 133 k = 665 k de prompts distincts.281            for i in range(2, 7):282                appel(prompt_sale(i), 1)283            _, _, d3, _ = appel(A, 1)284            r["prefill_evince"] = round(pA / d3)285            # 1,0 = rien recupere, > 1 = le dechargement sert vraiment.286            r["gain_eviction"] = round(d1 / d3, 2)287        r["hit"] = metrique("gpu_prefix_cache_hit_rate")288        qualite(r)289    except Exception as e:290        r["erreur"] = repr(e)[:200]291    return r292 293 294# ------------------------------------------------------- configurations295ov_indexer, note_indexer = surcharge_indexer(0.5)296 297CONFIGS = [298    # --- LE GOULOT DEMONTRE : la capacite. 0.93 rend 12,26 Gio ; le "fully299    # utilize" a 17,92 Gio avait tue le serveur. On sonde l'intervalle.300    ("kvmem15", {"--kv-cache-memory-bytes": 15 * GIO}, [], [], False),301    ("kvmem17", {"--kv-cache-memory-bytes": 17 * GIO}, [], [], False),302    # --- le dechargement : spectaculaire ou nul, rien entre les deux303    ("kvoff8", {"--kv-offloading-size": "8"}, [], [], True),304    # --- 166 blocs Pickle+SHA256 par prompt de 133 k, dans le chemin chaud305    ("xxhash", {"--prefix-caching-hash-algo": "xxhash"}, [], [], False),306    # --- le seul levier qui reduise le TRAVAIL de prefill307    ("indexer", {"--hf-overrides": ov_indexer} if ov_indexer else None,308     [], [], False),309    # --- granularite des succes de prefixe (bloc force a 800 pour Mamba)310    ("matchunit", {"--prefix-match-unit": "64"}, [], [], False),311    # --- partage de KV entre couches : le nom promet exactement notre regime312    ("kvshare", {}, ["--kv-sharing-fast-prefill"], [], False),313]314 315resultats = []316dire("=== indexer_budget : %s" % note_indexer)317dire("=== REFERENCE (socle 0.93 + autotune) -- relance propre")318arreter()319ok, motif = lancer("base", {}, [], [])320if ok:321    ref = banc("base", eviction=True)322    resultats.append(ref)323    dire("  base : %s" % json.dumps(ref, ensure_ascii=False))324else:325    ref = {}326    dire("  base NE DEMARRE PAS -- %s" % motif)327    resultats.append({"nom": "base", "echec": motif})328 329for nom, remplace, ajout, retire, evic in CONFIGS:330    if remplace is None:331        dire("=== %s : IGNOREE (%s)" % (nom, note_indexer))332        resultats.append({"nom": nom,333                          "echec": "non applicable : " + str(note_indexer)})334        continue335    if time.time() - DEBUT > PLAFOND:336        dire("!!! plafond de temps -- on assemble et on rend le serveur")337        break338    dire("=== %s : %s %s" % (nom, remplace, ajout))339    arreter()340    ok, motif = lancer(nom, remplace, ajout, retire)341    if not ok:342        dire("  %s : NE DEMARRE PAS -- %s" % (nom, motif))343        resultats.append({"nom": nom, "echec": motif})344        continue345    r = banc(nom, eviction=evic)346    resultats.append(r)347    dire("  %s : %s" % (nom, json.dumps(r, ensure_ascii=False)))348 349with open("/travail/balayage_cache.json", "w") as f:350    json.dump(resultats, f, indent=1, ensure_ascii=False)351 352 353# ------------------------------------------------- assemblage et remise354def trouve(nom):355    for r in resultats:356        if r["nom"] == nom and "echec" not in r and "erreur" not in r:357            return r358    return None359 360 361final_opts, final_fanions, raisons = {}, [], []362agg_ref = (ref or {}).get("agg8") or 0363kv_ref = (ref or {}).get("kv") or 0364 365# La capacite d'abord : le plus gros KV qui a demarre ET qui ne perd pas plus366# de 5 % d'agrege. Un cache plus grand ne vaut rien s'il ralentit.367for nom, octets in (("kvmem17", 17 * GIO), ("kvmem15", 15 * GIO)):368    r = trouve(nom)369    if r and r.get("kv") and (r.get("agg8") or 0) >= 0.95 * agg_ref \370            and r["kv"] > kv_ref:371        final_opts["--kv-cache-memory-bytes"] = octets372        raisons.append("%s (kv %d vs %d, agg8 %s vs %s)"373                       % (nom, r["kv"], kv_ref, r["agg8"], agg_ref))374        break375 376# Puis ce qui rend du debit sans rien couter.377for nom, cle, val in (("xxhash", "--prefix-caching-hash-algo", "xxhash"),378                      ("matchunit", "--prefix-match-unit", "64")):379    r = trouve(nom)380    if r and r.get("agg8") and r["agg8"] > agg_ref * 1.01:381        final_opts[cle] = val382        raisons.append("%s (agg8 %s > %s)" % (nom, r["agg8"], agg_ref))383 384# Le dechargement : seulement s'il recupere vraiment apres eviction.385r = trouve("kvoff8")386if r and (r.get("gain_eviction") or 0) > 1.15:387    final_opts["--kv-offloading-size"] = "8"388    raisons.append("kvoff8 (gain apres eviction %s)" % r["gain_eviction"])389 390r = trouve("kvshare")391if r and r.get("prefill_froid") and (ref or {}).get("prefill_froid") \392        and r["prefill_froid"] > 1.05 * ref["prefill_froid"]:393    final_fanions.append("--kv-sharing-fast-prefill")394    raisons.append("kvshare (prefill froid %s > %s)"395                   % (r["prefill_froid"], ref["prefill_froid"]))396 397dire("=== CONFIG FINALE : %s" % (", ".join(raisons) or "reference telle quelle"))398arreter()399ok, motif = lancer("final", final_opts, final_fanions, [])400if not ok:401    dire("  la config finale ne demarre pas (%s) -- retour a la reference" % motif)402    arreter()403    ok, motif = lancer("repli", {}, [], [])404    dire("  repli : %s %s" % ("OK" if ok else "ECHEC", motif))405    final_opts, final_fanions = {}, []406 407kv, conc = kv_annonce()408dire("=== SERVEUR RENDU : kv=%s jetons, concurrence %sx, options=%s %s"409     % (kv, conc, final_opts, final_fanions))410 411print("\n\n########## TABLEAU ##########")412ENT = "%-10s %9s %5s %6s %7s %8s %8s %6s %4s %6s"413print(ENT % ("config", "kv", "conc", "solo", "agg8", "pref_fr", "pref_ev",414             "gain", "cjk", "div4"))415for r in resultats:416    if "echec" in r:417        print("%-10s  NE DEMARRE PAS : %s" % (r["nom"], r["echec"][:95]))418    elif "erreur" in r:419        print("%-10s  ERREUR : %s" % (r["nom"], r["erreur"][:95]))420    else:421        print(ENT % (r["nom"], r.get("kv"), r.get("conc"), r.get("solo"),422                     r.get("agg8"), r.get("prefill_froid"),423                     r.get("prefill_evince", "-"), r.get("gain_eviction", "-"),424                     r.get("cjk"), r.get("div4")))425print("\n  gain = prefill apres eviction / prefill froid. 1,0 = rien recupere.")426print("  cjk  = ideogrammes dans 3 generations FRANCAISES (2 a temperature 1,0).")427print("         Zero est le seul resultat acceptable.")428print("\n########## FUITES CJK OBSERVEES ##########")429vu = False430for r in resultats:431    if r.get("fuite_cjk"):432        vu = True433        print("  %-10s ...%s..." % (r["nom"], r["fuite_cjk"]))434if not vu:435    print("  aucune")436