Samonte's picture
slm A-s00080
698535a verified
Raw History Blame Contribute Delete
7.78 kB
"""Run a model on items through a vLLM OpenAI server, the way the SLM pool calls it, with timings.
Each request is a raw completion of the arena's slm_prompt, greedy, max 3,072 tokens, stopped at
<|end_of_text|>, streamed so the first token time and the time per output token are measured on
this side. Start the server with scripts/serve_vllm.sh (batch invariant mode and the arena's
context length, like the official engine image). Timings are only meaningful at --concurrency 1,
which is how the envelope is measured; quality runs can use more.
python -m ccslm.generate --items out/data/test/items.jsonl --out out/gen/base.jsonl --concurrency 4
python -m ccslm.generate envelope --outputs out/gen/base.jsonl # p95 figures and the gate
"""
from __future__ import annotations
import argparse
import json
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any
from ccslm.common import EXPECTED, iter_jsonl, log, slm_prompt, write_json
LONG_ABOVE = 32_768
LIMITS = {"ttft_ms": 4000.0, "tpot_ms": 20.0, "long_ttft_ms": 25000.0, "long_e2e_ms": 75000.0}
VIOLATION_SHARE = 0.01
def complete(url: str, prompt: str, *, model: str, max_tokens: int, seed: int, timeout: float) -> dict[str, Any]:
import httpx
body = {
"model": model,
"prompt": prompt,
"max_tokens": max_tokens,
"temperature": 0,
"stop": EXPECTED["stop"],
"seed": seed,
"stream": True,
"stream_options": {"include_usage": True},
}
text: list[str] = []
first = 0.0
usage: dict[str, Any] = {}
finish = ""
started = time.perf_counter()
with httpx.Client(timeout=timeout) as http, http.stream("POST", url.rstrip("/") + "/v1/completions", json=body) as response:
response.raise_for_status()
for line in response.iter_lines():
if not line.startswith("data: "):
continue
payload = line[6:]
if payload.strip() == "[DONE]":
break
chunk = json.loads(payload)
if chunk.get("usage"):
usage = chunk["usage"]
for choice in chunk.get("choices") or []:
piece = choice.get("text") or ""
if piece and not first:
first = time.perf_counter()
text.append(piece)
finish = choice.get("finish_reason") or finish
ended = time.perf_counter()
completion = int(usage.get("completion_tokens") or 0)
ttft = (first - started) * 1000 if first else (ended - started) * 1000
tpot = (ended - first) * 1000 / (completion - 1) if first and completion > 1 else 0.0
return {
"text": "".join(text),
"prompt_tokens": int(usage.get("prompt_tokens") or 0),
"completion_tokens": completion,
"ttft_ms": round(ttft, 1),
"tpot_ms": round(tpot, 3),
"latency_ms": round((ended - started) * 1000, 1),
"finish_reason": finish,
}
def p95(values: list[float]) -> float:
if not values:
return 0.0
ordered = sorted(values)
return ordered[min(len(ordered) - 1, int(0.95 * len(ordered)))]
def envelope(rows: list[dict[str, Any]]) -> dict[str, Any]:
"""The arena's per item envelope rule (score.envelope_share) and the p95 figures."""
answered = [r for r in rows if r.get("completion_tokens")]
over = 0
for r in answered:
long = r["prompt_tokens"] > LONG_ABOVE
late = r["ttft_ms"] > (LIMITS["long_ttft_ms"] if long else LIMITS["ttft_ms"])
late = late or (long and r["latency_ms"] > LIMITS["long_e2e_ms"])
slow = r["completion_tokens"] > 1 and r["tpot_ms"] > LIMITS["tpot_ms"]
over += 1 if late or slow else 0
share = over / len(answered) if answered else 1.0
short = [r for r in answered if r["prompt_tokens"] <= LONG_ABOVE]
long_rows = [r for r in answered if r["prompt_tokens"] > LONG_ABOVE]
return {
"items": len(answered),
"over": over,
"violation_share": round(share, 4),
"envelope_pass": share <= VIOLATION_SHARE,
"ttft_p95_ms": p95([r["ttft_ms"] for r in answered]),
"ttft_p95_short_ms": p95([r["ttft_ms"] for r in short]),
"ttft_p95_long_ms": p95([r["ttft_ms"] for r in long_rows]),
"e2e_p95_long_ms": p95([r["latency_ms"] for r in long_rows]),
"tpot_p95_ms": p95([r["tpot_ms"] for r in answered]),
"completion_tokens_mean": round(sum(r["completion_tokens"] for r in answered) / max(1, len(answered)), 1),
"cut_off": sum(1 for r in answered if r.get("finish_reason") == "length"),
}
def run(args: argparse.Namespace) -> None:
out = Path(args.out)
done = {r["item_id"] for r in iter_jsonl(out)} if out.is_file() else set()
items = [i for i in iter_jsonl(args.items) if i["item_id"] not in done and not i.get("extra_cut")]
if args.limit:
originals = [i for i in items if not i.get("twin_of")][: args.limit]
keep = {i["item_id"] for i in originals}
items = originals + [i for i in items if i.get("twin_of") in keep]
if args.longest_first:
items.sort(key=lambda i: -int(i["label"]["input_tokens"]))
out.parent.mkdir(parents=True, exist_ok=True)
lock = threading.Lock()
handle = out.open("a", encoding="utf-8")
count = [0]
def one(item: dict[str, Any]) -> None:
try:
result = complete(
args.server,
slm_prompt(item["task"]),
model=args.model,
max_tokens=args.max_tokens,
seed=args.seed,
timeout=args.timeout,
)
except Exception as exc: # noqa: BLE001 - a failed call is recorded and counts as wrong
result = {"text": "", "error": str(exc)[:300], "completion_tokens": 0, "prompt_tokens": 0}
row = {"item_id": item["item_id"], "input_tokens": item["label"]["input_tokens"], **result}
with lock:
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
handle.flush()
count[0] += 1
if count[0] % 10 == 0:
log(f"generated {count[0]}/{len(items)}")
log(f"{len(items)} items to generate ({len(done)} already done)")
with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as pool:
list(pool.map(one, items))
handle.close()
def main() -> None:
if len(sys.argv) > 1 and sys.argv[1] == "envelope":
parser = argparse.ArgumentParser(description="envelope figures from a concurrency 1 generation run")
parser.add_argument("cmd")
parser.add_argument("--outputs", required=True)
parser.add_argument("--out", default="")
args = parser.parse_args()
report = envelope(list(iter_jsonl(args.outputs)))
if args.out:
write_json(args.out, report)
print(json.dumps(report, indent=2))
return
parser = argparse.ArgumentParser(description="generate summaries through a vLLM server")
parser.add_argument("--items", required=True)
parser.add_argument("--out", required=True)
parser.add_argument("--server", default="http://127.0.0.1:8000")
parser.add_argument("--model", default="slm")
parser.add_argument("--max-tokens", type=int, default=EXPECTED["max_tokens"])
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--concurrency", type=int, default=1)
parser.add_argument("--limit", type=int, default=0, help="original items (twins follow)")
parser.add_argument("--longest-first", action="store_true")
parser.add_argument("--timeout", type=float, default=300.0)
run(parser.parse_args())
if __name__ == "__main__":
main()