boundary_parity.py 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. #!/usr/bin/env python3
  2. """Does a verify step that commits exactly to a KDA block boundary break the reply?
  3. With prefix caching on, vLLM keeps each request's KDA state in the block of its
  4. current position (align mode). RecoverSSM used to store the state after a
  5. verify in column committed // block_size: when every draft was accepted and the
  6. commit ended exactly on a boundary, that is the next block, which is not
  7. allocated yet, so the state was lost and the rest of the reply turned to noise.
  8. The model repeats a passage word for word (drafts almost all accepted) from a
  9. prompt that ends ~150 tokens short of a boundary; 16 variants add 0-15 filler
  10. tokens so the verify steps land at different offsets. Force k=1 first, so every
  11. fully accepted step commits exactly 2 tokens:
  12. echo "force 1" | sudo tee /var/lib/docker/volumes/glm53_cache/_data/adaptive-k
  13. python3 dev/repro/boundary_parity.py http://127.0.0.1:8002
  14. sudo rm /var/lib/docker/volumes/glm53_cache/_data/adaptive-k
  15. A variant passes when its output reaches the passage's last line. On the
  16. broken code about half the variants loop or turn to noise right after the
  17. boundary; with the fix, or with VLLM_GLM5NEXT_RECOVERSSM=0, none should.
  18. Reads the passage from /usr/share/common-licenses/GPL-3.
  19. usage: boundary_parity.py BASE_URL [VARIANTS]
  20. """
  21. import json, os, sys, urllib.request
  22. BASE = sys.argv[1]
  23. N = int(sys.argv[2]) if len(sys.argv) > 2 else 16
  24. MODEL = os.environ.get("GATE_MODEL", "glm53")
  25. _m = urllib.request.urlopen(BASE + "/metrics", timeout=30).read().decode()
  26. MAMBA = int(_m.split('mamba_block_size="', 1)[1].split('"', 1)[0])
  27. passage = " ".join(open("/usr/share/common-licenses/GPL-3").read().split()[400:1000])
  28. last_line = " ".join(passage.split()[-12:])
  29. def post(path, body):
  30. r = urllib.request.Request(BASE + path, json.dumps(body).encode(), {"Content-Type": "application/json"})
  31. return json.loads(urllib.request.urlopen(r, timeout=1800).read())
  32. def messages(filler):
  33. return [{"role": "user", "content": "Notes (ignore these): " + filler + "\n\nRepeat the following passage "
  34. "exactly, word for word, with nothing before or after it:\n\n" + passage}]
  35. def prompt_len(filler):
  36. return post("/tokenize", {"model": MODEL, "messages": messages(filler), "add_generation_prompt": True,
  37. "chat_template_kwargs": {"enable_thinking": False}})["count"]
  38. filler = ""
  39. for _ in range(8): # one-token words until the prompt ends ~160 tokens short of a boundary
  40. short = (MAMBA - 160 - prompt_len(filler) % MAMBA) % MAMBA
  41. if short == 0:
  42. break
  43. filler += " the" * short
  44. broken = 0
  45. for v in range(N):
  46. f = filler + " the" * v
  47. P = prompt_len(f)
  48. r = post("/v1/chat/completions", {"model": MODEL, "messages": messages(f), "max_tokens": 1200,
  49. "temperature": 0, "cache_salt": os.urandom(8).hex(),
  50. "chat_template_kwargs": {"enable_thinking": False}})
  51. out = r["choices"][0]["message"].get("content") or ""
  52. ok = last_line in " ".join(out.split())
  53. broken += not ok
  54. print(f"variant {v:2d} prompt {P} boundary {-P % MAMBA:3d} tokens into the reply "
  55. f"out {r['usage']['completion_tokens']:4d} tokens {'ok' if ok else 'BROKEN'} tail {out[-70:]!r}", flush=True)
  56. print(f"{'FAIL' if broken else 'PASS'}: {broken} of {N} broken")