Download code/bench/bench2.py from ProprietaryLegal/dsv41-longctx-bench-tools: direct link, hf CLI and curl.
- Browser
- Download file 26.4 kB
-
https://huggingface.co/ProprietaryLegal/dsv41-longctx-bench-tools/resolve/main/code/bench/bench2.py
- Command line
-
hf download hf://ProprietaryLegal/dsv41-longctx-bench-tools/code/bench/bench2.py
-
curl -L -o bench2.py https://huggingface.co/ProprietaryLegal/dsv41-longctx-bench-tools/resolve/main/code/bench/bench2.py
26.4 kB
| #!/usr/bin/env python3 | |
| """Extended DSv4.1 TP2 harness, copied from deployment bench.py. | |
| Loads its workload text from DSV41_BENCH_SPEC_FILE (not shipped) and keeps the public | |
| CourtListener/RECAP prompt builders. | |
| Adds channel-aware SSE timing, exact occupied chat counts, run-wide speculative | |
| counters, concurrent/mixed requests and bounded telemetry. Coordinator alone runs | |
| benchmarks; --dry-run checks connectivity without inference or tokenization. | |
| See README.md for accounting limits and reproducible commands. | |
| """ | |
| import argparse | |
| import hashlib | |
| import json | |
| import os | |
| import random | |
| import re | |
| import threading | |
| import time | |
| import urllib.request | |
| import uuid | |
| from pathlib import Path | |
| from typing import TypeAlias, TypedDict, cast | |
| from telemetry import Telemetry | |
| JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None | |
| Object: TypeAlias = dict[str, JSON] | |
| URL = "http://localhost:8000" | |
| MODEL = "DeepSeek-V4.1-Flash-1017-EXL3-Spark-TP2" | |
| NODES = ["spark", "spark-b"] | |
| # Workload prompts and the synthetic-record vocabulary are not shipped with the public source (prompt-bearing text | |
| # stays outside the PLI Labs data boundary). DSV41_BENCH_SPEC_FILE names a JSON file with keys: | |
| # workloads {name: instruction} for the names in WORKLOAD_NAMES | |
| # first, last, places, verbs [str] synthetic-record vocabulary | |
| # claims [str], each may use {n} and {m} | |
| # paragraph str using {i} {d} {place} {a} {verb} {claim} {b} {dcn} | |
| WORKLOAD_NAMES = ("prose", "legal", "summary", "reason", "synth", "extract") | |
| class BenchSpec(TypedDict): | |
| workloads: dict[str, str] | |
| first: list[str] | |
| last: list[str] | |
| places: list[str] | |
| verbs: list[str] | |
| claims: list[str] | |
| paragraph: str | |
| _SPEC: BenchSpec | None = None | |
| def spec() -> BenchSpec: | |
| global _SPEC | |
| if _SPEC is None: | |
| path = os.environ.get("DSV41_BENCH_SPEC_FILE") | |
| if not path: | |
| raise SystemExit("set DSV41_BENCH_SPEC_FILE (JSON workload spec); prompts are not shipped") | |
| with open(path, encoding="utf-8") as fh: | |
| loaded = json.load(fh) | |
| missing = [k for k in ("workloads", "first", "last", "places", "verbs", "claims", "paragraph") if k not in loaded] | |
| if missing: | |
| raise SystemExit(f"DSV41_BENCH_SPEC_FILE {path} lacks keys {missing}") | |
| _SPEC = cast(BenchSpec, loaded) | |
| return _SPEC | |
| def record_paragraph(rng: random.Random, i: int) -> str: | |
| s = spec() | |
| d = f"20{rng.randint(22, 25)}-{rng.randint(1, 12):02d}-{rng.randint(1, 28):02d}" | |
| a, b = f"{rng.choice(s['first'])} {rng.choice(s['last'])}", f"{rng.choice(s['first'])} {rng.choice(s['last'])}" | |
| c = rng.choice(s["claims"]).format(n=rng.randint(1, 9), m=rng.randint(200, 25000)) | |
| return s["paragraph"].format(i=i, d=d, place=rng.choice(s["places"]), a=a, verb=rng.choice(s["verbs"]), claim=c, | |
| b=b, dcn=rng.randint(100000, 999999)) | |
| def tok_count(text: str) -> int: | |
| req = urllib.request.Request(URL + "/tokenize", data=json.dumps({"model": MODEL, "prompt": text}).encode(), | |
| headers={"Content-Type": "application/json"}) | |
| with urllib.request.urlopen(req, timeout=600) as response: | |
| return int(json.load(response)["count"]) | |
| _DOCS: list[dict[str, str]] | None = None | |
| def build_real(target_tokens: int, seed: int) -> str: | |
| """Concatenate public CourtListener opinions + RECAP filings (data/*.jsonl) to target_tokens (exact via /tokenize).""" | |
| global _DOCS | |
| if _DOCS is None: | |
| _DOCS = [] | |
| for filename in ("opinions_cl.jsonl", "filings_recap.jsonl"): | |
| with (DATA_DIR / filename).open() as source: | |
| _DOCS.extend(json.loads(line) for line in source) | |
| docs = _DOCS[:]; random.Random(seed).shuffle(docs) | |
| parts, chars = [], 0 | |
| ratio = 3.3 # chars/token first guess; corrected below | |
| for d in docs: | |
| parts.append(f"=== {d.get('title', '')} ({d.get('court', '')}, {d.get('date', '')}) ===\n{d['text']}") | |
| chars += len(parts[-1]) | |
| if chars / ratio >= target_tokens * 1.02: | |
| n = tok_count("\n\n".join(parts)); ratio = chars / n | |
| if n >= target_tokens: | |
| break | |
| text = "\n\n".join(parts) | |
| while tok_count(text) > target_tokens: # trim to the target | |
| text = text[: int(len(text) - (tok_count(text) - target_tokens) * ratio) - 50] | |
| return text | |
| def build_record(target_tokens: int, seed: int) -> str: | |
| rng = random.Random(seed) | |
| paras, i, approx = [], 1, 0.0 | |
| while approx < target_tokens: | |
| p = record_paragraph(rng, i) | |
| paras.append(p); approx += len(p) / 3.6; i += 1 # ~3.6 chars/token for this text (calibrated below) | |
| return "\n".join(paras) | |
| def post(path: str, body: Object, timeout: float = 3600) -> Object: | |
| req = urllib.request.Request(URL + path, data=json.dumps(body).encode(), | |
| headers={"Content-Type": "application/json"}) | |
| with urllib.request.urlopen(req, timeout=timeout) as response: | |
| return cast(Object, json.load(response)) | |
| def stream_request(body: Object, started: threading.Event | None = None) -> Object: | |
| """Count delivered tokens only. Channel tokenization is labelled, never guessed from chunks.""" | |
| req = urllib.request.Request(URL + "/v1/chat/completions", data=json.dumps(body).encode(), | |
| headers={"Content-Type": "application/json"}) | |
| begin = time.monotonic() | |
| first: dict[str, float] = {} | |
| last: dict[str, float] = {} | |
| chunks: dict[str, list[str]] = {"reasoning": [], "answer": []} | |
| # Arrival time of every content-bearing SSE event: vLLM streams one event per engine step, so the gaps are a direct | |
| # client-side round timer (at concurrency 1 a step is one verify round plus its draft). | |
| event_times: list[float] = [] | |
| usage: Object | None = None | |
| finish: JSON = None | |
| done = False | |
| with urllib.request.urlopen(req, timeout=3600) as response: | |
| for raw in response: | |
| line = raw.decode().strip() | |
| if not line.startswith("data:"): | |
| continue | |
| payload = line[5:].strip() | |
| if payload == "[DONE]": | |
| done = True | |
| break | |
| event = json.loads(payload) | |
| if event.get("error"): | |
| raise RuntimeError(f"stream error: {event['error']}") | |
| if event.get("usage"): | |
| usage = event["usage"] | |
| if any((c.get("delta", {}).get("reasoning_content") or c.get("delta", {}).get("reasoning") | |
| or c.get("delta", {}).get("content")) for c in event.get("choices", [])): | |
| event_times.append(time.monotonic() - begin) | |
| for choice in event.get("choices", []): | |
| delta = choice.get("delta", {}) | |
| for kind, piece in (("reasoning", delta.get("reasoning_content") or delta.get("reasoning")), | |
| ("answer", delta.get("content"))): | |
| if piece: | |
| now = time.monotonic() - begin | |
| first.setdefault(kind, now) | |
| last[kind] = now | |
| chunks[kind].append(piece) | |
| if started is not None: | |
| started.set() | |
| finish = choice.get("finish_reason") or finish | |
| elapsed = time.monotonic() - begin # excludes post-hoc /tokenize calls | |
| if started is not None: | |
| started.clear() # mixed prefill requires streams still generating, not retokenizing | |
| if not done or usage is None or finish is None: | |
| raise RuntimeError(f"incomplete stream: done={done}, usage={usage}, finish={finish}") | |
| result: Object = {"usage": usage, "finish": finish, "total_s": elapsed, | |
| "prompt_tokens": usage["prompt_tokens"], "completion_tokens": usage["completion_tokens"], | |
| "ttft_s": min(first.values()) if first else None, | |
| "first_visible_s": first.get("answer"), "first_by_kind_s": cast(Object, first), | |
| "last_by_kind_s": cast(Object, last)} | |
| details = cast(Object, usage.get("completion_tokens_details") or {}) | |
| server_reasoning = details.get("reasoning_tokens") | |
| for kind, pieces in chunks.items(): | |
| text = "".join(pieces) | |
| # Some servers report reasoning_tokens explicitly. Otherwise exact tokenization of | |
| # emitted text is useful, but is NOT the original generation token sequence. | |
| count = (int(cast(int, server_reasoning)) if kind == "reasoning" and server_reasoning is not None | |
| else tok_count(text) if text else 0) | |
| source = "server_usage" if kind == "reasoning" and server_reasoning is not None else "retokenized_text" | |
| if kind == "answer" and server_reasoning is not None: | |
| count = int(cast(int, usage["completion_tokens"])) - int(cast(int, server_reasoning)) | |
| source = "server_usage_difference_includes_control_tokens" | |
| span = last.get(kind, 0) - first.get(kind, 0) | |
| result[kind + "_tokens"] = count | |
| result[kind + "_token_source"] = source | |
| result[kind + "_decode_tps"] = (count - 1) / span if span > 0 and count > 1 else None | |
| result[kind + "_text"] = text | |
| gaps = sorted(1000.0 * (b - a) for a, b in zip(event_times, event_times[1:])) | |
| if gaps: | |
| def pct(q: float) -> float: | |
| return round(gaps[min(len(gaps) - 1, int(q * len(gaps)))], 3) | |
| result["step_gap_ms"] = {"n": len(gaps), "mean": round(sum(gaps) / len(gaps), 3), "p10": pct(0.10), | |
| "p50": pct(0.50), "p90": pct(0.90), "max": round(gaps[-1], 3)} | |
| result["step_events"] = len(event_times) | |
| result["step_times_ms"] = [round(1000.0 * t, 1) for t in event_times] | |
| all_span = max(last.values()) - min(first.values()) if first else 0 | |
| result["decode_tps"] = (int(cast(int, usage["completion_tokens"])) - 1) / all_span if all_span > 0 else None | |
| return result | |
| MetricMap: TypeAlias = dict[str, float] | |
| def scrape(url: str | None = None) -> str: | |
| with urllib.request.urlopen(url or URL + "/metrics", timeout=10) as response: | |
| return str(response.read().decode()) | |
| def parse_metrics(text: str) -> MetricMap: | |
| values: MetricMap = {} | |
| for line in text.splitlines(): | |
| match = re.fullmatch(r'([^#\s{]+)(\{.*\})?\s+([-+0-9.eE]+)(?:\s+\d+)?', line) | |
| if match: | |
| values[match[1] + (match[2] or "")] = float(match[3]) | |
| return values | |
| def metric_sum(values: MetricMap, name: str) -> float | None: | |
| found = [v for k, v in values.items() if k.split("{")[0] == name] | |
| return sum(found) if found else None | |
| def require_idle(values: MetricMap) -> None: | |
| for name in ("vllm:num_requests_running", "vllm:num_requests_waiting"): | |
| count = metric_sum(values, name) | |
| if count is None or count != 0: | |
| raise RuntimeError(f"serve is not verified idle: {name}={count}") | |
| def spec_delta(before: MetricMap, after: MetricMap) -> Object: | |
| keys = {k for k in before | after if k.startswith("vllm:spec_decode_") and k.split("{")[0].endswith("_total")} | |
| if any(k not in after or after[k] < before.get(k, 0) for k in keys): | |
| raise RuntimeError("speculative counters reset/disappeared during run; comparison invalid") | |
| # A per-position series may be lazily created on its first use. | |
| delta = {k: after[k] - before.get(k, 0) for k in keys} | |
| accepted = metric_sum(delta, "vllm:spec_decode_num_accepted_tokens_total") | |
| proposed = metric_sum(delta, "vllm:spec_decode_num_draft_tokens_total") | |
| rounds = metric_sum(delta, "vllm:spec_decode_num_drafts_total") | |
| positions: dict[str, float] = {} | |
| for key, value in delta.items(): | |
| if key.split("{")[0] == "vllm:spec_decode_num_accepted_tokens_per_pos_total": | |
| match = re.search(r'(?:\{|,)\s*position="([^"]+)"', key) | |
| if match: | |
| positions[match[1]] = positions.get(match[1], 0) + value | |
| return {"proposed_tokens": proposed, "accepted_tokens": accepted, "rounds": rounds, | |
| "mean_accepted_per_round": accepted / rounds if rounds and accepted is not None else None, | |
| "acceptance_fraction": accepted / proposed if proposed and accepted is not None else None, | |
| "accepted_per_position": cast(Object, positions), | |
| "acceptance_per_position_per_round": {k: v / rounds if rounds else None for k, v in positions.items()}, | |
| "raw_delta": cast(Object, delta), "scope": "server_global_run_window"} | |
| DATA_DIR = Path(__file__).resolve().parents[2] / "data" | |
| def models() -> Object: | |
| with urllib.request.urlopen(URL + "/v1/models", timeout=10) as response: | |
| return cast(Object, json.load(response)) | |
| def prompt_tokens(prompt: str, template: Object) -> int: | |
| result = post("/tokenize", {"model": MODEL, "messages": [{"role": "user", "content": prompt}], | |
| "add_generation_prompt": True, "chat_template_kwargs": template}) | |
| return int(cast(int, result["count"])) | |
| def make_prompt(ctx: int, workload: str, filler: str, seed: int, template: Object) -> tuple[str, int]: | |
| """Measure complete occupied chat prompt, including template and legal instruction.""" | |
| instruction = spec()["workloads"][workload] | |
| if not ctx: | |
| return instruction, prompt_tokens(instruction, template) | |
| # 4,096 tokens of slack: build_real's trim loop can undershoot its target by ~2K at 150K, and the search below only | |
| # shortens. The material is the same seeded document order either way, so shorter contexts keep identical prompts. | |
| rec = build_real(ctx + 4096, seed) if filler == "real" else build_record(ctx + 4096, seed) | |
| title = "MATERIALS (public court documents)" if filler == "real" else "CASE RECORD (fictional)" | |
| def wrap(length: int) -> str: | |
| return f"{title}:\n{rec[:length]}\n\nEND OF MATERIALS.\n\n{instruction}" | |
| lo, hi = 0, len(rec) | |
| best, count = wrap(0), prompt_tokens(wrap(0), template) | |
| if count > ctx: | |
| raise ValueError(f"requested context {ctx} is smaller than instruction/template ({count})") | |
| while lo <= hi: | |
| middle = (lo + hi) // 2 | |
| candidate = wrap(middle) | |
| n = prompt_tokens(candidate, template) | |
| if n <= ctx: | |
| best, count, lo = candidate, n, middle + 1 | |
| else: | |
| hi = middle - 1 | |
| if ctx - count > 16: | |
| raise ValueError(f"corpus cannot fill requested context {ctx}; measured {count}; supply more real documents") | |
| return best, count | |
| def request_body(prompt: str, args: argparse.Namespace, salt: str) -> Object: | |
| return {"model": MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": args.max_tokens, | |
| "temperature": args.temperature, "top_p": args.top_p, "seed": args.seed, | |
| "stream": True, "stream_options": {"include_usage": True, "continuous_usage_stats": False}, | |
| "chat_template_kwargs": {"thinking": args.thinking, "reasoning_effort": args.reasoning_effort}, | |
| "priority": args.priority, "cache_salt": salt} | |
| def settings(args: argparse.Namespace, model_info: Object) -> Object: | |
| return {"config_label": args.label, "url": URL, "model": MODEL, "models_response": model_info, | |
| "temperature": args.temperature, "top_p": args.top_p, "seed": args.seed, | |
| "chat_template_kwargs": {"thinking": args.thinking, "reasoning_effort": args.reasoning_effort}, | |
| "max_tokens": args.max_tokens, "reserved_output": args.max_tokens, "priority": args.priority, | |
| "max_model_len": args.max_model_len, | |
| "cache_mode": args.cache_mode, "filler": args.filler, "data_dir": str(DATA_DIR)} | |
| def write_row(path: Path, row: Object) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with path.open("a") as output: | |
| output.write(json.dumps(row, allow_nan=False) + "\n") | |
| output.flush() | |
| def run_group(args: argparse.Namespace, config: Object, ctx: int, workload: str, concurrency: int, | |
| rep: int, prompts: list[tuple[str, int]], salts: list[str], mixed: bool = False) -> Object: | |
| from concurrent.futures import ThreadPoolExecutor | |
| run_id = uuid.uuid4().hex | |
| before = parse_metrics(scrape()) | |
| require_idle(before) | |
| count = 3 if mixed else concurrency | |
| starts = [threading.Event() for _ in range(count)] | |
| barrier = threading.Barrier(2 if mixed else count) | |
| begin = time.monotonic() | |
| common: Object = {"run_id": run_id, "config_label": args.label, "target_ctx": ctx, "workload": workload, | |
| "concurrency": count, "mixed": mixed, "rep": rep, "settings": config, | |
| "cache_state": "cold" if args.cache_mode == "cold" or rep == 0 else "warm_repeat", | |
| "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())} | |
| def request(index: int, synchronize: bool = True) -> Object: | |
| if synchronize: | |
| barrier.wait(timeout=30) | |
| offset = time.monotonic() - begin | |
| prompt, occupied = prompts[index] | |
| body = request_body(prompt, args, salts[index]) | |
| try: | |
| result = stream_request(body, starts[index]) | |
| error = None | |
| except Exception as exc: # noqa: BLE001 -- request boundary records error and fails run | |
| result, error = {}, f"{type(exc).__name__}: {exc}" | |
| if not args.keep_text: | |
| result.pop("answer_text", None) | |
| result.pop("reasoning_text", None) | |
| return {**common, "type": "request", "request_index": index, "start_offset_s": offset, | |
| "occupied_prompt_tokens": occupied, "reserved_output": args.max_tokens, | |
| "prompt_sha256": hashlib.sha256(prompt.encode()).hexdigest(), | |
| "cache_salt": salts[index], "error": error, **result} | |
| sampler = Telemetry(args.spark_hosts, args.x86host_host, args.process_name, args.extra_metrics, args.x86host_nic) | |
| sampler.start() | |
| rows: list[Object] = [] | |
| schedule_error: str | None = None | |
| try: | |
| with ThreadPoolExecutor(max_workers=count) as pool: | |
| futures = [pool.submit(request, i) for i in range(2 if mixed else count)] | |
| if mixed: | |
| deadline = time.monotonic() + args.mixed_timeout | |
| while not all(event.is_set() for event in starts[:2]): | |
| if any(f.done() for f in futures) or time.monotonic() >= deadline: | |
| schedule_error = "mixed prefill not submitted: two active decoding streams not observed" | |
| break | |
| time.sleep(0.02) | |
| if schedule_error is None: | |
| time.sleep(args.mixed_delay) | |
| if any(f.done() for f in futures) or not all(e.is_set() for e in starts[:2]): | |
| schedule_error = "mixed prefill not submitted: a decoding stream already finished" | |
| else: | |
| futures.append(pool.submit(request, 2, False)) | |
| for future in futures: | |
| row = future.result() | |
| rows.append(row) | |
| write_row(Path(args.out), row) | |
| client_elapsed = time.monotonic() - begin | |
| elapsed = max((float(cast(float, r["start_offset_s"])) + float(cast(float, r.get("total_s") or 0)) for r in rows), default=client_elapsed) | |
| finally: | |
| sampler.finish() | |
| # vLLM counters are periodically published; this delay is excluded from throughput. | |
| time.sleep(args.metrics_settle) | |
| metric_error: str | None = None | |
| after: MetricMap = {} | |
| try: | |
| after = parse_metrics(scrape()) | |
| speculation = spec_delta(before, after) | |
| except Exception as exc: # noqa: BLE001 -- request boundary records error and fails run | |
| speculation = {"error": str(exc)} | |
| metric_error = f"metrics accounting failed: {type(exc).__name__}: {exc}" | |
| delivered = sum(int(cast(int, r.get("completion_tokens") or 0)) for r in rows) | |
| error = schedule_error or metric_error or ("one or more requests failed" if any(r["error"] for r in rows) else None) | |
| summary: Object = {**common, "type": "run", "error": error, "elapsed_s": elapsed, | |
| "delivered_tokens": delivered, "aggregate_tps": delivered / elapsed, | |
| "client_elapsed_s": client_elapsed, | |
| "spec": speculation, "telemetry": sampler.samples, | |
| "telemetry_errors": sampler.errors, | |
| "metrics_before": cast(Object, before), "metrics_after": cast(Object, after), | |
| "metrics_settle_s": args.metrics_settle, | |
| "request_count": len(rows)} | |
| write_row(Path(args.out), summary) | |
| print(json.dumps({k: v for k, v in summary.items() if k not in ("telemetry", "metrics_before", "metrics_after")} ), flush=True) | |
| if error: | |
| raise RuntimeError(error) | |
| return summary | |
| def main() -> None: | |
| global URL, MODEL, DATA_DIR | |
| ap = argparse.ArgumentParser(description="Coordinator benchmark harness; --dry-run never generates tokens") | |
| ap.add_argument("--url", default="http://SPARK_A_LAN_IP:8000") | |
| ap.add_argument("--model", help="served model id; default is first /v1/models id") | |
| ap.add_argument("--label", default="A") | |
| ap.add_argument("--ctx", type=int, nargs="+", default=[0]) | |
| ap.add_argument("--workload", nargs="+", choices=sorted(WORKLOAD_NAMES), default=["legal"]) | |
| ap.add_argument("--concurrency", type=int, nargs="+", choices=[1, 2, 3, 4], default=[1]) | |
| ap.add_argument("--mixed", action="store_true", help="also run two decoders with a long-prefill arrival") | |
| ap.add_argument("--mixed-short-ctx", type=int, default=0) | |
| ap.add_argument("--mixed-delay", type=float, default=1) | |
| ap.add_argument("--mixed-timeout", type=float, default=600) | |
| ap.add_argument("--reps", type=int, default=3) | |
| ap.add_argument("--max-tokens", type=int, default=1500) | |
| ap.add_argument("--max-model-len", type=int, default=327680, help="also capped by live /v1/models limit") | |
| ap.add_argument("--thinking", action=argparse.BooleanOptionalAction, default=True) | |
| ap.add_argument("--reasoning-effort", default="high") | |
| ap.add_argument("--temperature", type=float, default=1) | |
| ap.add_argument("--top-p", type=float, default=1) | |
| ap.add_argument("--seed", type=int, default=42) | |
| ap.add_argument("--priority", type=int, default=10) | |
| ap.add_argument("--cache-mode", choices=["cold", "warm"], default="cold") | |
| ap.add_argument("--filler", choices=["real", "synthetic"], default="real") | |
| ap.add_argument("--data-dir", type=Path, default=DATA_DIR) | |
| ap.add_argument("--out", default="offload/results/bench2.jsonl") | |
| ap.add_argument("--keep-text", action="store_true") | |
| ap.add_argument("--spark-hosts", nargs=2, default=["spark-a", "spark-b"]) | |
| ap.add_argument("--x86host-host", default="x86host") | |
| ap.add_argument("--process-name", action="append", default=[], help="exact /proc comm or executable basename") | |
| ap.add_argument("--x86host-nic", nargs=2, default=["enp130s0f0np0", "enp130s0f1np1"]) | |
| ap.add_argument("--extra-metrics", action="append", default=[]) | |
| ap.add_argument("--metrics-settle", type=float, default=2, help="counter publication wait after each group") | |
| ap.add_argument("--dry-run", action="store_true") | |
| args = ap.parse_args() | |
| if args.reps < 1 or args.max_tokens < 1 or min(args.ctx) < 0 or args.mixed_short_ctx < 0: | |
| ap.error("reps/output must be positive; context must be nonnegative") | |
| if min(args.mixed_delay, args.metrics_settle) < 0 or args.mixed_timeout <= 0: | |
| ap.error("timing options must be nonnegative; mixed-timeout must be positive") | |
| URL, DATA_DIR = args.url.rstrip("/"), args.data_dir.resolve() | |
| info = models() | |
| records = cast(list[Object], info["data"]) | |
| if not records: | |
| raise RuntimeError("/v1/models returned no models") | |
| MODEL = args.model or str(records[0]["id"]) | |
| if not any(record["id"] == MODEL for record in records): | |
| raise ValueError(f"model {MODEL!r} absent in /v1/models") | |
| selected = next(record for record in records if record["id"] == MODEL) | |
| args.max_model_len = min(args.max_model_len, int(cast(int, selected.get("max_model_len") or args.max_model_len))) | |
| initial = parse_metrics(scrape()) | |
| config = settings(args, info) | |
| if args.dry_run: | |
| # Connectivity reads only: no /tokenize, prompt construction or chat completions. | |
| sampler = Telemetry(args.spark_hosts, args.x86host_host, args.process_name, args.extra_metrics, args.x86host_nic) | |
| sampler.sample() | |
| row: Object = {"type": "dry_run", "settings": config, "metrics": cast(Object, initial), | |
| "telemetry": sampler.samples, "telemetry_errors": sampler.errors, | |
| "generation_requests": 0} | |
| write_row(Path(args.out), row) | |
| print(json.dumps(row), flush=True) | |
| if sampler.errors: | |
| raise RuntimeError("dry-run telemetry failed; see recorded errors") | |
| return | |
| require_idle(initial) | |
| template: Object = {"thinking": args.thinking, "reasoning_effort": args.reasoning_effort} | |
| for ctx in args.ctx: | |
| for workload in args.workload: | |
| for mixed, concurrency in [(False, c) for c in args.concurrency] + ([(True, 3)] if args.mixed else []): | |
| sizes = [args.mixed_short_ctx, args.mixed_short_ctx, ctx] if mixed else [ctx] * concurrency | |
| # Same prompts across repetitions/variants. Cold changes only salt; warm primes on rep zero. | |
| prompts = [make_prompt(size, workload, args.filler, args.seed + i, template) for i, size in enumerate(sizes)] | |
| for _, occupied in prompts: | |
| if occupied + args.max_tokens > args.max_model_len: | |
| raise ValueError(f"occupied {occupied} + output {args.max_tokens} exceeds {args.max_model_len}") | |
| warm_salts = [uuid.uuid4().hex for _ in sizes] | |
| for rep in range(args.reps): | |
| salts = warm_salts if args.cache_mode == "warm" else [uuid.uuid4().hex for _ in sizes] | |
| run_group(args, config, ctx, workload, concurrency, rep, prompts, salts, mixed) | |
| if __name__ == "__main__": | |
| main() | |