Files
esh-pfi-infrastructure/services/gen-seat-mixed-quant/bench/concbench.py
T
vh d47dd10795 feat(gen-seat): swap to AEON-ULTIMATE NVFP4 W4A4; default reasoning_effort=medium
Operator call: the incumbent abliterated model was the first one we could
find, not an optimised pick. sakamakismile/Qwen3.8-27B-AEON-ULTIMATE-
UNCENSORED-NVFP4 (base AEON-7 BF16, abliterix-abliterated, Apache-2.0),
byte-verified at /tank/aimodels/qwen38-27b-aeon-ultimate-nvfp4.

Measured on the same harness, same GPU, cache-busted per playbook 5.
Baseline was RE-measured live before the swap rather than trusted:

                        incumbent (W4A4+FP8 mixed)   AEON (W4A4)
  decode bs=1           94.09 tok/s                 104.22   +10.8%
  MTP acceptance        47.7%                        52.3%    +4.6pp
  abliteration          4/4                          4/4
  surface               6/6                          6/6
  weights               22.5 GB                      20.6 GB  -8.4%

AEON concurrency: conc=1 98.48 tok/s aggregate; conc=6 381.29 aggregate /
63.55 per-stream, MTP holding 50.6% under load.

Surface 6/6 includes vision (image-judge rides this seat) and a 36k-token
needle retrieval, which was the specific risk in going full-W4A4 -- the
packager only validated 32k, and W4A4 long-context collapse is in our own
notes from the Granite work. It held.

reasoning_effort: the AEON template defaults to xhigh (template line 47),
and at xhigh this model can spend its entire budget inside <think> and
emit no answer -- a silent-empty-response hazard for the automated
summarizer/classifier consumers. Seat now pins the default to medium via
--default-chat-template-kwargs, per-request overridable. Override PROVEN
live: chat_template_kwargs.reasoning_effort=bogus returns HTTP 400
carrying the template's own exception text, so caller values genuinely
reach the template and invalid ones fail loudly rather than silently
falling back. Empty GEN_REASONING_EFFORT omits the flag for models that do
not read the kwarg -- the Qwen3.6 line ignores it entirely, where setting
it would be a false lever.

All 7 aliases verified routing. Rollback is one .env line; the previous
build is untouched at /tank/aimodels/qwen38-27b-uncensored-nvfp4-mixed.

TWO GAPS, declared:
  - Incumbent concurrency was never captured before the swap (I baselined
    bs=1 only), so the conc=1/6 figures have no same-hardware comparator.
  - Perplexity NOT measured. eval_quality correctly refused it: under
    --speculative-config prompt_logprobs come back ~uniform (median rank
    ~130k), playbook trap 2. A real PPL number needs both seats served
    without spec-decode.

Adds concbench.py (concurrent throughput; wall-clock aggregate, not
sum-of-rates, and delta-based MTP accounting).
2026-08-16 14:56:37 -07:00

