import argparse import concurrent.futures import json import math import statistics import time import urllib.error import urllib.request from dataclasses import dataclass, asdict from datetime import datetime from pathlib import Path @dataclass class RequestResult: ok: bool elapsed_sec: float ttft_sec: float | None prompt_tokens: int cached_tokens: int completion_tokens: int reasoning_tokens: int output_tps: float | None error: str | None = None def percentile(values, pct): if not values: return None values = sorted(values) if len(values) == 1: return values[0] pos = (len(values) - 1) * pct / 100.0 lo = math.floor(pos) hi = math.ceil(pos) if lo == hi: return values[lo] return values[lo] * (hi - pos) + values[hi] * (pos - lo) def load_dataset(path): text = Path(path).read_text(encoding="utf-8") if path.endswith(".jsonl"): data = [json.loads(line) for line in text.splitlines() if line.strip()] else: data = json.loads(text) # Official-like format: each item is already an OpenAI chat completion # request with messages/tools/tool_choice/stream/etc. if data and isinstance(data[0], dict) and "messages" in data[0]: return data requests = [] for item in data: system_prompt = item.get("system_prompt") or "You are a helpful assistant." history = [{"role": "system", "content": system_prompt}] for question in item.get("user_questions", []): messages = history + [{"role": "user", "content": question}] requests.append({"messages": messages}) # Synthetic assistant placeholder keeps later prompts multi-turn. history = messages + [{"role": "assistant", "content": "好的,我们继续。"}] return requests def post_stream(url, model, request_item, max_tokens, timeout): allowed_fields = { "messages", "tools", "tool_choice", "model", "max_tokens", "temperature", "top_p", "stop", "presence_penalty", "frequency_penalty", "n", "response_format", } payload = {k: v for k, v in dict(request_item).items() if k in allowed_fields} payload["model"] = payload.get("model") or model # The benchmark CLI controls output length, even if the synthetic dataset # stores a larger official-like max_tokens value. payload["max_tokens"] = max_tokens payload["temperature"] = payload.get("temperature", 0) payload["stream"] = True payload["stream_options"] = {"include_usage": True} req = urllib.request.Request( url.rstrip("/") + "/v1/chat/completions", data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST", ) start = time.perf_counter() first_token_at = None usage = {} try: with urllib.request.urlopen(req, timeout=timeout) as resp: for raw in resp: line = raw.decode("utf-8", errors="replace").strip() if not line or not line.startswith("data:"): continue data = line[5:].strip() if data == "[DONE]": break obj = json.loads(data) if obj.get("usage"): usage = obj["usage"] for choice in obj.get("choices") or []: delta = choice.get("delta") or {} text = delta.get("content") or delta.get("reasoning_content") or "" if text and first_token_at is None: first_token_at = time.perf_counter() elapsed = time.perf_counter() - start ttft = first_token_at - start if first_token_at else None completion_tokens = int(usage.get("completion_tokens") or 0) prompt_tokens = int(usage.get("prompt_tokens") or 0) reasoning_tokens = int(usage.get("reasoning_tokens") or 0) details = usage.get("prompt_tokens_details") or {} cached_tokens = int(details.get("cached_tokens") or 0) decode_sec = elapsed - ttft if ttft is not None else elapsed output_tps = completion_tokens / decode_sec if completion_tokens and decode_sec > 0 else None return RequestResult( ok=True, elapsed_sec=elapsed, ttft_sec=ttft, prompt_tokens=prompt_tokens, cached_tokens=cached_tokens, completion_tokens=completion_tokens, reasoning_tokens=reasoning_tokens, output_tps=output_tps, ) except urllib.error.HTTPError as exc: body = "" try: body = exc.read().decode("utf-8", errors="replace") except Exception: pass elapsed = time.perf_counter() - start return RequestResult( ok=False, elapsed_sec=elapsed, ttft_sec=None, prompt_tokens=0, cached_tokens=0, completion_tokens=0, reasoning_tokens=0, output_tps=None, error=f"HTTPError {exc.code}: {body[:1000]}", ) except Exception as exc: elapsed = time.perf_counter() - start return RequestResult( ok=False, elapsed_sec=elapsed, ttft_sec=None, prompt_tokens=0, cached_tokens=0, completion_tokens=0, reasoning_tokens=0, output_tps=None, error=f"{type(exc).__name__}: {exc}", ) def main(): parser = argparse.ArgumentParser() parser.add_argument("--url", default="http://127.0.0.1:1111") parser.add_argument("--model", default="llm") parser.add_argument("--dataset", required=True) parser.add_argument("--concurrency", type=int, default=1) parser.add_argument("--max-requests", type=int, default=20) parser.add_argument("--max-tokens", type=int, default=128) parser.add_argument("--timeout", type=int, default=600) parser.add_argument("--out", default="/root/work/logs/formal_perf_result.json") args = parser.parse_args() reqs = load_dataset(args.dataset) if not reqs: raise SystemExit("empty dataset") scheduled = [reqs[i % len(reqs)] for i in range(args.max_requests)] wall_start = time.perf_counter() results = [] with concurrent.futures.ThreadPoolExecutor(max_workers=args.concurrency) as pool: futures = [ pool.submit(post_stream, args.url, args.model, request_item, args.max_tokens, args.timeout) for request_item in scheduled ] for fut in concurrent.futures.as_completed(futures): results.append(fut.result()) print(f"done {len(results)}/{len(scheduled)} ok={results[-1].ok}", flush=True) wall_sec = time.perf_counter() - wall_start ok_results = [r for r in results if r.ok] success_rate = len(ok_results) / len(results) if results else 0.0 ttfts = [r.ttft_sec for r in ok_results if r.ttft_sec is not None] output_tps_values = [r.output_tps for r in ok_results if r.output_tps is not None] prompt_tokens = sum(r.prompt_tokens for r in ok_results) cached_tokens = sum(r.cached_tokens for r in ok_results) input_tokens_uncached = max(prompt_tokens - cached_tokens, 0) output_tokens = sum(r.completion_tokens for r in ok_results) aggregate_output_tps = output_tokens / wall_sec if wall_sec > 0 else 0.0 aggregate_input_tps = input_tokens_uncached / wall_sec if wall_sec > 0 else 0.0 aggregate_cache_tps = cached_tokens / wall_sec if wall_sec > 0 else 0.0 weighted = aggregate_output_tps * 16.796 + aggregate_input_tps * 2.799 + aggregate_cache_tps * 0.56 cache_hit_rate = cached_tokens / prompt_tokens if prompt_tokens else 0.0 summary = { "created_at": datetime.now().isoformat(timespec="seconds"), "url": args.url, "model": args.model, "dataset": args.dataset, "concurrency": args.concurrency, "max_requests": args.max_requests, "max_tokens": args.max_tokens, "wall_sec": wall_sec, "success_rate": success_rate, "ttft_p90_sec": percentile(ttfts, 90), "output_tps_p10_per_request": percentile(output_tps_values, 10), "aggregate_output_tps": aggregate_output_tps, "aggregate_input_tps_uncached": aggregate_input_tps, "aggregate_cache_tps": aggregate_cache_tps, "cache_hit_rate": cache_hit_rate, "weighted_token_throughput": weighted, "totals": { "requests": len(results), "success": len(ok_results), "prompt_tokens": prompt_tokens, "input_tokens_uncached": input_tokens_uncached, "cached_tokens": cached_tokens, "completion_tokens": output_tokens, "reasoning_tokens": sum(r.reasoning_tokens for r in ok_results), }, "targets": { "output_tps_p10_per_request_gte_20": (percentile(output_tps_values, 10) or 0) >= 20, "ttft_p90_lte_5": (percentile(ttfts, 90) or 999) <= 5, "cache_hit_rate_gte_50pct": cache_hit_rate >= 0.5, "success_rate_gte_99pct": success_rate >= 0.99, "weighted_token_throughput_gte_8000": weighted >= 8000, }, "results": [asdict(r) for r in results], } Path(args.out).parent.mkdir(parents=True, exist_ok=True) Path(args.out).write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps({k: v for k, v in summary.items() if k != "results"}, ensure_ascii=False, indent=2)) print("RESULT_FILE", args.out) if __name__ == "__main__": main()