CoolFace
Modelpublic

patdev/k3-a40-bootstrap

sourceHugging Faceotherupdated 17d agoView on Hugging Face
0likes1.3kdownloads
multi_session.py148 linesDownload Raw Back to root
1"""Plusieurs sessions Claude Code simultanées : le cache tient-il, et à quel prix ?2 3Deux mécanismes indépendants sont en jeu, et ils tirent en sens contraire.4 5Le cache de préfixe de vLLM est **global**, pas par session : c'est un arbre6partagé entre toutes les requêtes. Deux clients qui envoient le même prompt7système ne le prefillent qu'une fois — le gain croît donc avec le nombre de8sessions.9 10La spéculation, elle, **perd** en concurrence : les brouillons consomment le11budget de batch. Ce script mesure les deux effets ensemble, par session et en12agrégé, pour situer le point de bascule.13 14    python multi_session.py --base https://<pod>-8080.proxy.runpod.net --sessions 1,2,4,815"""16 17from __future__ import annotations18 19import argparse20import json21import re22import threading23import time24import urllib.error25import urllib.request26 27# Prompt systeme identique pour toutes les sessions : c'est la situation reelle28# (Claude Code envoie le meme) et c'est ce qui rend le cache partage observable.29SYSTEM = ("Tu es un agent de codage autonome. Tu lis avant d'ecrire, tu procedes "30          "par etapes verifiables, et tu produis le fichier complet modifie.\n"31          + "Conventions du projet : type hints partout, docstrings sur les "32            "fonctions publiques, pas de dependance non justifiee, tests pytest.\n" * 60)33 34MODULE = """def handler_{i}(request: dict, context: Any) -> dict:35    payload = request.get('payload')36    if payload is None:37        raise ValueError('missing payload in handler_{i}')38    result = context.process(payload, retries=3, timeout={t})39    return {{'status': 'ok', 'handler': {i}, 'result': result}}40"""41 42 43def post(url: str, body: dict, timeout: int = 900) -> dict | None:44    req = urllib.request.Request(45        url, data=json.dumps(body).encode(),46        headers={"Content-Type": "application/json",47                 # Cloudflare rejette la signature par defaut d'urllib (403/1010).48                 "User-Agent": "Mozilla/5.0 (multi-session)"})49    try:50        with urllib.request.urlopen(req, timeout=timeout) as r:51            return json.loads(r.read())52    except Exception as e:53        return {"_err": f"{type(e).__name__}: {e}"}54 55 56def metrics(base: str) -> dict:57    """Compteurs de blocs de cache : c'est la preuve directe du partage."""58    try:59        req = urllib.request.Request(base.rstrip("/") + "/metrics",60                                     headers={"User-Agent": "Mozilla/5.0"})61        with urllib.request.urlopen(req, timeout=20) as r:62            txt = r.read().decode("utf-8", "ignore")63    except Exception:64        return {}65    out = {}66    for key in ("vllm:prefix_cache_queries_total", "vllm:prefix_cache_hits_total",67                "vllm:gpu_prefix_cache_queries_total", "vllm:gpu_prefix_cache_hits_total"):68        tot = 0.069        for m in re.finditer(rf"^{re.escape(key)}\{{.*?\}}\s+([\d.e+]+)$", txt, re.M):70            tot += float(m.group(1))71        if tot:72            out[key.split(":")[-1]] = tot73    return out74 75 76def session(base: str, model: str, sid: int, turns: int, out: list) -> None:77    """Une session = plusieurs tours qui reutilisent le meme prompt systeme."""78    url = base.rstrip("/") + "/v1/chat/completions"79    history = [{"role": "system", "content": SYSTEM}]80    got = 081    t0 = time.time()82    for t in range(turns):83        i = sid * 100 + t84        code = "".join(MODULE.format(i=j, t=j + 5) for j in range(i, i + 3))85        history.append({"role": "user", "content":86                        f"Ajoute la gestion d'erreur a handler_{i}.\n\n"87                        f"```python\n{code}\n```\n\nProduis le fichier complet."})88        r = post(url, {"model": model, "messages": history,89                       "max_tokens": 400, "temperature": 0})90        if not r or "_err" in r:91            out.append({"sid": sid, "err": (r or {}).get("_err", "?")})92            return93        msg = (r.get("choices") or [{}])[0].get("message", {}).get("content") or ""94        got += (r.get("usage") or {}).get("completion_tokens", 0)95        history.append({"role": "assistant", "content": msg})96    out.append({"sid": sid, "tokens": got, "sec": time.time() - t0})97 98 99def main() -> None:100    ap = argparse.ArgumentParser()101    ap.add_argument("--base", required=True)102    ap.add_argument("--model", default="qwen")103    ap.add_argument("--sessions", default="1,2,4,8")104    ap.add_argument("--turns", type=int, default=4)105    ap.add_argument("--hourly", type=float, default=0.44)106    a = ap.parse_args()107 108    print(f"{'sessions':>8} {'tokens':>8} {'duree':>7} {'agrege':>10} "109          f"{'/session':>10} {'$/1M':>8}  cache partage")110    print("-" * 78)111 112    for n in [int(x) for x in a.sessions.split(",")]:113        before = metrics(a.base)114        out: list = []115        threads = [threading.Thread(target=session,116                                    args=(a.base, a.model, s, a.turns, out))117                   for s in range(n)]118        t0 = time.time()119        for th in threads:120            th.start()121        for th in threads:122            th.join()123        dt = time.time() - t0124        after = metrics(a.base)125 126        errs = [o for o in out if "err" in o]127        toks = sum(o.get("tokens", 0) for o in out)128        agg = toks / dt if dt else 0129 130        # Le taux de reussite du cache sur CE palier : ce que les sessions ont131        # evite de reprefiller grace au prefixe partage.132        # vLLM expose `prefix_cache_*`, pas `gpu_prefix_cache_*` : chercher le133        # mauvais nom rendait "n/d" et laissait croire que le cache ne marchait134        # pas, alors qu'il tournait a ~90 % de reussite.135        dq = after.get("prefix_cache_queries_total", 0) - before.get("prefix_cache_queries_total", 0)136        dh = after.get("prefix_cache_hits_total", 0) - before.get("prefix_cache_hits_total", 0)137        share = f"{100*dh/dq:.0f} % ({dh:.0f}/{dq:.0f} blocs)" if dq else "n/d"138 139        print(f"{n:>8} {toks:>8} {dt:>6.1f}s {agg:>9.1f} {agg/n:>10.1f} "140              f"{a.hourly/(agg*3600)*1e6 if agg else 0:>8.3f}  {share}"141              + (f"   {len(errs)} ECHECS" if errs else ""))142        for e in errs[:2]:143            print(f"          session {e['sid']}: {e['err'][:100]}")144 145 146if __name__ == "__main__":147    main()148