| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- #!/usr/bin/env python3
- """Identical greedy requests over a long prompt return different completions.
- Standard library only. Point it at a running vLLM OpenAI server:
- python greedy_nondet.py --url http://127.0.0.1:8000 --model glm53
- Sends the same ~42k-token prompt N times at temperature 0 with a fixed seed and
- groups the completions. More than one group means the forward pass is not
- reproducible.
- """
- import argparse
- import collections
- import hashlib
- import json
- import queue
- import threading
- import urllib.request
- # Repeated verbatim to reach the target length. Content does not matter: any
- # filler of the same size behaves the same.
- PARAGRAPH = """The job runner reads a JSON manifest, then runs each job in order.
- Every job carries a name, a command, a retry count and an environment overlay.
- The overlay is applied on top of the inherited environment on each attempt, so a
- key set to null must remove the inherited value rather than set it to the empty
- string. Failures are retried up to the configured count, and the runner returns
- the worst exit code it saw. The exporter reads the same manifest and publishes
- one gauge per job plus a counter for retries.
- """
- QUESTION = "\nIn one sentence: what does the runner return when every job fails?"
- def post(url, payload, timeout=1800):
- req = urllib.request.Request(
- url, data=json.dumps(payload).encode(), headers={"content-type": "application/json"}
- )
- with urllib.request.urlopen(req, timeout=timeout) as r:
- return json.loads(r.read())
- def build_prompt(base, model, target):
- """Repeat PARAGRAPH until the tokenizer reports at least `target` tokens."""
- low, high = 1, 16
- while True:
- text = PARAGRAPH * high + QUESTION
- if count_tokens(base, model, text) >= target:
- break
- low, high = high, high * 2
- while low < high:
- mid = (low + high) // 2
- if count_tokens(base, model, PARAGRAPH * mid + QUESTION) >= target:
- high = mid
- else:
- low = mid + 1
- text = PARAGRAPH * low + QUESTION
- return text, count_tokens(base, model, text)
- def count_tokens(base, model, text):
- return post(base + "/tokenize", {"model": model, "prompt": text}, timeout=300)["count"]
- def complete(base, model, prompt, max_tokens):
- body = {
- "model": model,
- "messages": [{"role": "user", "content": prompt}],
- "temperature": 0,
- "seed": 1234,
- "max_tokens": max_tokens,
- }
- choice = post(base + "/v1/chat/completions", body)["choices"][0]
- message = choice["message"]
- # Compare the reasoning too, under either spelling: `reasoning_content` from
- # SGLang, `reasoning` from vLLM. A thinking model whose trace does not close
- # inside max_tokens returns empty content, and hashing content alone then
- # groups every run together and reports perfect agreement -- the divergence
- # this script exists to find happens in the trace as readily as in the answer.
- text = "".join(
- message.get(key) or ""
- for key in ("reasoning_content", "reasoning", "content")
- )
- if not text:
- raise SystemExit(
- f"empty completion and empty reasoning ({choice.get('finish_reason')}, "
- f"{max_tokens} max_tokens): nothing to compare. Raise --max-tokens so "
- "the trace can close, or send reasoning_effort=none."
- )
- return text
- def main():
- ap = argparse.ArgumentParser()
- ap.add_argument("--url", default="http://127.0.0.1:8000")
- ap.add_argument("--model", required=True)
- ap.add_argument("--prompt-tokens", type=int, default=42000)
- ap.add_argument("--max-tokens", type=int, default=48)
- ap.add_argument("--runs", type=int, default=16)
- ap.add_argument("--concurrency", type=int, default=3)
- a = ap.parse_args()
- base = a.url.rstrip("/")
- prompt, n_tokens = build_prompt(base, a.model, a.prompt_tokens)
- print(f"prompt: {n_tokens} tokens, {a.runs} runs at temperature 0, seed 1234")
- results, lock = [], threading.Lock()
- work = queue.Queue()
- for i in range(a.runs):
- work.put(i)
- def worker():
- while True:
- try:
- work.get_nowait()
- except queue.Empty:
- return
- try:
- out = complete(base, a.model, prompt, a.max_tokens)
- except Exception as exc:
- out = f"<error {exc!r}>"
- with lock:
- results.append(out)
- print(f" {len(results):>3}/{a.runs} {hashlib.sha1(out.encode()).hexdigest()[:8]}",
- flush=True)
- threads = [threading.Thread(target=worker) for _ in range(a.concurrency)]
- for t in threads:
- t.start()
- for t in threads:
- t.join()
- groups = collections.Counter(results)
- print(f"\n{len(groups)} distinct completion(s) from {a.runs} identical requests")
- for text, n in groups.most_common():
- print(f"\n--- {n} run(s) ---\n{text}")
- raise SystemExit(0 if len(groups) == 1 else 1)
- if __name__ == "__main__":
- main()
|