# -*- coding: utf-8 -*- """s4_server.py — samai-4b 网页聊天demo (Colab T4, port 7861) [v4, 移植自 r18 v3.2] 协议适配 Spark 模板: - force_think=True(默认): 生成提示 = ...<|Bot|> + "\\n" (与 SFT 训练格式一致) - force_think=False: 生成提示 = ...<|Bot|> + "\\n" (跳过思考) - eos=[1] (<|end▁of▁sentence|>); decode 不剥特殊token, 手工清理模板标记 继承: AntiLoop 复读截断 / best-of-n 投票 / 强制思考开关 / ponder 步数展示 端点: GET / | GET /health | POST /chat {message, history, force_think, n_votes} """ import json, os, re, threading, time, uuid from collections import Counter import torch from flask import Flask, request, jsonify, Response from transformers import AutoModelForCausalLM, AutoTokenizer, StoppingCriteria, StoppingCriteriaList MODEL_DIR = "/content/samai-4b-sft" PORT = 7861 MAX_NEW_THINK = 320 MAX_NEW_AUTO = 192 MAX_PROMPT_TOKENS = 900 EOS_IDS = [1] STATE = {"loaded": False, "error": None, "ckpt": os.path.basename(MODEL_DIR), "t0": time.time()} LOCK = threading.Lock() JOBS = {} JOBS_MU = threading.Lock() MODEL = {"tok": None, "m": None} app = Flask(__name__) class AntiLoop(StoppingCriteria): """末尾片段(L=3..16 token)连续重复 >=3 次判定为复读退化, 提前截断.""" def __call__(self, input_ids, scores, **kwargs): ids = input_ids[0].tolist() tail = ids[-64:] if len(tail) < 9: return False for L in range(3, 17): if len(tail) < 3 * L: break seg = tail[-L:] if seg == tail[-2 * L:-L] == tail[-3 * L:-2 * L]: return True return False def build(): global MODEL, STATE print("[build] loading tokenizer...", flush=True) tok = AutoTokenizer.from_pretrained(MODEL_DIR) print("[build] loading model fp16 -> cuda ...", flush=True) model = AutoModelForCausalLM.from_pretrained( MODEL_DIR, trust_remote_code=True, dtype=torch.float16).cuda().eval() MODEL["tok"], MODEL["m"] = tok, model STATE["loaded"] = True print("[build] DONE", type(model).__name__, flush=True) MARKS = ["", "", "<|User|>", "<|Bot|>", "<|System|>", "<|Tool|>", "<|start▁of▁sentence|>", "<|end▁of▁sentence|>", "<|▁pad▁|>", "<|start▁of▁text|>", "<|end▁of▁text|>", ""] def build_inputs(msgs, force_think): tok = MODEL["tok"] text = tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False, enable_thinking=force_think) # force=True: ...<|Bot|> + "\n"; force=False: ...<|Bot|> + "\n" text += "\n" return tok(text, return_tensors="pt", return_dict=True, add_special_tokens=False) def split_think(text): text = text.split("<|end▁of▁sentence|>")[0] def clean(s): for mk in MARKS: s = s.replace(mk, "") return s.strip() if "" in text and "" in text: a, b = text.split("", 1) th, rest = b.split("", 1) return th.strip(), clean(a + rest) if "" in text: return text.split("", 1)[1].strip(), "" if "" in text: th, rest = text.split("", 1) return th.strip(), clean(rest) return "", clean(text) @app.route("/health") def health(): gpu = "" try: import subprocess r = subprocess.run(["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader"], capture_output=True, text=True) gpu = r.stdout.strip().splitlines()[0] if r.stdout.strip() else "" except Exception: pass return jsonify({"loaded": STATE["loaded"], "error": STATE["error"], "ckpt": STATE["ckpt"], "uptime_s": round(time.time() - STATE["t0"]), "gpu_mem_used": gpu}) @app.route("/chat", methods=["POST"]) def chat(): if not STATE["loaded"]: return jsonify({"error": "model loading"}), 503 d = request.get_json(force=True) msg = (d.get("message") or "").strip() history = d.get("history") or [] force = bool(d.get("force_think", True)) try: n_votes = max(1, min(5, int(d.get("n_votes") or 1))) except Exception: n_votes = 1 if not msg: return jsonify({"error": "empty"}), 400 msgs = [m for m in history if m.get("role") in ("user", "assistant") and m.get("content")] msgs = msgs[-8:] + [{"role": "user", "content": msg}] jid = uuid.uuid4().hex[:12] with JOBS_MU: JOBS[jid] = {"status": "running", "reply": "", "think": "", "steps": None, "elapsed_s": 0, "error": None} if len(JOBS) > 32: for k in [k for k, v in JOBS.items() if v["status"] != "running"][:-16]: JOBS.pop(k, None) threading.Thread(target=run_job, args=(jid, msgs, force, n_votes), daemon=True).start() return jsonify({"job_id": jid}) def run_job(jid, msgs, force_think, n_votes=1): job = JOBS[jid] tok, model = MODEL["tok"], MODEL["m"] with LOCK: try: enc = build_inputs(msgs, force_think) n_in = enc["input_ids"].shape[1] if n_in > MAX_PROMPT_TOKENS: msgs = msgs[-4:] enc = build_inputs(msgs, force_think) n_in = enc["input_ids"].shape[1] enc = {k: v.to(model.device) for k, v in enc.items()} max_new = MAX_NEW_THINK if force_think else MAX_NEW_AUTO temp = 0.6 if force_think else 1.0 def gen_once(): n_log = len(model._ponder_log) t0 = time.time() with torch.no_grad(): out = model.generate(**enc, max_new_tokens=max_new, do_sample=True, temperature=temp, top_p=0.95, repetition_penalty=1.05, pad_token_id=2, eos_token_id=EOS_IDS, stopping_criteria=StoppingCriteriaList([AntiLoop()])) el = round(time.time() - t0, 1) text = tok.decode(out[0][n_in:], skip_special_tokens=False) entries = model._ponder_log[n_log:] steps = None if entries: steps = round(sum(e.get("steps_mean", e.get("executed", 0)) or 0 for e in entries) / len(entries), 2) new_tokens = int(out.shape[1] - n_in) stopped = int(out[0][-1]) in EOS_IDS loop_hit = (not stopped) and (new_tokens < max_new) return text, steps, new_tokens, stopped, loop_hit def vote_key(reply): nums = re.findall(r"\d[\d,]*(?:\.\d+)?", (reply or "").replace(",", "")) if nums: return "n:" + nums[-1] return "t:" + (reply or "").strip()[:40] t0 = time.time() votes = None if n_votes <= 1: text, steps, new_tokens, stopped, loop = gen_once() think, reply = split_think(text) else: rs = [] for _ in range(n_votes): text_i, st, nt, sp, lp = gen_once() th, rp = split_think(text_i) rs.append({"think": th, "reply": rp, "steps": st, "new_tokens": nt, "stopped": sp, "loop": lp}) cnt = Counter(vote_key(r["reply"]) for r in rs) bk = cnt.most_common(1)[0][0] best = next(r for r in rs if vote_key(r["reply"]) == bk) think, reply = best["think"], best["reply"] steps, new_tokens = best["steps"], best["new_tokens"] stopped, loop = best["stopped"], best["loop"] votes = {(k[2:] if k[:2] in ("n:", "t:") else k): c for k, c in cnt.most_common()} el = round(time.time() - t0, 1) job.update({"status": "done", "reply": reply or text[:400], "think": think, "steps": steps, "elapsed_s": el, "new_tokens": new_tokens, "stopped": stopped, "loop": loop, "votes": votes, "mode": ("think" if force_think else "auto") + ("x%d" % n_votes if n_votes > 1 else ""), "temp": temp}) except Exception as e: import traceback traceback.print_exc() job.update({"status": "error", "error": repr(e)[:300]}) @app.route("/result") def result(): jid = request.args.get("id", "") with JOBS_MU: job = JOBS.get(jid) if job is None: return jsonify({"error": "unknown job"}), 404 return jsonify(dict(job)) PAGE = """ samai-4b · chat
samai-4b · pnet-dMoE (Spark-X2.5 骨干)loading…
samai-4b v4 · Spark-X2.5-4B + Pondernet(8专家/后8层) SFT · 强制思考 T=0.6 max320 · 反复读截断 · 🎯投票×5 · eos=[1]
""" @app.route("/") def index(): return Response(PAGE, mimetype="text/html") if __name__ == "__main__": threading.Thread(target=build, daemon=True).start() app.run(host="0.0.0.0", port=PORT, threaded=True)