DeepInfer/serve/check_prefix_cache.py
Jiejing c6faea3bc2 Add a minimal OpenAI-compatible chat server over the offline engine
serve/openai_server.py exposes /v1/chat/completions (streaming, tools,
usage) on top of LLMEngine: one engine process per GPU (attention DP + MoE
EP, the layout of examples/benchmark.py), prompts rendered with the
checkpoint's own encoding.py, conversation-affinity routing, a watchdog
that fails open requests if an engine rank dies, and /deepinfer/metrics.
serve/server.sh runs it as a process group so a stop also reaps the
engine's helper processes, which otherwise keep the GPUs.

It integrates the host prefix cache from deepinfer/hicache when present
(--hicache-size-gb, --hicache-state-slots, per-request "prefix_cache":
false bypass; serve/check_prefix_cache.py is the A/B check) and serves
without it otherwise. It also sets DEEPINFER_PREFILL_ANY_ATTEMPT_INTERVAL
to 2 by default (--prefill-any-interval): with the engine default of 100,
a rank whose peers are idle admits a waiting prompt only every 100 decode
steps, a ~1.4 s TTFT floor on every request in a serving setting.

Used to drive the InferenceX AgentX replay (aiperf) on 4x GB300.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNVW4sqoaPqbNtSrgpA2zK
2026-10-21 22:26:29 +00:00

88 lines
3.6 KiB
Python

"""A/B check of the host prefix cache on a running serve/openai_server.py.
Builds a multi-turn conversation with a long shared prefix, sends every turn twice (cache bypassed via
"prefix_cache": false, then with the cache) with greedy sampling, and compares outputs and TTFT.
python serve/check_prefix_cache.py --port 8888 --turns 3 --prefix-tokens 6000
"""
from __future__ import annotations
import argparse
import json
import time
import requests
p = argparse.ArgumentParser()
p.add_argument("--port", type=int, default=8888)
p.add_argument("--model", default="deepseek-ai/DeepSeek-V4.1-Flash-1017")
p.add_argument("--turns", type=int, default=3)
p.add_argument("--prefix-tokens", type=int, default=6000, help="approximate size of the shared document")
p.add_argument("--max-tokens", type=int, default=64)
args = p.parse_args()
URL = f"http://localhost:{args.port}/v1/chat/completions"
# A deterministic pseudo-document; ~1 token per word for this tokenizer is close enough.
words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu"]
doc = " ".join(f"{words[i % len(words)]}{i % 97}" for i in range(args.prefix_tokens))
questions = [
"Which word appears right after 'gamma2'? Answer briefly.",
"How many distinct word stems are used in the document? Answer briefly.",
"Quote the first five words of the document.",
"What is the last word of the document?",
]
def ask(messages: list[dict], use_cache: bool) -> tuple[str, float, float, dict]:
body = {
"model": args.model,
"messages": messages,
"stream": True,
"stream_options": {"include_usage": True},
"temperature": 0,
"max_tokens": args.max_tokens,
"seed": 1,
"prefix_cache": use_cache,
}
t0 = time.perf_counter()
ttft = None
text, usage = [], {}
with requests.post(URL, json=body, stream=True, timeout=1800) as r:
r.raise_for_status()
for line in r.iter_lines():
if not line or not line.startswith(b"data: ") or line == b"data: [DONE]":
continue
obj = json.loads(line[6:])
if obj.get("usage"):
usage = obj["usage"]
for ch in obj.get("choices", []):
delta = ch["delta"].get("content")
if delta:
if ttft is None:
ttft = time.perf_counter() - t0
text.append(delta)
return "".join(text), ttft or 0.0, time.perf_counter() - t0, usage
messages = [{"role": "system", "content": "You answer questions about the document below.\n\n" + doc}]
mismatches = 0
for turn in range(args.turns):
messages.append({"role": "user", "content": questions[turn % len(questions)]})
base, ttft_b, tot_b, usage_b = ask(messages, use_cache=False)
cached, ttft_c, tot_c, usage_c = ask(messages, use_cache=True)
same = base == cached
mismatches += not same
print(
f"turn {turn + 1}: prompt {usage_c.get('prompt_tokens')} tok | TTFT no-cache {ttft_b * 1e3:7.0f} ms, cache {ttft_c * 1e3:7.0f} ms "
f"({ttft_b / max(ttft_c, 1e-6):.1f}x) | outputs {'identical' if same else 'DIFFER'}"
)
if not same:
print(" no-cache:", repr(base[:200]))
print(" cache :", repr(cached[:200]))
messages.append({"role": "assistant", "content": base})
stats = requests.get(f"http://localhost:{args.port}/deepinfer/metrics", timeout=30).text
print("\n".join(line for line in stats.splitlines() if "hicache:hits" in line or "hicache:lookups" in line or "hit_rate" in line))
raise SystemExit(1 if mismatches else 0)