| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- #!/usr/bin/env python3
- """Do identical requests give identical logprobs?
- Standard library only. Point it at a running vLLM OpenAI server with no other
- traffic:
- python determinism.py --url http://127.0.0.1:8000 --model glm53 prompt
- python determinism.py --url http://127.0.0.1:8000 --model glm53 generate
- prompt: for each length, sends one prompt RUNS times with prompt_logprobs and
- reports, per pair of runs, how many prompt logprobs differ, the first differing
- position and the largest difference.
- generate: sends one prompt RUNS times and compares the generated tokens and
- their logprobs.
- Every request carries a fresh cache_salt, so none of them hits the prefix
- cache. A request that ran alongside other traffic is flagged, because a
- different batch shape changes the rounding.
- """
- import argparse
- import itertools
- import json
- import os
- import random
- import string
- import urllib.request
- def post(base, path, payload, timeout=1800):
- request = urllib.request.Request(
- base + path, data=json.dumps(payload).encode(), headers={"content-type": "application/json"})
- with urllib.request.urlopen(request, timeout=timeout) as response:
- return json.loads(response.read())
- def finished(base):
- """Requests the server has completed so far."""
- text = urllib.request.urlopen(base + "/metrics", timeout=30).read().decode()
- return sum(float(line.split()[-1]) for line in text.splitlines()
- if line.startswith("vllm:request_success_total"))
- def tokens(base, model, n, seed):
- """n token ids of pseudo-words from a fixed seed: the same ids on any server with this tokenizer."""
- rng = random.Random(seed)
- ids = []
- while len(ids) < n:
- text = " ".join("".join(rng.choice(string.ascii_lowercase) for _ in range(rng.randint(2, 9)))
- for _ in range(3000)) + ".\n"
- ids += post(base, "/tokenize", {"model": model, "prompt": text, "add_special_tokens": False})["tokens"]
- return ids[:n]
- def complete(base, model, prompt, **extra):
- before = finished(base)
- out = post(base, "/v1/completions", {"model": model, "prompt": prompt, "temperature": 0,
- "cache_salt": os.urandom(8).hex(), **extra})
- others = finished(base) - before - 1
- return out["choices"][0], int(others)
- def compare(a, b):
- diffs = [abs(x - y) for x, y in zip(a, b)]
- first = next((i for i, d in enumerate(diffs) if d), None)
- return sum(d > 0 for d in diffs), first, max(diffs, default=0.0)
- def prompt_mode(args):
- for n in args.lengths:
- prompt = tokens(args.url, args.model, n, seed=n)
- runs = []
- for _ in range(args.runs):
- choice, others = complete(args.url, args.model, prompt, max_tokens=1, prompt_logprobs=0)
- runs.append([next(iter(e.values()))["logprob"] for e in choice["prompt_logprobs"][1:]])
- if others:
- print(f" warning: {others} other requests finished during a {n}-token run")
- for i, j in itertools.combinations(range(len(runs)), 2):
- count, first, largest = compare(runs[i], runs[j])
- print(f"{n:6d} tokens run {j} vs {i}: {count} of {len(runs[i])} differ, "
- f"first at {first}, max {largest:.4f}")
- def generate_mode(args):
- prompt = tokens(args.url, args.model, args.length, seed=args.length)
- runs = []
- for i in range(args.runs):
- choice, others = complete(args.url, args.model, prompt, max_tokens=args.max_tokens, logprobs=1)
- runs.append((choice["logprobs"]["tokens"], choice["logprobs"]["token_logprobs"]))
- print(f"run {i}: first logprobs {[round(x, 4) for x in runs[-1][1][:3]]}"
- + (f" ({others} other requests during the run)" if others else ""))
- for i in range(1, len(runs)):
- count, first, largest = compare(runs[0][1], runs[i][1])
- print(f"run {i} vs 0: tokens {'same' if runs[i][0] == runs[0][0] else 'DIFFER'}, "
- f"{count} logprobs differ, first at {first}, max {largest:.4f}")
- def main():
- parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
- parser.add_argument("--url", default="http://127.0.0.1:8000")
- parser.add_argument("--model", default="glm53")
- sub = parser.add_subparsers(dest="mode", required=True)
- p = sub.add_parser("prompt")
- p.add_argument("--lengths", type=lambda s: [int(x) for x in s.split(",")],
- default=[3000, 6000, 10000, 14000, 40000])
- p.add_argument("--runs", type=int, default=3)
- g = sub.add_parser("generate")
- g.add_argument("--length", type=int, default=5208)
- g.add_argument("--runs", type=int, default=6)
- g.add_argument("--max-tokens", type=int, default=48)
- args = parser.parse_args()
- # The first long request after a boot can read differently; spend it here.
- complete(args.url, args.model, tokens(args.url, args.model, 8000, seed=0), max_tokens=1)
- (prompt_mode if args.mode == "prompt" else generate_mode)(args)
- if __name__ == "__main__":
- main()
|