Instructions to use patdev/k3-a40-bootstrap with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use patdev/k3-a40-bootstrap with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf patdev/k3-a40-bootstrap:BF16 # Run inference directly in the terminal: llama cli -hf patdev/k3-a40-bootstrap:BF16
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf patdev/k3-a40-bootstrap:BF16 # Run inference directly in the terminal: llama cli -hf patdev/k3-a40-bootstrap:BF16
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf patdev/k3-a40-bootstrap:BF16 # Run inference directly in the terminal: ./llama-cli -hf patdev/k3-a40-bootstrap:BF16
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf patdev/k3-a40-bootstrap:BF16 # Run inference directly in the terminal: ./build/bin/llama-cli -hf patdev/k3-a40-bootstrap:BF16
Use Docker
docker model run hf.co/patdev/k3-a40-bootstrap:BF16
- LM Studio
- Jan
- Ollama
How to use patdev/k3-a40-bootstrap with Ollama:
ollama run hf.co/patdev/k3-a40-bootstrap:BF16
- Unsloth Desktop
- Docker Model Runner
How to use patdev/k3-a40-bootstrap with Docker Model Runner:
docker model run hf.co/patdev/k3-a40-bootstrap:BF16
- Lemonade
How to use patdev/k3-a40-bootstrap with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull patdev/k3-a40-bootstrap:BF16
Run and chat with the model
lemonade run user.k3-a40-bootstrap-BF16
List all available models
lemonade list
- Atomic Chat
| # Balayage capacite / cache / prefill sur le pod de production. | |
| # | |
| # Socle = la meilleure config connue : 0.93 + --enable-flashinfer-autotune. | |
| # UNE variable change par configuration ; la reference est mesuree d'abord. | |
| # | |
| # TROIS NOUVEAUTES par rapport aux balayages precedents : | |
| # | |
| # 1. --kv-cache-memory-bytes. On est a 12,26 Gio de KV avec 0.93. vLLM annonce | |
| # 19 243 520 512 (17,92 Gio) comme "fully utilize" -- et cette valeur-la a | |
| # tue le serveur. L'intervalle entre les deux n'a JAMAIS ete essaye. | |
| # C'est le seul levier qui attaque le goulot demontre : la capacite. | |
| # | |
| # 2. Le prefill FROID APRES EVICTION, pas seulement le froid initial. C'est le | |
| # seul cas que le dechargement KV peut ameliorer. Les prompts sont SALES en | |
| # TETE : sans ca le cache de prefixe fausse le "froid" d'un facteur 20. | |
| # | |
| # 3. Une sonde de FUITE CJK. Le 26/08 le modele a ecrit un ideogramme au | |
| # milieu d'un mot francais -- un glissement de jeton vers le chinois. | |
| # C'est la signature d'une marge de logit ecrasee. On la mesure a | |
| # temperature 1,0 (le regime de Claude Code) et a 0,7 (celui des bancs). | |
| # Un debit ne vaut rien si le texte deraille. | |
| # | |
| # Le script rend TOUJOURS un serveur : la meilleure config qui a demarre, ou | |
| # la reference en repli. Ce pod sert les sessions de l'utilisateur. | |
| import glob | |
| import json | |
| import os | |
| import re | |
| import subprocess | |
| import time | |
| import urllib.request | |
| from concurrent.futures import ThreadPoolExecutor | |
| BASE = "http://127.0.0.1:8000" | |
| MODELE = "RadixArk/Qwen3.8-Flash-Next-NVFP4" | |
| LOG = "/travail/vllm.log" | |
| JOURNAL = "/travail/banc.log" | |
| DEBUT = time.time() | |
| PLAFOND = 4200 # 70 min : au-dela on assemble et on rend le serveur. | |
| SOCLE = { | |
| "--served-model-name": "flashnext", | |
| "--host": "127.0.0.1", | |
| "--port": "8000", | |
| "--max-model-len": "262144", | |
| "--max-num-seqs": "16", | |
| "--gpu-memory-utilization": "0.93", | |
| "--distributed-executor-backend": "mp", | |
| "--reasoning-parser": "qwen3", | |
| "--tool-call-parser": "qwen3_coder", | |
| "--limit-mm-per-prompt": '{"image":0,"video":0}', | |
| } | |
| FANIONS = ["--trust-remote-code", "--enable-prefix-caching", | |
| "--enable-auto-tool-choice", "--enable-flashinfer-autotune", | |
| "--enable-prompt-tokens-details"] | |
| GIO = 1024 ** 3 | |
| def dire(msg): | |
| ligne = "[%5.1f min] %s" % ((time.time() - DEBUT) / 60, msg) | |
| print(ligne, flush=True) | |
| with open(JOURNAL, "a") as f: | |
| f.write(ligne + "\n") | |
| # ---------------------------------------------------------------- indexer | |
| def cherche_config(): | |
| """Le seul levier qui reduise le TRAVAIL de prefill au lieu de | |
| l'ordonnancer : le budget de l'attention eparse QSA. On ne devine pas sa | |
| valeur, on la lit -- et on note ou elle se trouve dans l'arbre, parce que | |
| --hf-overrides doit reproduire exactement ce chemin.""" | |
| motifs = [ | |
| "/root/.cache/huggingface/hub/models--RadixArk--*Flash-Next*/snapshots/*/config.json", | |
| "/travail/**/models--RadixArk--*Flash-Next*/snapshots/*/config.json", | |
| os.path.expanduser( | |
| "~/.cache/huggingface/hub/models--RadixArk--*Flash-Next*/snapshots/*/config.json"), | |
| ] | |
| for m in motifs: | |
| for p in glob.glob(m, recursive=True): | |
| try: | |
| return json.load(open(p)), p | |
| except Exception: | |
| pass | |
| return None, None | |
| def surcharge_indexer(facteur=0.5): | |
| cfg, chemin = cherche_config() | |
| if not cfg: | |
| return None, "config.json introuvable" | |
| if "indexer_budget" in cfg: | |
| v = cfg["indexer_budget"] | |
| return (json.dumps({"indexer_budget": int(v * facteur)}), | |
| "racine %s -> %d" % (v, int(v * facteur))) | |
| tc = cfg.get("text_config") or {} | |
| if "indexer_budget" in tc: | |
| v = tc["indexer_budget"] | |
| return (json.dumps({"text_config": {"indexer_budget": int(v * facteur)}}), | |
| "text_config %s -> %d (%s)" % (v, int(v * facteur), chemin)) | |
| return None, "pas d'indexer_budget dans %s" % chemin | |
| # ------------------------------------------------------------- lancement | |
| def arreter(): | |
| out = subprocess.run(["ps", "-eo", "pid,rss,comm", "--no-headers"], | |
| capture_output=True, text=True).stdout | |
| for l in out.splitlines(): | |
| p = l.split(None, 2) | |
| if len(p) != 3: | |
| continue | |
| pid, rss, comm = int(p[0]), int(p[1]), p[2].strip() | |
| # Filtrer sur le NOM, jamais sur la ligne de commande : ce script | |
| # contient lui-meme la commande de lancement. | |
| if re.match(r"^(vllm|VLLM|Ple|EngineCore)", comm) or \ | |
| (rss > 10_000_000 and re.match(r"^python3?$", comm)): | |
| try: | |
| os.kill(pid, 9) | |
| except Exception: | |
| pass | |
| time.sleep(12) | |
| FATAL = re.compile(r"Engine core initialization failed|EngineDeadError|" | |
| r"Traceback \(most recent call last\)") | |
| def raison(txt): | |
| m = re.findall(r"^.*(?:Error|Exception|not supported|NotImplemented).*$", | |
| txt, re.M) | |
| return " || ".join(x.strip()[:200] for x in m[-3:]) or "cause non identifiee" | |
| def lancer(nom, remplace, ajout, retire): | |
| if os.path.exists(LOG): | |
| os.replace(LOG, "/travail/vllm-%s-precedent.log" % nom) | |
| opts = dict(SOCLE) | |
| opts.update(remplace) | |
| cmd = ["vllm", "serve", MODELE] | |
| for k, v in opts.items(): | |
| cmd += [k, str(v)] | |
| cmd += [f for f in FANIONS if f not in retire] + list(ajout) | |
| env = dict(os.environ, VLLM_PLE_CPU_OFFLOAD="1") | |
| with open(LOG, "w") as sortie: | |
| subprocess.Popen(cmd, stdout=sortie, stderr=subprocess.STDOUT, | |
| stdin=subprocess.DEVNULL, start_new_session=True, env=env) | |
| for i in range(45): | |
| time.sleep(20) | |
| try: | |
| urllib.request.urlopen(BASE + "/v1/models", timeout=5) | |
| return True, "" | |
| except Exception: | |
| pass | |
| try: | |
| txt = open(LOG, errors="replace").read() | |
| except Exception: | |
| txt = "" | |
| if FATAL.search(txt): | |
| return False, raison(txt) | |
| return False, "jamais pret en 15 min" | |
| def kv_annonce(): | |
| try: | |
| txt = open(LOG, errors="replace").read() | |
| k = re.findall(r"GPU KV cache size: ([\d,]+) tokens", txt) | |
| c = re.findall(r"concurrency for [\d,]+ tokens per request: ([\d.]+)x", txt) | |
| return (int(k[-1].replace(",", "")) if k else None, | |
| float(c[-1]) if c else None) | |
| except Exception: | |
| return None, None | |
| # ------------------------------------------------------------------ banc | |
| def metrique(nom): | |
| try: | |
| t = urllib.request.urlopen(BASE + "/metrics", timeout=5).read().decode() | |
| m = re.search(r"^vllm:%s\S*\s+([0-9.eE+-]+)$" % nom, t, re.M) | |
| return float(m.group(1)) if m else None | |
| except Exception: | |
| return None | |
| def attendre_vide(limite=90): | |
| """Trois mesures de debit solo ont deja ete polluees par le trafic de | |
| l'utilisateur. On refuse de mesurer plutot que de publier un faux.""" | |
| t0 = time.time() | |
| while time.time() - t0 < limite: | |
| n = metrique("num_requests_running") | |
| if n is not None and n < 0.5: | |
| return True | |
| time.sleep(5) | |
| return False | |
| def appel(prompt, n, ignore=True, temp=0.7): | |
| corps = json.dumps({"model": "flashnext", "prompt": prompt, "max_tokens": n, | |
| "ignore_eos": ignore, "temperature": temp}).encode() | |
| t = time.time() | |
| r = json.loads(urllib.request.urlopen(urllib.request.Request( | |
| BASE + "/v1/completions", data=corps, | |
| headers={"content-type": "application/json"}), timeout=600).read().decode()) | |
| u = r["usage"] | |
| return (u["completion_tokens"], u["prompt_tokens"], time.time() - t, | |
| r["choices"][0]["text"]) | |
| def prompt_sale(graine, mots=24000): | |
| """Le sel est en TETE : rien ne peut correspondre dans le cache.""" | |
| return ("sel%08d " % graine) + " ".join("mot%d" % (i + graine) | |
| for i in range(mots)) | |
| CJK = re.compile(r"[-ヿ㐀-䶿一-鿿가-]") | |
| Q_PROMPT = ("Redige en francais, en trois paragraphes techniques et precis, " | |
| "comment on compile un projet Zig avec build.zig, ce que fait " | |
| "l'etape de verification des types, et pourquoi il faut relancer " | |
| "la compilation apres avoir modifie un module partage.\n\n") | |
| def div4(texte): | |
| mots = texte.split() | |
| if len(mots) < 40: | |
| return None | |
| q = [tuple(mots[i:i + 4]) for i in range(len(mots) - 3)] | |
| return round(len(set(q)) / len(q), 3) | |
| def qualite(r): | |
| """La sonde du bug du 26/08 : des ideogrammes au milieu du francais. | |
| Trois tirages, dont deux a temperature 1,0 -- c'est ce que Claude Code | |
| envoie, et c'est la que le glissement a ete observe.""" | |
| total, echantillons, pire = 0, [], "" | |
| for temp in (1.0, 1.0, 0.7): | |
| try: | |
| _, _, _, txt = appel(Q_PROMPT, 420, ignore=False, temp=temp) | |
| except Exception: | |
| continue | |
| n = len(CJK.findall(txt)) | |
| total += n | |
| echantillons.append({"temp": temp, "cjk": n, "div4": div4(txt)}) | |
| if n and not pire: | |
| i = CJK.search(txt).start() | |
| pire = txt[max(0, i - 45):i + 25].replace("\n", " ") | |
| r["cjk"] = total | |
| r["qualite"] = echantillons | |
| r["div4"] = echantillons[0]["div4"] if echantillons else None | |
| if pire: | |
| r["fuite_cjk"] = pire | |
| def banc(nom, eviction): | |
| r = {"nom": nom} | |
| r["kv"], r["conc"] = kv_annonce() | |
| try: | |
| appel("Bonjour.", 64) # rodage des noyaux | |
| if not attendre_vide(): | |
| r["erreur"] = "serveur non oisif : mesure refusee" | |
| return r | |
| n, _, d, _ = appel("Ecris un texte long et detaille sur la mer.", 300) | |
| r["solo"] = round(n / d, 1) | |
| r["ms_pas"] = round(d * 1000 / n, 2) | |
| t = time.time() | |
| with ThreadPoolExecutor(max_workers=8) as ex: | |
| res = list(ex.map(lambda i: appel( | |
| "Ecris un texte long sur la mer numero %d." % i, 200), range(8))) | |
| r["agg8"] = round(sum(x[0] for x in res) / (time.time() - t), 1) | |
| A = prompt_sale(1) | |
| _, pA, d1, _ = appel(A, 1) | |
| r["prefill_froid"] = round(pA / d1) | |
| r["jetons"] = pA | |
| _, _, d2, _ = appel(A, 1) | |
| r["prefill_chaud"] = round(pA / d2) | |
| if eviction: | |
| # Chasser A du cache GPU : le KV tient ~500 k jetons, on pousse | |
| # 5 x 133 k = 665 k de prompts distincts. | |
| for i in range(2, 7): | |
| appel(prompt_sale(i), 1) | |
| _, _, d3, _ = appel(A, 1) | |
| r["prefill_evince"] = round(pA / d3) | |
| # 1,0 = rien recupere, > 1 = le dechargement sert vraiment. | |
| r["gain_eviction"] = round(d1 / d3, 2) | |
| r["hit"] = metrique("gpu_prefix_cache_hit_rate") | |
| qualite(r) | |
| except Exception as e: | |
| r["erreur"] = repr(e)[:200] | |
| return r | |
| # ------------------------------------------------------- configurations | |
| ov_indexer, note_indexer = surcharge_indexer(0.5) | |
| CONFIGS = [ | |
| # --- LE GOULOT DEMONTRE : la capacite. 0.93 rend 12,26 Gio ; le "fully | |
| # utilize" a 17,92 Gio avait tue le serveur. On sonde l'intervalle. | |
| ("kvmem15", {"--kv-cache-memory-bytes": 15 * GIO}, [], [], False), | |
| ("kvmem17", {"--kv-cache-memory-bytes": 17 * GIO}, [], [], False), | |
| # --- le dechargement : spectaculaire ou nul, rien entre les deux | |
| ("kvoff8", {"--kv-offloading-size": "8"}, [], [], True), | |
| # --- 166 blocs Pickle+SHA256 par prompt de 133 k, dans le chemin chaud | |
| ("xxhash", {"--prefix-caching-hash-algo": "xxhash"}, [], [], False), | |
| # --- le seul levier qui reduise le TRAVAIL de prefill | |
| ("indexer", {"--hf-overrides": ov_indexer} if ov_indexer else None, | |
| [], [], False), | |
| # --- granularite des succes de prefixe (bloc force a 800 pour Mamba) | |
| ("matchunit", {"--prefix-match-unit": "64"}, [], [], False), | |
| # --- partage de KV entre couches : le nom promet exactement notre regime | |
| ("kvshare", {}, ["--kv-sharing-fast-prefill"], [], False), | |
| ] | |
| resultats = [] | |
| dire("=== indexer_budget : %s" % note_indexer) | |
| dire("=== REFERENCE (socle 0.93 + autotune) -- relance propre") | |
| arreter() | |
| ok, motif = lancer("base", {}, [], []) | |
| if ok: | |
| ref = banc("base", eviction=True) | |
| resultats.append(ref) | |
| dire(" base : %s" % json.dumps(ref, ensure_ascii=False)) | |
| else: | |
| ref = {} | |
| dire(" base NE DEMARRE PAS -- %s" % motif) | |
| resultats.append({"nom": "base", "echec": motif}) | |
| for nom, remplace, ajout, retire, evic in CONFIGS: | |
| if remplace is None: | |
| dire("=== %s : IGNOREE (%s)" % (nom, note_indexer)) | |
| resultats.append({"nom": nom, | |
| "echec": "non applicable : " + str(note_indexer)}) | |
| continue | |
| if time.time() - DEBUT > PLAFOND: | |
| dire("!!! plafond de temps -- on assemble et on rend le serveur") | |
| break | |
| dire("=== %s : %s %s" % (nom, remplace, ajout)) | |
| arreter() | |
| ok, motif = lancer(nom, remplace, ajout, retire) | |
| if not ok: | |
| dire(" %s : NE DEMARRE PAS -- %s" % (nom, motif)) | |
| resultats.append({"nom": nom, "echec": motif}) | |
| continue | |
| r = banc(nom, eviction=evic) | |
| resultats.append(r) | |
| dire(" %s : %s" % (nom, json.dumps(r, ensure_ascii=False))) | |
| with open("/travail/balayage_cache.json", "w") as f: | |
| json.dump(resultats, f, indent=1, ensure_ascii=False) | |
| # ------------------------------------------------- assemblage et remise | |
| def trouve(nom): | |
| for r in resultats: | |
| if r["nom"] == nom and "echec" not in r and "erreur" not in r: | |
| return r | |
| return None | |
| final_opts, final_fanions, raisons = {}, [], [] | |
| agg_ref = (ref or {}).get("agg8") or 0 | |
| kv_ref = (ref or {}).get("kv") or 0 | |
| # La capacite d'abord : le plus gros KV qui a demarre ET qui ne perd pas plus | |
| # de 5 % d'agrege. Un cache plus grand ne vaut rien s'il ralentit. | |
| for nom, octets in (("kvmem17", 17 * GIO), ("kvmem15", 15 * GIO)): | |
| r = trouve(nom) | |
| if r and r.get("kv") and (r.get("agg8") or 0) >= 0.95 * agg_ref \ | |
| and r["kv"] > kv_ref: | |
| final_opts["--kv-cache-memory-bytes"] = octets | |
| raisons.append("%s (kv %d vs %d, agg8 %s vs %s)" | |
| % (nom, r["kv"], kv_ref, r["agg8"], agg_ref)) | |
| break | |
| # Puis ce qui rend du debit sans rien couter. | |
| for nom, cle, val in (("xxhash", "--prefix-caching-hash-algo", "xxhash"), | |
| ("matchunit", "--prefix-match-unit", "64")): | |
| r = trouve(nom) | |
| if r and r.get("agg8") and r["agg8"] > agg_ref * 1.01: | |
| final_opts[cle] = val | |
| raisons.append("%s (agg8 %s > %s)" % (nom, r["agg8"], agg_ref)) | |
| # Le dechargement : seulement s'il recupere vraiment apres eviction. | |
| r = trouve("kvoff8") | |
| if r and (r.get("gain_eviction") or 0) > 1.15: | |
| final_opts["--kv-offloading-size"] = "8" | |
| raisons.append("kvoff8 (gain apres eviction %s)" % r["gain_eviction"]) | |
| r = trouve("kvshare") | |
| if r and r.get("prefill_froid") and (ref or {}).get("prefill_froid") \ | |
| and r["prefill_froid"] > 1.05 * ref["prefill_froid"]: | |
| final_fanions.append("--kv-sharing-fast-prefill") | |
| raisons.append("kvshare (prefill froid %s > %s)" | |
| % (r["prefill_froid"], ref["prefill_froid"])) | |
| dire("=== CONFIG FINALE : %s" % (", ".join(raisons) or "reference telle quelle")) | |
| arreter() | |
| ok, motif = lancer("final", final_opts, final_fanions, []) | |
| if not ok: | |
| dire(" la config finale ne demarre pas (%s) -- retour a la reference" % motif) | |
| arreter() | |
| ok, motif = lancer("repli", {}, [], []) | |
| dire(" repli : %s %s" % ("OK" if ok else "ECHEC", motif)) | |
| final_opts, final_fanions = {}, [] | |
| kv, conc = kv_annonce() | |
| dire("=== SERVEUR RENDU : kv=%s jetons, concurrence %sx, options=%s %s" | |
| % (kv, conc, final_opts, final_fanions)) | |
| print("\n\n########## TABLEAU ##########") | |
| ENT = "%-10s %9s %5s %6s %7s %8s %8s %6s %4s %6s" | |
| print(ENT % ("config", "kv", "conc", "solo", "agg8", "pref_fr", "pref_ev", | |
| "gain", "cjk", "div4")) | |
| for r in resultats: | |
| if "echec" in r: | |
| print("%-10s NE DEMARRE PAS : %s" % (r["nom"], r["echec"][:95])) | |
| elif "erreur" in r: | |
| print("%-10s ERREUR : %s" % (r["nom"], r["erreur"][:95])) | |
| else: | |
| print(ENT % (r["nom"], r.get("kv"), r.get("conc"), r.get("solo"), | |
| r.get("agg8"), r.get("prefill_froid"), | |
| r.get("prefill_evince", "-"), r.get("gain_eviction", "-"), | |
| r.get("cjk"), r.get("div4"))) | |
| print("\n gain = prefill apres eviction / prefill froid. 1,0 = rien recupere.") | |
| print(" cjk = ideogrammes dans 3 generations FRANCAISES (2 a temperature 1,0).") | |
| print(" Zero est le seul resultat acceptable.") | |
| print("\n########## FUITES CJK OBSERVEES ##########") | |
| vu = False | |
| for r in resultats: | |
| if r.get("fuite_cjk"): | |
| vu = True | |
| print(" %-10s ...%s..." % (r["nom"], r["fuite_cjk"])) | |
| if not vu: | |
| print(" aucune") | |