decode_window.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #!/usr/bin/env python3
  2. """Mia-style decode-window sweep over the public OpenAI chat path.
  3. Estimates the window for tokens 129-641 from character growth on greedy code
  4. generation at C1/C2/C4/C8, plus wall-clock aggregate tok/s. Requires an idle server.
  5. BASE_URL=http://127.0.0.1:8888 python3 benchmarks/decode_window.py
  6. """
  7. from __future__ import annotations
  8. import concurrent.futures
  9. import json
  10. import os
  11. import statistics
  12. import time
  13. import urllib.request
  14. from datetime import datetime, timezone
  15. from pathlib import Path
  16. try:
  17. from .openai_serving import request_headers
  18. except ImportError: # direct script invocation
  19. from openai_serving import request_headers
  20. BASE = os.environ.get("BASE_URL", "http://127.0.0.1:8888").rstrip("/")
  21. MODEL = os.environ.get("SERVED_MODEL_NAME", "deepseek-v4.1-flash")
  22. OUT = Path(os.environ.get("OUT_DIR", "docs/results/decode-window-"+datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")))
  23. INSTRUCTION = (
  24. "Write a complete Python module implementing an LRU cache with a doubly "
  25. "linked list and dictionary. Include get, put, delete, iteration, resize, "
  26. "clear, invariant validation and detailed docstrings. Then provide ten "
  27. "usage examples. Return code only. Implement all methods fully."
  28. )
  29. def loads():
  30. req = urllib.request.Request(BASE + "/v1/loads", headers=request_headers())
  31. with urllib.request.urlopen(req, timeout=10) as r:
  32. rows = json.load(r).get("loads")
  33. if not rows or any(k not in rows[0] for k in ("num_running_reqs", "num_waiting_reqs")):
  34. raise RuntimeError("cannot verify idle state: missing load counters")
  35. return rows[0]
  36. def require_idle():
  37. snap = loads()
  38. running = snap.get("num_running_reqs") or 0
  39. waiting = snap.get("num_waiting_reqs") or 0
  40. if running or waiting:
  41. raise SystemExit(f"server not idle: running={running} waiting={waiting}")
  42. return snap
  43. def stream_one(index, concurrency):
  44. payload = {
  45. "model": MODEL,
  46. "messages": [{"role": "user", "content": f"{index}:{concurrency}\n{INSTRUCTION}"}],
  47. "temperature": 0,
  48. "max_tokens": 4096,
  49. "stream": True,
  50. "stream_options": {"include_usage": True},
  51. "chat_template_kwargs": {"thinking": False},
  52. }
  53. req = urllib.request.Request(
  54. BASE + "/v1/chat/completions",
  55. data=json.dumps(payload).encode(),
  56. headers=request_headers(),
  57. )
  58. events = []
  59. char_count = 0
  60. ttft = None
  61. done = False
  62. usage = {}
  63. finish = None
  64. start = time.monotonic()
  65. with urllib.request.urlopen(req, timeout=900) as response:
  66. for raw in response:
  67. if not raw.startswith(b"data:"):
  68. continue
  69. data = raw[5:].strip()
  70. if data == b"[DONE]":
  71. done = True
  72. break
  73. event = json.loads(data)
  74. if event.get("error"):
  75. raise RuntimeError("server returned an SSE error")
  76. now = time.monotonic() - start
  77. choice = (event.get("choices") or [{}])[0]
  78. delta = choice.get("delta") or {}
  79. piece = delta.get("content") or ""
  80. if piece:
  81. char_count += len(piece)
  82. if ttft is None:
  83. ttft = now
  84. if choice.get("finish_reason"):
  85. finish = choice.get("finish_reason")
  86. if event.get("usage"):
  87. usage = event["usage"]
  88. events.append({"seconds": now, "chars": char_count, "finish": finish})
  89. if not done or finish is None:
  90. raise RuntimeError("incomplete SSE stream (missing finish reason or [DONE])")
  91. elapsed = time.monotonic() - start
  92. completion = usage.get("completion_tokens")
  93. # Approximate token timeline from char growth if usage arrives only at the end.
  94. # This is a character-scaled estimate, not exact token timestamps.
  95. # Final usage is exact for wall-clock aggregate throughput.
  96. chars = [e["chars"] for e in events]
  97. times = [e["seconds"] for e in events]
  98. window = None
  99. if not isinstance(completion, int) or completion <= 0:
  100. raise RuntimeError("missing positive completion_tokens in final usage")
  101. if completion >= 641 and chars and chars[-1] > 0:
  102. def at_tokens(n):
  103. target = n / completion * chars[-1]
  104. for t, c in zip(times, chars):
  105. if c >= target:
  106. return t
  107. return None
  108. left, right = at_tokens(129), at_tokens(641)
  109. if left is not None and right is not None and right > left:
  110. window = 512 / (right - left)
  111. return {
  112. "index": index,
  113. "concurrency": concurrency,
  114. "seconds": round(elapsed, 3),
  115. "ttft_s": round(ttft, 3) if ttft is not None else None,
  116. "window_method": "character_scaled_estimate",
  117. "window_129_641_tps": round(window, 2) if window else None,
  118. "completion_tokens": completion,
  119. "prompt_tokens": usage.get("prompt_tokens"),
  120. "finish_reason": finish,
  121. "chars": chars[-1] if chars else 0,
  122. "events": len(events),
  123. }
  124. def wave(concurrency):
  125. idle = require_idle()
  126. wall0 = time.monotonic()
  127. with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as pool:
  128. rows = list(pool.map(lambda i: stream_one(i, concurrency), range(concurrency)))
  129. wall = time.monotonic() - wall0
  130. after = loads()
  131. completion = sum(r.get("completion_tokens") or 0 for r in rows)
  132. windows = [r["window_129_641_tps"] for r in rows if r.get("window_129_641_tps")]
  133. ttfts = [r["ttft_s"] for r in rows if r.get("ttft_s") is not None]
  134. return {
  135. "concurrency": concurrency,
  136. "wall_seconds": round(wall, 3),
  137. "successes": sum(1 for r in rows if r.get("completion_tokens")),
  138. "completion_tokens": completion,
  139. "aggregate_tok_s": round(completion / wall, 2) if wall else None,
  140. "median_window_tps": round(statistics.median(windows), 2) if windows else None,
  141. "mean_window_tps": round(statistics.mean(windows), 2) if windows else None,
  142. "median_ttft_s": round(statistics.median(ttfts), 3) if ttfts else None,
  143. "finish_reasons": [r.get("finish_reason") for r in rows],
  144. "idle_before": {k: idle.get(k) for k in ("num_running_reqs", "num_waiting_reqs", "num_used_tokens")},
  145. "after": {k: after.get(k) for k in ("num_running_reqs", "num_waiting_reqs", "num_used_tokens")},
  146. "rows": rows,
  147. }
  148. def main():
  149. OUT.mkdir(parents=True, exist_ok=True)
  150. report = {
  151. "captured_utc": datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ"),
  152. "base_url": BASE,
  153. "waves": [],
  154. }
  155. require_idle()
  156. for conc in (1, 2, 4, 8):
  157. print(f"wave C{conc} starting", flush=True)
  158. cell = wave(conc)
  159. report["waves"].append(cell)
  160. (OUT / "partial.json").write_text(json.dumps(report, indent=2))
  161. print(json.dumps({k: cell[k] for k in (
  162. "concurrency", "wall_seconds", "aggregate_tok_s",
  163. "median_window_tps", "median_ttft_s", "successes"
  164. )}), flush=True)
  165. time.sleep(2)
  166. require_idle()
  167. (OUT / "summary.json").write_text(json.dumps(report, indent=2))
  168. print(str(OUT), flush=True)
  169. if __name__ == "__main__":
  170. main()