patdev/k3-a40-bootstrap
01.3k
1#!/bin/bash2# Valide un checkpoint Flash-Next sur un GPU HF Jobs, SANS toucher au pod de3# production. Charge, mesure le pool KV, verifie la qualite du texte et une4# aiguille longue, puis sort. Concu pour tenir dans un job de 45 min.5set -u6MODELE="${VL_MODEL:-patdev/Qwen3.8-Flash-Next-NVFP4-W8A16attn}"7CTX="${VL_CTX:-524288}"8KVB="${KVBYTES:-18239700172}"9REF="${VL_REF:-}" # si non vide : depot de reference pour comparaison10 11echo "=== validation de $MODELE (ctx=$CTX, KV=$KVB) ==="12nvidia-smi --query-gpu=name,memory.total --format=csv,noheader13mkdir -p /travail && cd /travail14 15# Le correctif PLE est indispensable : sans lui le processus CPU du worker16# refuse ngram_embedding.weight_scale.17curl -sL "https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/patch_ple.py" -o patch_ple.py18python3 patch_ple.py || { echo "ECHEC: correctif PLE refuse"; exit 1; }19 20# Instrumentation optionnelle : VL_DUMP=1 installe un sitecustomize.py sur le21# PYTHONPATH, donc importe par TOUS les processus vLLM (y compris les workers22# multiproc, ou l'echec se produit). Il imprime le prefixe que ModelOpt cherche23# reellement, au lieu de nous laisser deviner.24if [ "${VL_DUMP:-0}" = "1" ]; then25 mkdir -p /travail/instr26 curl -sL "https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/sitecustomize.py" \27 -o /travail/instr/sitecustomize.py28 export PYTHONPATH="/travail/instr${PYTHONPATH:+:$PYTHONPATH}"29 echo "[instrumentation ModelOpt active]"30fi31 32export VLLM_PLE_CPU_OFFLOAD=133export VLLM_PLE_OFFLOAD_READY_TIMEOUT=180034export VLLM_ALLOW_LONG_MAX_MODEL_LEN=135YARN='{"text_config":{"mrope_interleaved":true,"rope_parameters":{"mrope_interleaved":true,"mrope_section":[11,11,10],"rope_type":"yarn","rope_theta":10000000,"partial_rotary_factor":0.25,"factor":2.0,"original_max_position_embeddings":262144}}}'36 37setsid nohup vllm serve "$MODELE" \38 --served-model-name flashnext --host 127.0.0.1 --port 8000 --trust-remote-code \39 --max-model-len "$CTX" --max-num-seqs 4 --enable-prefix-caching \40 --gpu-memory-utilization 0.93 --distributed-executor-backend mp \41 --reasoning-parser qwen3 --tool-call-parser qwen3_coder --enable-auto-tool-choice \42 --enable-flashinfer-autotune --limit-mm-per-prompt '{"image":4,"video":0}' \43 --kv-cache-memory-bytes "$KVB" \44 --hf-overrides "$YARN" \45 > /travail/vllm.log 2>&1 < /dev/null &46 47PRET=048for i in $(seq 1 110); do49 sleep 2050 if curl -sf --max-time 5 http://127.0.0.1:8000/v1/models >/dev/null 2>&1; then PRET=1; break; fi51 # NE PAS declencher sur "Traceback" : vLLM en emet un benin depuis son52 # thread de telemetrie (usage_lib -> cpuinfo -> JSONDecodeError) dans tout53 # conteneur. Cette detection trop large a tue un demarrage SAIN le 05/09.54 # On ne retient que les marqueurs reellement fatals pour le moteur.55 if grep -aq "Engine core initialization failed\|EngineDeadError\|WorkerProc initialization failed" /travail/vllm.log; then56 echo "=== ECHEC AU CHARGEMENT (t=$((i*20))s) ==="57 # La cause racine est dans le PREMIER traceback du worker, pas dans le58 # tail : celui-ci ne contient que la remontee EngineCore/APIServer.59 echo "--- premier bloc d'erreur ---"60 awk '/ERROR|Traceback/{f=1} f' /travail/vllm.log | head -7061 # Et on publie le log entier : relancer un GPU juste pour lire une trace62 # coute plus cher que de la mettre de cote.63 if [ -n "${HF_TOKEN:-}" ]; then64 python3 - <<'PYU'65import os66from huggingface_hub import HfApi67HfApi().upload_file(path_or_fileobj="/travail/vllm.log",68 path_in_repo="etat/validation-vllm.log",69 repo_id="patdev/k3-a40-bootstrap",70 commit_message="log complet de validation",71 token=os.environ["HF_TOKEN"])72PYU73 echo "log complet : https://huggingface.co/patdev/k3-a40-bootstrap/resolve/main/etat/validation-vllm.log"74 fi75 exit 176 fi77 [ $((i % 9)) -eq 0 ] && echo " ... $((i*20))s, GPU $(nvidia-smi --query-gpu=memory.used --format=csv,noheader)"78done79[ "$PRET" = "1" ] || { echo "=== TIMEOUT ==="; tail -c 8000 /travail/vllm.log; exit 1; }80 81echo "=== CHARGE ==="82grep -aE "Model loading took|GPU KV cache size|Maximum concurrency|Graph capturing|MIXED|MixedPrecision|modelopt" /travail/vllm.log | tail -883 84echo "=== QUALITE : reponses courtes, reflexion coupee ==="85python3 - <<'PY'86import json, urllib.request87U = "http://127.0.0.1:8000/v1/chat/completions"88tests = [89 ("capitale", "Quelle est la capitale de la Suisse ? Une phrase.", ["berne", "bern"]),90 ("calcul", "Combien font 17 * 23 ? Reponds juste le nombre.", ["391"]),91 ("code", "Ecris une fonction Python qui renvoie le n-ieme nombre de Fibonacci. Code seul.", ["def", "fib"]),92 ("francais", "Explique en deux phrases ce qu'est une liste chainee.", ["liste", "element"]),93]94ok = 095for nom, prompt, attendus in tests:96 body = json.dumps({"model": "flashnext", "max_tokens": 220,97 "chat_template_kwargs": {"enable_thinking": False},98 "messages": [{"role": "user", "content": prompt}]}).encode()99 r = urllib.request.Request(U, data=body, headers={"Content-Type": "application/json"})100 d = json.load(urllib.request.urlopen(r, timeout=180))101 t = (d["choices"][0]["message"].get("content") or "").strip()102 bon = any(a in t.lower() for a in attendus)103 ok += bon104 # Un signe classique de checkpoint casse : caracteres non latins ou repetition.105 latin = sum(c.isascii() or c in "éèêàçùôîû" for c in t) / max(len(t), 1)106 print(f" [{'OK ' if bon else 'NON'}] {nom:<9} latin={latin:.0%} | {t[:110]!r}")107print(f" -> {ok}/{len(tests)} reponses correctes")108PY109 110echo "=== AIGUILLE A 150K ==="111python3 - <<'PY'112import json, urllib.request, time113U = "http://127.0.0.1:8000/v1/chat/completions"114bourre = "Le systeme enregistre des evenements de routine sans importance particuliere. "115aiguille = "IMPORTANT : le code de verification du coffre est ZEPHYR-8412."116n = 150000 // 18117texte = bourre * (n // 2) + aiguille + " " + bourre * (n // 2)118body = json.dumps({"model": "flashnext", "max_tokens": 60,119 "chat_template_kwargs": {"enable_thinking": False},120 "messages": [{"role": "user",121 "content": texte + "\n\nQuel est le code de verification du coffre ?"}]}).encode()122t0 = time.time()123r = urllib.request.Request(U, data=body, headers={"Content-Type": "application/json"})124d = json.load(urllib.request.urlopen(r, timeout=600))125t = (d["choices"][0]["message"].get("content") or "").strip()126u = d["usage"]127trouve = "ZEPHYR-8412" in t128print(f" entree={u['prompt_tokens']} jetons | {time.time()-t0:.1f}s | aiguille {'TROUVEE' if trouve else 'MANQUEE'}")129print(f" reponse: {t[:160]!r}")130PY131 132echo "=== FIN DE VALIDATION ==="133 