| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- #!/usr/bin/env python3
- """Does a prefix-cache hit change what the model generates?
- Standard library only. Point it at a running vLLM OpenAI server with prefix
- caching on and no other traffic:
- python prefix_cache.py --url http://127.0.0.1:8000 --model glm53
- For each shared-prefix length L: request A (the first L + 700 tokens of a
- document) fills the cache under a salt. Request B (the same first L tokens,
- then 600 tokens of other text) runs twice: under A's salt, so it can hit the
- cache, and twice under fresh salts, so it prefills cold. All three generate
- greedily. The warm run is compared with the first cold run, and the two cold
- runs with each other: that second comparison is the noise floor, the difference
- two runs show without the cache involved. A length whose cache hit differs by
- more than the floor runs twice more with new salts: corruption repeats at the
- same length, while occasional decode noise (the verify length picked from step
- timing, for example) does not. The hit size comes from the
- server's prefix-cache hit counter, since usage.cached_tokens can read 0 on
- hybrid models.
- Run determinism.py first: this compares two runs, so it means something only
- when two cold runs already agree. Generated logprobs are used because
- prompt_logprobs requests skip the prefix cache.
- The default lengths are multiples and half multiples of the attention block
- and the mamba block, read from the server's cache_config_info.
- """
- import argparse
- 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 metrics(base):
- return urllib.request.urlopen(base + "/metrics", timeout=30).read().decode()
- def counter(text, name):
- return sum(float(line.split()[-1]) for line in text.splitlines() if line.startswith(name))
- def label(text, name):
- return text.split(f'{name}="', 1)[1].split('"', 1)[0]
- def tokens(base, model, n, seed):
- """n token ids of pseudo-words from a fixed seed."""
- 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 generate(base, model, prompt, salt, max_tokens):
- before = metrics(base)
- out = post(base, "/v1/completions", {"model": model, "prompt": prompt, "temperature": 0,
- "max_tokens": max_tokens, "logprobs": 1, "cache_salt": salt})
- after = metrics(base)
- hit = counter(after, "vllm:prefix_cache_hits_total") - counter(before, "vllm:prefix_cache_hits_total")
- others = counter(after, "vllm:request_success_total") - counter(before, "vllm:request_success_total") - 1
- lp = out["choices"][0]["logprobs"]
- return lp["tokens"], lp["token_logprobs"], int(hit), int(others)
- def compare(a, b):
- """(first differing generated token or None, largest logprob difference, same tokens)."""
- diffs = [abs(x - y) for x, y in zip(a[1], b[1])]
- return next((i for i, d in enumerate(diffs) if d), None), max(diffs, default=0.0), a[0] == b[0]
- def describe(first, largest, same_tokens):
- if first is None and same_tokens:
- return "identical"
- return f"from token {first}, max {largest:.4f}" + ("" if same_tokens else ", tokens differ")
- 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")
- parser.add_argument("--lengths", type=lambda s: [int(x) for x in s.split(",")])
- parser.add_argument("--max-tokens", type=int, default=48)
- args = parser.parse_args()
- text = metrics(args.url)
- block, mamba = int(label(text, "block_size")), int(label(text, "mamba_block_size"))
- print(f"block_size {block}, mamba_block_size {mamba}, mamba_cache_mode {label(text, 'mamba_cache_mode')}")
- lengths = args.lengths or sorted({b * h // 2 for b in (block, mamba) for h in (3, 4, 5, 6, 8, 9, 14, 15)})
- doc = tokens(args.url, args.model, max(lengths) + 800, seed=1)
- tail = tokens(args.url, args.model, 600, seed=2)
- def trial(n):
- """(hit, warm vs cold, cold vs cold, other requests, worse than the floor) for length n."""
- salt = os.urandom(8).hex()
- generate(args.url, args.model, doc[:n + 700], salt, args.max_tokens)
- prompt = doc[:n] + tail
- warm = generate(args.url, args.model, prompt, salt, args.max_tokens)
- cold = generate(args.url, args.model, prompt, os.urandom(8).hex(), args.max_tokens)
- cold2 = generate(args.url, args.model, prompt, os.urandom(8).hex(), args.max_tokens)
- wf, wl, ws = compare(warm, cold)
- cf, cl, cs = compare(cold, cold2)
- # Worse than the floor: an earlier first difference, or a larger one.
- worse = (wf is not None or not ws) and (cf is None or (wf or 0) < cf or wl > 2 * cl)
- return warm[2], (wf, wl, ws), (cf, cl, cs), warm[3] + cold[3] + cold2[3], worse
- failed = 0
- for n in lengths:
- hit, w, c, others, worse = trial(n)
- verdict = ""
- if worse:
- repeats = sum(trial(n)[4] for _ in range(2))
- verdict = f" <- worse than noise, and again in {repeats} of 2 reruns"
- failed += repeats == 2
- print(f"L {n:6d} (mod {block} = {n % block:5d}, mod {mamba} = {n % mamba:5d}) hit {hit:6d} "
- f"warm vs cold: {describe(*w)}; cold vs cold: {describe(*c)}{verdict}"
- + (f" ({others} other requests)" if others else ""))
- print("PASS" if not failed else f"{failed} of {len(lengths)} lengths repeatedly worse than the noise floor")
- if __name__ == "__main__":
- main()
|