perf(gen-seat): record prefill measurements — roughly doubled

Closes the one axis of the original premise left unverified. Measured
cold (cache-busted) on both builds under matching serve configs:

  ~6.7k-token prompt   3,206 -> 6,334 tok/s prefill   (+98%)
  ~27k-token prompt    2,862 -> 5,085 tok/s prefill   (+78%)
  TTFT on a ~27k doc    9.43 -> 5.31 s                (-44%)

Prefill gains far exceed the +18% decode gain, and that ordering is the
expected one: decode at bs=1 is memory-bandwidth-bound and the weights
are 4-bit under either scheme, so little changes; prefill is
compute-bound, which is where native Blackwell FP4 tensor cores replace
the Marlin dequant-to-BF16 path. The summarizer aliases are the
consumers that feel this.

Adds bench/prefill_bench.py plus the raw JSON. The harness deliberately
uses SystemRandom: a seeded nonce regenerates the previous run's prompts
verbatim, prefix caching then serves them, and the first attempt read
~41k tok/s of cache-hit rather than ~5k of actual prefill.
This commit is contained in:
2026-08-15 02:33:50 -07:00
parent fa4f652a39
commit 4a5c3fcccf
5 changed files with 119 additions and 0 deletions
+12
View File
@@ -90,13 +90,25 @@ Speed alone does not justify cutting over a seat backing 7 LiteLLM aliases.
| metric | W4A16 (old) | mixed (new) | delta |
|---|---|---|---|
| decode tok/s, bs=1, cache-busted | 80.12 | **94.53** | **+18.0%** |
| **prefill tok/s, ~6.7k prompt** | 3,206 | **6,334** | **+98%** |
| **prefill tok/s, ~27k prompt** | 2,862 | **5,085** | **+78%** |
| TTFT on a ~27k-token doc | 9.43 s | **5.31 s** | 44% |
| MTP acceptance | 47.8% | 47.7% | unchanged |
| perplexity, 6 held-out passages | 6.941 | 7.059 | +1.7% worse |
| abliteration compliance | 4/4 | 4/4 | preserved |
| weights on disk | 27.7 GB | 22.5 GB | 19% |
**Prefill roughly doubled** — the bigger practical win, and exactly what theory predicts:
decode at bs=1 is memory-bandwidth-bound (weights are 4-bit either way, so little changes),
while prefill is compute-bound and is where Blackwell's native FP4 tensor cores replace the
Marlin dequant-to-BF16 path. This is what the `summarizer` / `summarizer-large` aliases feel
on long documents.
- `quickbench.py` — cache-busted bs=1 decode + MTP acceptance. **Bust the cache:** with
a fixed prompt, prefix caching returns byte-identical timings and you measure nothing.
- `prefill_bench.py` — TTFT on long prompts. Same trap, worse: a *seeded* nonce reproduces
the previous run's prompts verbatim, so prefix caching serves them and you read ~41k tok/s
of cache-hit instead of ~5k of real prefill. Uses `SystemRandom`; never seed it.
- `eval_quality.py` — perplexity, deterministic generations, abliteration survival.
**PPL must be measured with `--speculative-config` OFF**: under MTP, vLLM's
`prompt_logprobs` come back ~uniform over the vocab (median rank ~10^5, logprob
@@ -0,0 +1,29 @@
{
"tag": "w4a16-recheck",
"model": "probe",
"tok_s_median": 77.51086756145159,
"tok_s_mean": 77.55102759761677,
"tok_s_min": 69.14223827875307,
"tok_s_max": 88.11959921725081,
"mtp_accept_median": 0.4493388052895577,
"rates": [
82.25341481185131,
77.12830062541333,
78.45783182678721,
69.14223827875307,
71.73740809898078,
77.89343449748986,
88.11959921725081,
75.67599342440771
],
"accs": [
0.494824016563147,
0.44573643410852715,
0.4536489151873767,
0.36300174520069806,
0.3894927536231884,
0.45294117647058824,
0.56,
0.43238095238095237
]
}
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Prefill throughput via TTFT on long prompts (prefill-dominated)."""
import argparse, json, random, statistics, time, urllib.request
def ttft(base, model, prompt):
req = urllib.request.Request(base + "/v1/chat/completions",
data=json.dumps({"model": model, "messages": [{"role": "user", "content": prompt}],
"max_tokens": 16, "temperature": 0, "stream": True,
"chat_template_kwargs": {"enable_thinking": False}}).encode(),
headers={"Content-Type": "application/json"})
t0 = time.perf_counter()
with urllib.request.urlopen(req, timeout=900) as r:
for line in r:
if line.startswith(b"data: ") and b"[DONE]" not in line:
dl = json.loads(line[6:])["choices"][0].get("delta", {})
if dl.get("content") or dl.get("reasoning"):
return time.perf_counter() - t0
return None
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--base", required=True)
ap.add_argument("--model", required=True)
ap.add_argument("--tag", default="run")
ap.add_argument("--out", default=None)
a = ap.parse_args()
# NO fixed seed: a seeded nonce reproduces the previous run's prompts verbatim,
# so --enable-prefix-caching serves them from cache and you measure cache-hit
# TTFT, not prefill. (First attempt read 41k tok/s that way.) Lengths stay
# identical across builds; only the nonce differs.
rnd = random.SystemRandom()
res = {"tag": a.tag}
body = ""
for reps, label in [(4000, "~6.5k"), (16000, "~26k")]:
ts = []
for _ in range(3):
n = rnd.randint(10**12, 10**13)
body = f"[doc {n}] The archived engineering report details subsystem telemetry. " * (reps // 12)
v = ttft(a.base, a.model, body + "\n\nSummarize in one sentence.")
if v:
ts.append(v)
tok = len(body) // 4
med = statistics.median(ts)
rate = tok / med
res[label] = {"approx_prompt_tokens": tok, "ttft_median_s": med, "prefill_tok_s": rate}
print(f" {label} prompt (~{tok} tok): TTFT {med:.3f}s -> ~{rate:,.0f} tok/s prefill")
if a.out:
json.dump(res, open(a.out, "w"), indent=2)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,13 @@
{
"tag": "mixed",
"~6.5k": {
"approx_prompt_tokens": 6743,
"ttft_median_s": 1.0645924881100655,
"prefill_tok_s": 6333.878996244484
},
"~26k": {
"approx_prompt_tokens": 26993,
"ttft_median_s": 5.307923917658627,
"prefill_tok_s": 5085.415770598847
}
}
@@ -0,0 +1,13 @@
{
"tag": "w4a16",
"~6.5k": {
"approx_prompt_tokens": 6743,
"ttft_median_s": 2.1035482240840793,
"prefill_tok_s": 3205.536209152522
},
"~26k": {
"approx_prompt_tokens": 26993,
"ttft_median_s": 9.43084010668099,
"prefill_tok_s": 2862.205243080904
}
}