139 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""Cache-busted CONCURRENT throughput bench against a vLLM OpenAI seat.
Companion to quickbench.py, which is bs=1 only. Single-stream tok/s and
aggregate throughput are different questions: one is latency-bound, the other is
batch-bound, and a seat can win on one while losing the other.
MEASUREMENT TRAPS THIS AVOIDS (all three have produced confident wrong results
here before — see docs/pfi/model-quantization-playbook.md §5):
1. Prefix caching fakes speed. Every request gets a FRESH UNSEEDED nonce, so
no prompt is ever repeated. Never seed the cache-buster: a seeded nonce
regenerates the previous run's prompts verbatim and reads cache-hit
throughput (~41k tok/s) instead of real prefill (~5k).
2. MTP acceptance is read from /metrics as a DELTA across the run, not as a
cumulative total, or a long-lived seat's history swamps the measurement.
3. Aggregate throughput is computed from the WALL CLOCK of the whole batch,
not the sum of per-request rates — the latter double-counts overlap and
reports a number the seat cannot actually deliver.
Usage:
uv run concbench.py --base http://10.250.50.54:8015 \
--model qwen3.8-27b-uncensored --concurrency 1 --concurrency 6
"""
import argparse, json, random, statistics, string, sys, time
import urllib.request
from concurrent.futures import ThreadPoolExecutor
TOPICS = [
"a coral reef ecosystem", "the Roman aqueduct system",
"how lithium-ion cells degrade", "the history of the printing press",
"how radar altimeters work", "glacier mass balance",
"the design of the Saturn V F-1 engine", "how sourdough fermentation works",
"the metallurgy of Damascus steel", "how noise-cancelling headphones work",
"the ecology of mangrove forests", "the development of the marine chronometer",
]
def nonce(n=14):
# Unseeded on purpose. A seeded RNG reproduces the previous run's prompts
# and turns this into a prefix-cache benchmark.
return "".join(random.choices(string.ascii_lowercase + string.digits, k=n))
def spec_counters(base):
"""(drafted, accepted) from /metrics, or (None, None) if unavailable."""
try:
with urllib.request.urlopen(base + "/metrics", timeout=30) as r:
text = r.read().decode()
except Exception:
return None, None
d = a = None
for line in text.splitlines():
if line.startswith("#"):
continue
if line.startswith("vllm:spec_decode_num_draft_tokens_total"):
d = float(line.rsplit(" ", 1)[1])
elif line.startswith("vllm:spec_decode_num_accepted_tokens_total"):
a = float(line.rsplit(" ", 1)[1])
return d, a
def one(base, model, max_tokens, timeout):
body = {
"model": model,
"messages": [{"role": "user", "content":
f"[req-{nonce()}] Explain {random.choice(TOPICS)} in detail."}],
"max_tokens": max_tokens,
"temperature": 0.7,
}
req = urllib.request.Request(
base + "/v1/chat/completions", data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
t0 = time.time()
with urllib.request.urlopen(req, timeout=timeout) as r:
d = json.load(r)
dt = time.time() - t0
return d["usage"]["completion_tokens"], dt
def run(base, model, conc, reqs, max_tokens, timeout):
d0, a0 = spec_counters(base)
t0 = time.time()
with ThreadPoolExecutor(max_workers=conc) as ex:
out = list(ex.map(lambda _: one(base, model, max_tokens, timeout), range(reqs)))
wall = time.time() - t0
d1, a1 = spec_counters(base)
toks = sum(t for t, _ in out)
lat = [dt for _, dt in out]
acc = None
if d0 is not None and a0 is not None and d1 is not None and a1 is not None:
drafted = d1 - d0
if drafted > 0:
acc = (a1 - a0) / drafted * 100
return {
"concurrency": conc, "requests": reqs, "wall_s": round(wall, 2),
"completion_tokens": toks,
"aggregate_tok_s": round(toks / wall, 2), # wall clock, not sum-of-rates
"per_stream_tok_s": round(toks / wall / conc, 2),
"median_latency_s": round(statistics.median(lat), 2),
"mtp_acceptance_pct": round(acc, 1) if acc is not None else None,
}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--base", default="http://10.250.50.54:8015")
ap.add_argument("--model", default="qwen3.8-27b-uncensored")
ap.add_argument("--concurrency", type=int, action="append", default=[])
ap.add_argument("--requests-per-stream", type=int, default=4)
ap.add_argument("--max-tokens", type=int, default=400)
ap.add_argument("--timeout", type=int, default=900)
ap.add_argument("--tag", default="run")
ap.add_argument("--out", default=None)
a = ap.parse_args()
concs = a.concurrency or [1, 6]
results = []
for c in concs:
reqs = c * a.requests_per_stream
r = run(a.base, a.model, c, reqs, a.max_tokens, a.timeout)
results.append(r)
print(f" conc={c:<3} reqs={reqs:<3} wall={r['wall_s']:>7.2f}s "
f"aggregate={r['aggregate_tok_s']:>8.2f} tok/s "
f"per-stream={r['per_stream_tok_s']:>7.2f} "
f"median-lat={r['median_latency_s']:>6.2f}s "
f"MTP={r['mtp_acceptance_pct']}%")
if a.out:
json.dump({"tag": a.tag, "model": a.model, "base": a.base,
"max_tokens": a.max_tokens, "results": results},
open(a.out, "w"), indent=2)
return 0
if __name__ == "__main__":
sys.exit(main())