overnight_bench.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. #!/usr/bin/env python3
  2. """Controlled decode benchmark for the overnight TP3 campaign.
  3. Token counts come from the server's usage block (completion_tokens), never from
  4. the number of SSE events. Timing uses the first streamed content/reasoning delta
  5. as the first-token time, so:
  6. decode tok/s = (completion_tokens - 1) / (t_last - t_first)
  7. e2e tok/s = completion_tokens / (t_last - t_send)
  8. C4 aggregate = sum(completion_tokens) / (wave wall time), all four requests
  9. sent together. Prints UTC phase boundaries so acceptance/step lines in the head
  10. log can be joined to each phase afterwards.
  11. Usage: BASE_URL=http://10.0.0.1:8888 python3 overnight_bench.py OUT.jsonl [reps] [max_tokens]
  12. """
  13. from __future__ import annotations
  14. import concurrent.futures
  15. import json
  16. import os
  17. import statistics
  18. import sys
  19. import time
  20. import urllib.request
  21. from datetime import datetime, timezone
  22. BASE = os.environ.get("BASE_URL", "http://10.0.0.1:8888").rstrip("/")
  23. MODEL = os.environ.get("SERVED_MODEL_NAME", "deepseek-v4.1-flash")
  24. KEY = os.environ.get("API_KEY", "")
  25. PROSE = ("Write a detailed, flowing essay of about 800 words on the history and engineering "
  26. "of lighthouses, from the Pharos of Alexandria to automated LED beacons. Use full "
  27. "paragraphs only: no lists, no headings, no markdown.")
  28. CODE = ("Write a complete Python module implementing a thread-safe LRU cache using a doubly "
  29. "linked list and a dict, with get, put, delete, resize and a stats() method, full "
  30. "docstrings and type hints, followed by a unittest suite covering eviction order, "
  31. "resizing and concurrent access. Output only the code.")
  32. PROSE2 = ("Tell the story of a small fishing village over one year as the seasons change, following "
  33. "three families. Write it as continuous literary prose of about 800 words, no headings or lists.")
  34. CHAT = ("Explain to a curious fifteen-year-old how vaccines teach the immune system to "
  35. "recognise a virus, including memory B cells and T cells, and why booster shots "
  36. "exist. Be warm and concrete.")
  37. WORKLOADS = {
  38. "prose": dict(prompt=PROSE, temperature=0.0),
  39. "code": dict(prompt=CODE, temperature=0.0),
  40. "chat_sampled": dict(prompt=CHAT, temperature=0.7, top_p=0.95),
  41. "prose2": dict(prompt=PROSE2, temperature=0.0),
  42. }
  43. # C1 workloads to run (prose2 was added at E2; earlier runs have the first three only)
  44. C1_SET = os.environ.get("C1_WORKLOADS", "prose,code,chat_sampled,prose2").split(",")
  45. def now_utc() -> str:
  46. return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
  47. def one(workload: str, max_tokens: int, tag: str) -> dict:
  48. w = WORKLOADS[workload]
  49. body = {
  50. "model": MODEL,
  51. "messages": [{"role": "user", "content": w["prompt"]}],
  52. "max_tokens": max_tokens,
  53. "temperature": w["temperature"],
  54. "stream": True,
  55. "stream_options": {"include_usage": True},
  56. "chat_template_kwargs": {"thinking": False},
  57. }
  58. if "top_p" in w:
  59. body["top_p"] = w["top_p"]
  60. headers = {"Content-Type": "application/json"}
  61. if KEY:
  62. headers["Authorization"] = "Bearer " + KEY
  63. req = urllib.request.Request(BASE + "/v1/chat/completions", data=json.dumps(body).encode(),
  64. headers=headers, method="POST")
  65. t0 = time.perf_counter()
  66. t_first = t_last = None
  67. usage = None
  68. finish = None
  69. text = []
  70. err = None
  71. try:
  72. with urllib.request.urlopen(req, timeout=1800) as r:
  73. for raw in r:
  74. line = raw.decode("utf-8", "replace").strip()
  75. if not line.startswith("data:"):
  76. continue
  77. data = line[5:].strip()
  78. if data == "[DONE]":
  79. break
  80. obj = json.loads(data)
  81. if obj.get("usage"):
  82. usage = obj["usage"]
  83. for ch in obj.get("choices") or []:
  84. d = ch.get("delta") or {}
  85. piece = (d.get("content") or "") + (d.get("reasoning_content") or "")
  86. if piece:
  87. t = time.perf_counter()
  88. if t_first is None:
  89. t_first = t
  90. t_last = t
  91. text.append(piece)
  92. if ch.get("finish_reason"):
  93. finish = ch["finish_reason"]
  94. except Exception as e: # recorded, never silently dropped
  95. err = repr(e)
  96. t_end = time.perf_counter()
  97. ct = (usage or {}).get("completion_tokens")
  98. rec = {"tag": tag, "workload": workload, "max_tokens": max_tokens, "finish": finish,
  99. "completion_tokens": ct, "prompt_tokens": (usage or {}).get("prompt_tokens"),
  100. "ttft_s": None if t_first is None else t_first - t0,
  101. "total_s": t_end - t0, "error": err, "text_head": "".join(text)[:160],
  102. "text_sha": __import__("hashlib").sha256("".join(text).encode()).hexdigest()[:16]}
  103. if ct and t_first is not None and t_last is not None and t_last > t_first and ct > 1:
  104. rec["decode_tps"] = (ct - 1) / (t_last - t_first)
  105. rec["e2e_tps"] = ct / (t_last - t0)
  106. return rec
  107. def summarize(rows, key):
  108. vals = [r[key] for r in rows if r.get(key) is not None]
  109. if not vals:
  110. return None
  111. return {"median": statistics.median(vals), "min": min(vals), "max": max(vals),
  112. "stdev": statistics.pstdev(vals) if len(vals) > 1 else 0.0, "n": len(vals)}
  113. def main():
  114. out = sys.argv[1]
  115. reps = int(sys.argv[2]) if len(sys.argv) > 2 else 5
  116. max_tokens = int(sys.argv[3]) if len(sys.argv) > 3 else 512
  117. phases = os.environ.get("PHASES", "c1,c4").split(",")
  118. rows = []
  119. summary = {"base": BASE, "reps": reps, "max_tokens": max_tokens, "phases": {}}
  120. def log(r):
  121. rows.append(r)
  122. with open(out, "a") as f:
  123. f.write(json.dumps(r) + "\n")
  124. # Warm-up: one of each workload, discarded, plus one C4 wave.
  125. for w in C1_SET:
  126. one(w, 64, "warmup")
  127. with concurrent.futures.ThreadPoolExecutor(4) as ex:
  128. list(ex.map(lambda w: one(w, 64, "warmup"), ["prose", "code", "prose", "code"]))
  129. if "c1" in phases:
  130. for w in C1_SET:
  131. start = now_utc()
  132. rs = []
  133. for i in range(reps):
  134. r = one(w, max_tokens, f"c1-{w}")
  135. r["rep"] = i
  136. log(r)
  137. rs.append(r)
  138. summary["phases"][f"c1-{w}"] = {
  139. "start": start, "end": now_utc(),
  140. "decode_tps": summarize(rs, "decode_tps"), "e2e_tps": summarize(rs, "e2e_tps"),
  141. "ttft_s": summarize(rs, "ttft_s"),
  142. "completion_tokens": [r["completion_tokens"] for r in rs],
  143. "distinct_outputs": len({r["text_sha"] for r in rs}),
  144. "errors": [r["error"] for r in rs if r["error"]]}
  145. if "c4" in phases:
  146. start = now_utc()
  147. waves = []
  148. mix = ["prose", "code", "prose", "code"]
  149. for i in range(reps):
  150. t0 = time.perf_counter()
  151. with concurrent.futures.ThreadPoolExecutor(4) as ex:
  152. rs = list(ex.map(lambda w: one(w, max_tokens, "c4"), mix))
  153. wall = time.perf_counter() - t0
  154. toks = sum(r["completion_tokens"] or 0 for r in rs)
  155. for r in rs:
  156. r["wave"] = i
  157. log(r)
  158. waves.append({"wall_s": wall, "tokens": toks, "agg_tps": toks / wall,
  159. "per_stream_decode": [r.get("decode_tps") for r in rs],
  160. "errors": [r["error"] for r in rs if r["error"]]})
  161. summary["phases"]["c4"] = {
  162. "start": start, "end": now_utc(),
  163. "agg_tps": summarize(waves, "agg_tps"),
  164. "per_stream_decode_median": statistics.median(
  165. [x for w in waves for x in w["per_stream_decode"] if x]),
  166. "waves": waves}
  167. with open(out.replace(".jsonl", "") + ".summary.json", "w") as f:
  168. json.dump(summary, f, indent=1)
  169. for k, v in summary["phases"].items():
  170. if k == "c4":
  171. print(f"{k:16s} agg {v['agg_tps']['median']:.2f} tok/s (min {v['agg_tps']['min']:.2f} max {v['agg_tps']['max']:.2f}) "
  172. f"per-stream {v['per_stream_decode_median']:.2f} {v['start']}..{v['end']}")
  173. else:
  174. d = v["decode_tps"]
  175. print(f"{k:16s} decode {d['median']:.2f} tok/s (min {d['min']:.2f} max {d['max']:.2f} sd {d['stdev']:.2f}) "
  176. f"ttft {v['ttft_s']['median']:.3f}s toks {v['completion_tokens']} distinct {v['distinct_outputs']} "
  177. f"err {len(v['errors'])} {v['start']}..{v['end']}")
  178. if __name__ == "__main__":
  179. main()