| |
| """Single-stream decode benchmark for an OpenAI-compatible endpoint (vLLM or SGLang). |
| |
| Discipline, because casual decode benchmarks are usually wrong: |
| * waits for a genuinely idle engine and DISCARDS any run foreign traffic touched mid-flight |
| (a background keepalive will otherwise silently poison your numbers) |
| * forces generation with `ignore_eos` so the run actually decodes instead of stopping early |
| * reports the MEDIAN of N clean runs, never the best |
| * names prompt, token count and clock state on every result |
| |
| NOTE: `ignore_eos` makes this a FLOOR, not real-world throughput -- it pushes generation past the |
| natural stopping point into degenerate text. Real generation on the same stack measured ~33% higher. |
| |
| Engine metric namespaces differ (`vllm:*` vs `sglang:*`) and the names are NOT the same; both are |
| parsed BY NAME. If neither matches, in-flight reads as UNVERIFIED -- never as "idle". |
| |
| Env: BENCH_HOST BENCH_MODEL BENCH_NS BENCH_RUNS BENCH_MAX_TOK BENCH_LABEL BENCH_CLOCK_CMD BENCH_DEPTH |
| """ |
| import json, os, re, subprocess, sys, threading, time, urllib.request |
|
|
| |
| |
| |
| HOST = os.environ.get("BENCH_HOST", "http://127.0.0.1:8899") |
| MODEL = os.environ.get("BENCH_MODEL", "qwen38-flash-next") |
| |
| |
| |
| NS = os.environ.get("BENCH_NS", "sglang") |
| _RUNNING = {"vllm": "vllm:num_requests_running", "sglang": "sglang:num_running_reqs"}[NS] |
| _WAITING = {"vllm": "vllm:num_requests_waiting", "sglang": "sglang:num_queue_reqs"}[NS] |
| RUNS = int(os.environ.get("BENCH_RUNS", "5")) |
| MAX_TOK = int(os.environ.get("BENCH_MAX_TOK", "800")) |
| LABEL = os.environ.get("BENCH_LABEL", "unlabelled") |
| PROMPTS = { |
| "chat": "Explain how consensus works in a distributed database, with examples.", |
| "code": "Write a Python LRU cache with TTL expiry, thread-safe, with tests. Explain the design.", |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| DEPTH = int(os.environ.get("BENCH_DEPTH", "0")) |
| _depth_seq = [0] |
|
|
|
|
| def pad(prompt): |
| """Return (prompt, expected_prefill_tokens). No padding when DEPTH is 0.""" |
| if DEPTH <= 0: |
| return prompt, 0 |
| _depth_seq[0] += 1 |
| |
| |
| words = [] |
| seed = f"run{_depth_seq[0]}x{int(time.time())}" |
| for i in range(DEPTH): |
| words.append(f"{seed}-{i:06d}") |
| filler = " ".join(words)[: DEPTH * 4] |
| return (f"Reference log (ignore it; answer only the question at the end):\n" |
| f"{filler}\n\nQuestion: {prompt}"), DEPTH |
|
|
| def inflight(): |
| try: |
| b = urllib.request.urlopen(HOST + "/metrics", timeout=8).read().decode() |
| r = re.search(r"^" + re.escape(_RUNNING) + r"\{?[^}\s]*\}?\s+([0-9.]+)", b, re.M) |
| w = re.search(r"^" + re.escape(_WAITING) + r"\{?[^}\s]*\}?\s+([0-9.]+)", b, re.M) |
| if r is None or w is None: |
| return None |
| return float(r.group(1)) + float(w.group(1)) |
| except Exception: |
| return None |
|
|
| def wait_idle(timeout=1800): |
| end = time.time() + timeout; streak = 0 |
| while time.time() < end: |
| if inflight() == 0: |
| streak += 1 |
| if streak >= 2: return True |
| else: streak = 0 |
| time.sleep(15) |
| return False |
|
|
| def sample(stop, out): |
| while not stop.is_set(): |
| f = inflight(); out.append(f if f is not None else -1.0); time.sleep(1.5) |
|
|
| def contended(samples): |
| if not samples: return "UNVERIFIED: no samples" |
| foreign = blind = 0 |
| for f in samples: |
| if f < 0: |
| blind += 1; foreign = 0 |
| if blind >= 2: return "UNVERIFIED: /metrics unreadable mid-run" |
| else: |
| blind = 0; foreign = foreign + 1 if f >= 2.0 else 0 |
| if foreign >= 2: return "foreign traffic mid-run" |
| return "" |
|
|
| def one(prompt): |
| prompt, _ = pad(prompt) |
| body = json.dumps({"model": MODEL, "messages": [{"role": "user", "content": prompt}], |
| "max_tokens": MAX_TOK, "min_tokens": MAX_TOK, "ignore_eos": True, |
| "temperature": 0, "stream": False}).encode() |
| s = []; stop = threading.Event() |
| th = threading.Thread(target=sample, args=(stop, s), daemon=True); th.start() |
| t0 = time.time() |
| try: |
| d = json.load(urllib.request.urlopen( |
| urllib.request.Request(HOST + "/v1/chat/completions", body, |
| {"Content-Type": "application/json"}), timeout=600)) |
| finally: |
| stop.set(); th.join(timeout=5) |
| dt = time.time() - t0 |
| u = d["usage"] |
| tok = u["completion_tokens"] |
| if tok < MAX_TOK * 0.9: |
| return None, f"only {tok}/{MAX_TOK} tok generated" |
| |
| |
| cached = (u.get("prompt_tokens_details") or {}).get("cached_tokens", 0) or 0 |
| if DEPTH and cached > u["prompt_tokens"] * 0.2: |
| return None, f"prefix cache served {cached}/{u['prompt_tokens']} prompt tok — depth not real" |
| return (dt, tok, u["prompt_tokens"], cached, accept_length()), contended(s) |
|
|
|
|
| def accept_length(): |
| """sglang:spec_accept_length, BY NAME. None (never 0.0) when absent.""" |
| try: |
| b = urllib.request.urlopen(HOST + "/metrics", timeout=8).read().decode() |
| m = re.search(r"^sglang:spec_accept_length\{[^}]*\}\s+([0-9.eE+-]+)", b, re.M) |
| return float(m.group(1)) if m else None |
| except Exception: |
| return None |
|
|
| def main(): |
| |
| |
| |
| |
| _cmd = os.environ.get("BENCH_CLOCK_CMD", "") |
| if _cmd: |
| _p = subprocess.run(_cmd, shell=True, capture_output=True, text=True) |
| clk = (_p.stdout.strip() or "UNVERIFIED") if _p.returncode == 0 else "UNVERIFIED" |
| else: |
| clk = "UNVERIFIED (set BENCH_CLOCK_CMD)" |
| print(f"=== decode bench :: {LABEL} ===") |
| print(f" prompts=chat,code max_tokens={MAX_TOK} (ignore_eos) runs={RUNS}/prompt clock={clk}") |
| if inflight() != 0: |
| print(" engine busy — waiting for idle...", flush=True) |
| if not wait_idle(): |
| print(" ABORT: never idle. UNVERIFIED."); return 2 |
| out = {} |
| for name, p in PROMPTS.items(): |
| res = []; att = 0 |
| while len(res) < RUNS and att < RUNS * 4: |
| att += 1 |
| if inflight() != 0: |
| if not wait_idle(900): print(" gave up waiting"); break |
| r, why = one(p) |
| if why or r is None: |
| print(f" [{name}] attempt {att}: DISCARDED — {why}", flush=True); continue |
| dt, tok, ptok, cached, al = r |
| res.append((tok / dt, ptok, al)) |
| print(f" [{name}] clean {len(res)}/{RUNS}: {tok/dt:6.2f} tok/s ({dt:.2f}s) " |
| f"prompt={ptok:,} cached={cached} accept_len={al if al is None else round(al,2)}", |
| flush=True) |
| if res: |
| res.sort(key=lambda x: x[0]) |
| mid = res[len(res)//2] |
| als = [x[2] for x in res if x[2] is not None] |
| out[name] = {"tok_s": mid[0], "prompt_tokens": mid[1], |
| "accept_length_median": (sorted(als)[len(als)//2] if als else None)} |
| print() |
| for k, v in out.items(): |
| al = v["accept_length_median"] |
| print(f" {k:5} median {v['tok_s']:.2f} tok/s prompt={v['prompt_tokens']:,} tok " |
| f"accept_len={'UNVERIFIED' if al is None else round(al,2)}") |
| if out: |
| print(f" MEAN of medians: {sum(v['tok_s'] for v in out.values())/len(out):.2f} tok/s") |
| print(json.dumps({"label": LABEL, "clock": clk, "max_tok": MAX_TOK, |
| "depth_requested": DEPTH, "results": out})) |
| return 0 |
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|