determinism.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. #!/usr/bin/env python3
  2. """Do identical requests give identical logprobs?
  3. Standard library only. Point it at a running vLLM OpenAI server with no other
  4. traffic:
  5. python determinism.py --url http://127.0.0.1:8000 --model glm53 prompt
  6. python determinism.py --url http://127.0.0.1:8000 --model glm53 generate
  7. prompt: for each length, sends one prompt RUNS times with prompt_logprobs and
  8. reports, per pair of runs, how many prompt logprobs differ, the first differing
  9. position and the largest difference.
  10. generate: sends one prompt RUNS times and compares the generated tokens and
  11. their logprobs.
  12. Every request carries a fresh cache_salt, so none of them hits the prefix
  13. cache. A request that ran alongside other traffic is flagged, because a
  14. different batch shape changes the rounding.
  15. """
  16. import argparse
  17. import itertools
  18. import json
  19. import os
  20. import random
  21. import string
  22. import urllib.request
  23. def post(base, path, payload, timeout=1800):
  24. request = urllib.request.Request(
  25. base + path, data=json.dumps(payload).encode(), headers={"content-type": "application/json"})
  26. with urllib.request.urlopen(request, timeout=timeout) as response:
  27. return json.loads(response.read())
  28. def finished(base):
  29. """Requests the server has completed so far."""
  30. text = urllib.request.urlopen(base + "/metrics", timeout=30).read().decode()
  31. return sum(float(line.split()[-1]) for line in text.splitlines()
  32. if line.startswith("vllm:request_success_total"))
  33. def tokens(base, model, n, seed):
  34. """n token ids of pseudo-words from a fixed seed: the same ids on any server with this tokenizer."""
  35. rng = random.Random(seed)
  36. ids = []
  37. while len(ids) < n:
  38. text = " ".join("".join(rng.choice(string.ascii_lowercase) for _ in range(rng.randint(2, 9)))
  39. for _ in range(3000)) + ".\n"
  40. ids += post(base, "/tokenize", {"model": model, "prompt": text, "add_special_tokens": False})["tokens"]
  41. return ids[:n]
  42. def complete(base, model, prompt, **extra):
  43. before = finished(base)
  44. out = post(base, "/v1/completions", {"model": model, "prompt": prompt, "temperature": 0,
  45. "cache_salt": os.urandom(8).hex(), **extra})
  46. others = finished(base) - before - 1
  47. return out["choices"][0], int(others)
  48. def compare(a, b):
  49. diffs = [abs(x - y) for x, y in zip(a, b)]
  50. first = next((i for i, d in enumerate(diffs) if d), None)
  51. return sum(d > 0 for d in diffs), first, max(diffs, default=0.0)
  52. def prompt_mode(args):
  53. for n in args.lengths:
  54. prompt = tokens(args.url, args.model, n, seed=n)
  55. runs = []
  56. for _ in range(args.runs):
  57. choice, others = complete(args.url, args.model, prompt, max_tokens=1, prompt_logprobs=0)
  58. runs.append([next(iter(e.values()))["logprob"] for e in choice["prompt_logprobs"][1:]])
  59. if others:
  60. print(f" warning: {others} other requests finished during a {n}-token run")
  61. for i, j in itertools.combinations(range(len(runs)), 2):
  62. count, first, largest = compare(runs[i], runs[j])
  63. print(f"{n:6d} tokens run {j} vs {i}: {count} of {len(runs[i])} differ, "
  64. f"first at {first}, max {largest:.4f}")
  65. def generate_mode(args):
  66. prompt = tokens(args.url, args.model, args.length, seed=args.length)
  67. runs = []
  68. for i in range(args.runs):
  69. choice, others = complete(args.url, args.model, prompt, max_tokens=args.max_tokens, logprobs=1)
  70. runs.append((choice["logprobs"]["tokens"], choice["logprobs"]["token_logprobs"]))
  71. print(f"run {i}: first logprobs {[round(x, 4) for x in runs[-1][1][:3]]}"
  72. + (f" ({others} other requests during the run)" if others else ""))
  73. for i in range(1, len(runs)):
  74. count, first, largest = compare(runs[0][1], runs[i][1])
  75. print(f"run {i} vs 0: tokens {'same' if runs[i][0] == runs[0][0] else 'DIFFER'}, "
  76. f"{count} logprobs differ, first at {first}, max {largest:.4f}")
  77. def main():
  78. parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
  79. parser.add_argument("--url", default="http://127.0.0.1:8000")
  80. parser.add_argument("--model", default="glm53")
  81. sub = parser.add_subparsers(dest="mode", required=True)
  82. p = sub.add_parser("prompt")
  83. p.add_argument("--lengths", type=lambda s: [int(x) for x in s.split(",")],
  84. default=[3000, 6000, 10000, 14000, 40000])
  85. p.add_argument("--runs", type=int, default=3)
  86. g = sub.add_parser("generate")
  87. g.add_argument("--length", type=int, default=5208)
  88. g.add_argument("--runs", type=int, default=6)
  89. g.add_argument("--max-tokens", type=int, default=48)
  90. args = parser.parse_args()
  91. # The first long request after a boot can read differently; spend it here.
  92. complete(args.url, args.model, tokens(args.url, args.model, 8000, seed=0), max_tokens=1)
  93. (prompt_mode if args.mode == "prompt" else generate_mode)(args)
  94. if __name__ == "__main__":
  95. main()