Download server.py from Yougori/Ling-mini-2.0-Megakernel: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/Yougori/Ling-mini-2.0-Megakernel/resolve/main/server.py
- Command line
-
hf download hf://Yougori/Ling-mini-2.0-Megakernel/server.py
-
curl -L -o server.py https://huggingface.co/Yougori/Ling-mini-2.0-Megakernel/resolve/main/server.py
14.8 kB
| """Several chats at once on the Ling-mini-2.0 megakernel: a local web app. | |
| python server.py [--port 8790] | |
| Then open http://127.0.0.1:8790. Every pane is its own conversation with its own cache. One thread | |
| owns the GPU and works in rounds: each round, every chat that is reading a prompt or writing an | |
| answer gets its own launch of the megakernel on a share of the SMs (82 / chats working, at most 5 | |
| at once; more wait their turn), all launches run together, and their words go out to the panes as | |
| the round ends. One chat alone gets the whole GPU (~850 words/s); three together ~460 each. | |
| Greedy decoding, thinking off, up to 4,096 tokens per conversation. Binds to 127.0.0.1 only. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import queue | |
| import sys | |
| import threading | |
| import time | |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | |
| from pathlib import Path | |
| from urllib.parse import parse_qs, urlparse | |
| import torch | |
| from ling_kernel import HERE, NQ, LingKernel, State | |
| CHUNK = 32 # answer tokens per launch | |
| PREFILL = 64 # prompt tokens per launch (keeps rounds short while others are writing) | |
| MAX_AT_ONCE = 5 # chats working in the same round (16 SMs each at 5: one attention block per head) | |
| MAX_POS = 4096 | |
| MAX_NEW = 1024 | |
| PANES = 8 # conversations the page can open | |
| class Chat: | |
| """One conversation: its GPU state (KV cache and buffers), its history, its listeners.""" | |
| def __init__(self, mk: LingKernel, cid: int): | |
| self.mk, self.cid = mk, cid | |
| self.state = State(MAX_POS) | |
| self.out = self.state.out | |
| self.stream = torch.cuda.Stream() | |
| self.history: list[dict] = [] | |
| self.cached: list[int] = [] # tokens that have cache entries, in order | |
| self.listeners: list[queue.Queue] = [] | |
| self.job: dict | None = None # the answer being written | |
| def launch(self, ins: list[int], pos0: int, n: int, forced: bool, nblocks: int) -> None: | |
| self.mk.launch_async(self.state, ins, pos0, n, forced, nblocks, self.stream) | |
| def send(self, event: dict) -> None: | |
| for q in list(self.listeners): | |
| q.put(event) | |
| class Server: | |
| def __init__(self) -> None: | |
| from transformers import AutoTokenizer | |
| self.tok = AutoTokenizer.from_pretrained(HERE) | |
| print("loading the Ling-mini-2.0 megakernel (4-bit) ...", flush=True) | |
| self.mk = LingKernel(max_pos=MAX_POS, log=lambda s: print(s, flush=True)) | |
| self.sms = self.mk.sms | |
| self.eos = {i for i in (self.tok.convert_tokens_to_ids("<|role_end|>"), self.tok.eos_token_id, | |
| self.mk.config.get("eos_token_id")) if isinstance(i, int) and i >= 0} | |
| self.chats = [Chat(self.mk, i) for i in range(PANES)] | |
| self.scratch = Chat(self.mk, -1) # warms the GPU's clocks without touching a conversation | |
| self.lock = threading.Lock() | |
| self.wake = threading.Event() | |
| self.recent: list[tuple[float, int]] = [] # (time, tokens) of finished rounds, for the total rate | |
| self.last_work = 0.0 | |
| threading.Thread(target=self.loop, daemon=True).start() | |
| # ---- requests (HTTP threads) ---- | |
| def start(self, cid: int, text: str) -> str | None: | |
| with self.lock: | |
| c = self.chats[cid] | |
| if c.job is not None: | |
| return "this chat is still writing" | |
| c.job = {"text": text, "state": "new"} | |
| c.send({"type": "user", "text": text}) # every open page shows it | |
| self.wake.set() | |
| return None | |
| def reset(self, cid: int) -> str | None: | |
| with self.lock: | |
| c = self.chats[cid] | |
| if c.job is not None: | |
| return "this chat is still writing" | |
| c.history, c.cached = [], [] | |
| c.send({"type": "cleared"}) | |
| return None | |
| def transcript(self, cid: int) -> dict: | |
| with self.lock: | |
| c = self.chats[cid] | |
| job = c.job | |
| msgs = list(c.history) | |
| if job is not None: | |
| msgs.append({"role": "user", "content": job["text"]}) | |
| msgs.append({"role": "assistant", "content": job.get("shown", "")}) | |
| return {"messages": msgs, "writing": job is not None} | |
| def total_rate(self) -> float: | |
| now = time.perf_counter() | |
| with self.lock: | |
| self.recent = [(t, n) for t, n in self.recent if now - t < 1.0] | |
| n = sum(k for _, k in self.recent) | |
| return n / 1.0 | |
| # ---- the GPU loop (one thread) ---- | |
| def begin(self, c: Chat) -> None: | |
| """Turn a new message into prompt tokens; keep the cache's common prefix.""" | |
| job = c.job | |
| messages = c.history + [{"role": "user", "content": job["text"]}] | |
| ids = self.tok.apply_chat_template(messages, tokenize=True, add_generation_prompt=True) | |
| if hasattr(ids, "keys"): | |
| ids = ids["input_ids"] | |
| ids = list(ids) | |
| if len(ids) + 2 * CHUNK + 1 > MAX_POS: | |
| c.job = None | |
| c.send({"type": "error", "text": f"this conversation is full ({MAX_POS} tokens): press Clear"}) | |
| return | |
| n = 0 | |
| while n < min(len(ids) - 1, len(c.cached)) and ids[n] == c.cached[n]: | |
| n += 1 | |
| job.update(state="read", messages=messages, ids=ids, pos=n, out=[], shown="", seconds=0.0, steps=0, | |
| max_new=min(MAX_NEW, MAX_POS - len(ids) - CHUNK)) | |
| c.send({"type": "start", "prompt_tokens": len(ids) - n}) | |
| def finish(self, c: Chat, note: str = "") -> None: | |
| job = c.job | |
| text = job["shown"].strip() | |
| c.history = job["messages"] + [{"role": "assistant", "content": text}] | |
| c.cached = job["ids"] + job["out"][:-1] | |
| rate = job["steps"] / job["seconds"] if job["seconds"] else 0.0 | |
| c.send({"type": "done", "tokens": len(job["out"]), "rate": rate, "note": note}) | |
| c.job = None | |
| def warm(self) -> None: | |
| """After the GPU idled, Windows lowers its clocks: run the scratch chat until launches are fast.""" | |
| start = time.perf_counter() | |
| best = float("inf") | |
| while time.perf_counter() - start < 2.0: | |
| t = time.perf_counter() | |
| self.scratch.launch([1000], 0, CHUNK, False, self.sms) | |
| torch.cuda.synchronize() | |
| d = time.perf_counter() - t | |
| if d > 0.97 * best and d < 1.2 * best: | |
| break | |
| best = min(best, d) | |
| def loop(self) -> None: | |
| while True: | |
| with self.lock: | |
| for c in self.chats: | |
| if c.job is not None and c.job["state"] == "new": | |
| self.begin(c) | |
| working = [c for c in self.chats if c.job is not None][:MAX_AT_ONCE] | |
| if not working: | |
| self.wake.wait(0.5) | |
| self.wake.clear() | |
| continue | |
| if time.perf_counter() - self.last_work > 1.0: | |
| self.warm() | |
| nb = max(NQ, self.sms // len(working)) | |
| plan = [] | |
| try: | |
| for c in working: | |
| job = c.job | |
| if job["state"] == "read": | |
| k = min(PREFILL, len(job["ids"]) - job["pos"]) | |
| c.launch(job["ids"][job["pos"]:job["pos"] + k], job["pos"], k, True, nb) | |
| else: | |
| k = min(CHUNK, job["max_new"] - len(job["out"])) | |
| c.launch([job["out"][-1]], job["pos"], k, False, nb) | |
| plan.append((c, k)) | |
| t = time.perf_counter() | |
| torch.cuda.synchronize() | |
| dt = time.perf_counter() - t | |
| except Exception as e: # report it and drop the round's jobs | |
| torch.cuda.synchronize() | |
| with self.lock: | |
| for c, _ in plan or [(c, 0) for c in working]: | |
| if c.job is not None: | |
| c.send({"type": "error", "text": f"GPU error: {e}"}) | |
| c.job = None | |
| continue | |
| self.last_work = time.perf_counter() | |
| made = 0 | |
| with self.lock: | |
| for c, k in plan: | |
| job = c.job | |
| outs = c.out[:k].tolist() | |
| if job["state"] == "read": | |
| job["pos"] += k | |
| if job["pos"] == len(job["ids"]): # the prompt is read: its last step's word is the first answer word | |
| job["state"] = "write" | |
| job["out"] = [outs[k - 1]] | |
| made += 1 | |
| self.emit(c) | |
| if outs[k - 1] in self.eos: | |
| self.finish(c) | |
| continue | |
| job["seconds"] += dt | |
| job["steps"] += k | |
| job["pos"] += k | |
| for t_ in outs: | |
| job["out"].append(t_) | |
| made += 1 | |
| if t_ in self.eos: | |
| break | |
| self.emit(c) | |
| if job["out"][-1] in self.eos: | |
| self.finish(c) | |
| elif len(job["out"]) >= job["max_new"]: | |
| self.finish(c, "stopped at the length limit") | |
| self.recent.append((time.perf_counter(), made)) | |
| def emit(self, c: Chat) -> None: | |
| job = c.job | |
| words = [t for t in job["out"] if t not in self.eos] | |
| text = self.tok.decode(words, skip_special_tokens=True) | |
| if text.endswith("�"): # half of a character: wait for the rest | |
| text = text.rstrip("�") | |
| if len(text) > len(job["shown"]) and text.startswith(job["shown"]): | |
| c.send({"type": "delta", "text": text[len(job["shown"]):]}) | |
| job["shown"] = text | |
| elif text != job["shown"] and not text.startswith(job["shown"]): | |
| c.send({"type": "replace", "text": text}) | |
| job["shown"] = text | |
| def handler(server: Server): | |
| page = (HERE / "multichat.html").read_bytes() | |
| class H(BaseHTTPRequestHandler): | |
| def log_message(self, *a): # quiet | |
| pass | |
| def reply(self, code: int, body: bytes, ctype: str = "application/json") -> None: | |
| self.send_response(code) | |
| self.send_header("Content-Type", ctype) | |
| self.send_header("Content-Length", str(len(body))) | |
| self.send_header("Cache-Control", "no-store") | |
| self.end_headers() | |
| self.wfile.write(body) | |
| def do_GET(self): | |
| u = urlparse(self.path) | |
| if u.path == "/": | |
| return self.reply(200, page, "text/html; charset=utf-8") | |
| if u.path == "/history": | |
| cid = int(parse_qs(u.query).get("chat", ["0"])[0]) | |
| if not 0 <= cid < PANES: | |
| return self.reply(404, b"{}") | |
| return self.reply(200, json.dumps(server.transcript(cid)).encode()) | |
| if u.path == "/status": | |
| with server.lock: | |
| busy = [c.cid for c in server.chats if c.job is not None] | |
| return self.reply(200, json.dumps({"total_rate": server.total_rate(), "busy": busy, | |
| "sms": server.sms, "panes": PANES}).encode()) | |
| if u.path == "/events": | |
| cid = int(parse_qs(u.query).get("chat", ["0"])[0]) | |
| if not 0 <= cid < PANES: | |
| return self.reply(404, b"{}") | |
| q: queue.Queue = queue.Queue() | |
| c = server.chats[cid] | |
| with server.lock: | |
| c.listeners.append(q) | |
| self.send_response(200) | |
| self.send_header("Content-Type", "text/event-stream") | |
| self.send_header("Cache-Control", "no-store") | |
| self.end_headers() | |
| try: | |
| self.wfile.write(b": open\n\n") | |
| self.wfile.flush() | |
| while True: | |
| try: | |
| ev = q.get(timeout=15) | |
| self.wfile.write(f"data: {json.dumps(ev)}\n\n".encode()) | |
| except queue.Empty: | |
| self.wfile.write(b": keep-alive\n\n") | |
| self.wfile.flush() | |
| except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError): | |
| pass | |
| finally: | |
| with server.lock: | |
| if q in c.listeners: | |
| c.listeners.remove(q) | |
| return | |
| return self.reply(404, b"{}") | |
| def do_POST(self): | |
| u = urlparse(self.path) | |
| n = int(self.headers.get("Content-Length", "0") or 0) | |
| try: | |
| body = json.loads(self.rfile.read(n) or b"{}") | |
| cid = int(body.get("chat", -1)) | |
| except (ValueError, TypeError): | |
| return self.reply(400, b'{"error": "bad request"}') | |
| if not 0 <= cid < PANES: | |
| return self.reply(404, b'{"error": "no such chat"}') | |
| if u.path == "/send": | |
| text = str(body.get("text", "")).strip() | |
| if not text: | |
| return self.reply(400, b'{"error": "empty message"}') | |
| err = server.start(cid, text) | |
| elif u.path == "/reset": | |
| err = server.reset(cid) | |
| else: | |
| return self.reply(404, b"{}") | |
| if err: | |
| return self.reply(409, json.dumps({"error": err}).encode()) | |
| return self.reply(200, b'{"ok": true}') | |
| return H | |
| def main() -> None: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--port", type=int, default=8790) | |
| args = ap.parse_args() | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| server = Server() | |
| httpd = ThreadingHTTPServer(("127.0.0.1", args.port), handler(server)) | |
| httpd.daemon_threads = True | |
| print(f"open http://127.0.0.1:{args.port} ({server.sms} SMs; up to {MAX_AT_ONCE} chats writing at once)", flush=True) | |
| httpd.serve_forever() | |
| if __name__ == "__main__": | |
| main() | |