bench-spec-ab: think-free echo/structured shapes, and prove the echo is an echo

The first run asked for a verbatim repeat with thinking ON, so the model copied
the passage inside reasoning_content, content came back empty for all three
configurations, and the cross-configuration identity check compared '' to '' --
a pass that proves nothing. echo and structured now disable thinking via
chat_template_kwargs, the echo row carries a copy_ratio (share of answer lines
that really are passage lines), and an empty answer in any shape is a loud
warning rather than a silent pass.

'structured' is new and answers the case the fleet actually runs on: repetitive
OUTPUT with no prompt overlap (record lists, tables, config rewrites), which is
where an n-gram matcher should pay and an echo-shaped test cannot tell.
This commit is contained in:
Frank committed 2026-09-16 16:27:57 -07:00
1 parent 6e6e7187ca
commit e767810c9a
1 file changed
+64 -40
+64 -40
View File
@@ -17,25 +17,28 @@ Config syntax: `name=SPEC_TYPE[@n_max]`, semicolon-separated. The systemd unit i
restored at the end, so the endpoint comes back on whatever the env file says restored at the end, so the endpoint comes back on whatever the env file says
whatever happens here. whatever happens here.
TWO SHAPES, because a single one answers the wrong question: THREE SHAPES, because a single one answers the wrong question. A proposer can
only draft text it has seen, so the shape decides the verdict:
prose a 1k-token prompt asking for a sentence — nothing to copy, so only a prose a 1k prompt asking for one sentence. Nothing to copy and nothing
model-backed proposer can help. This is the shape that compares repetitive, so only a model-backed proposer can help: this is the
drafters. shape that compares drafters.
echo a ~1.5k-token passage of unique lines with "repeat it back verbatim". echo a ~1.5k passage of unique lines, "repeat it back verbatim". The
The output is almost entirely a copy of the prompt, which is the only output IS the prompt, which is ngram-mod's home turf.
traffic where an n-gram matcher has anything to match. Measuring structured "25 JSON records, one per line" — repetitive OUTPUT with no prompt
ngram-mod on prose alone would call it useless; measuring DFlash on overlap, which is what an ngram matcher is really for in an
echo alone would call it wasteful. agentic fleet (config edits, tables, record lists).
Results on Dankserver (frank-medium payload, 2026-09-16, temperature 0): echo and structured disable thinking (`chat_template_kwargs`), otherwise the
model thinks the answer into `reasoning_content`, `content` comes back empty and
the cross-configuration identity check passes vacuously — which is exactly how
this harness first "verified" an echo it never looked at.
prose 1k/512 none 17.7 | mtp n=3 32.4 | mtp n=7 26.7 | dflash=7 43.5 tok/s Results on Dankserver (frank-medium payload, 2026-09-16, temperature 0). prose
card 1 VRAM none 26.07 | mtp n=3 29.35 | mtp n=7 29.65 | dflash 29.42 GB 1k/512 decode: none 17.7 | mtp n=3 32.4 | mtp n=7 26.7 | dflash=7 43.5 tok/s.
`draft-mtp,draft-dflash` fails at load (a chain has ONE draft context and MTP
...and `draft-mtp,draft-dflash` fails at load: a chain has ONE draft context and builds against whatever model that context holds); ngram-mod has no such
MTP builds against whatever model that context holds. ngram-mod has no such constraint — it is draftless and joins the chain unconditionally.
constraint — it is draftless and is added to the chain unconditionally.
""" """
import argparse import argparse
import json import json
@@ -51,7 +54,8 @@ FILLER = ("The quick brown fox jumps over the lazy dog near the river bank. "
TOKENS_PER_REP = 28 TOKENS_PER_REP = 28
UNIT = "frank-medium.service" UNIT = "frank-medium.service"
MODEL = "frank-medium" MODEL = "frank-medium"
SPEC_GREP = "adding speculative implementation|- n_max=|- block_size=|creating MTP|ngram_mod with" SPEC_GREP = ("adding speculative implementation|- n_max=|- block_size=|creating MTP|"
"draft acceptance")
DEFAULT_CONFIGS = [ DEFAULT_CONFIGS = [
("none", {"SPEC_TYPE": "none"}), ("none", {"SPEC_TYPE": "none"}),
@@ -73,10 +77,14 @@ def echo_passage(lines=90):
for i in range(lines)) for i in range(lines))
ECHO_INSTRUCTION = ("\n\nRepeat the passage above verbatim, every line, exactly as written, " ECHO = (echo_passage() + "\n\nRepeat the passage above verbatim, every line, exactly as "
"with no commentary before or after it.") "written, with no commentary before or after it.")
PROSE_INSTRUCTION = "\n\nReply with a single short sentence." STRUCTURED = ("Output exactly 25 JSON records, one per line, no prose and no code fence. "
IDENTITY_PROMPT = "List the first five prime numbers, comma separated, then stop." 'Each line must look like {"id": N, "state": "active", "region": "us-west-2"} '
"with N running from 1 to 25.")
PROSE = FILLER * (1000 // TOKENS_PER_REP) + "\n\nReply with a single short sentence."
IDENTITY = "List the first five prime numbers, comma separated, then stop."
NO_THINK = {"chat_template_kwargs": {"enable_thinking": False}}
def log(msg, logfile): def log(msg, logfile):
@@ -159,11 +167,15 @@ def stop_unit():
time.sleep(1) time.sleep(1)
def run_shape(base, prompt, max_tokens, label): def run_shape(base, prompt, max_tokens, label, extra=None):
t = post(base, {"model": MODEL, body = {"model": MODEL, "messages": [{"role": "user", "content": prompt}],
"messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, "temperature": 0.0}
"max_tokens": max_tokens, "temperature": 0.0}) if extra:
tm = t["timings"] body.update(extra)
d = post(base, body)
tm = d["timings"]
msg = d["choices"][0]["message"]
content = (msg.get("content") or "").strip()
row = {"shape": label, row = {"shape": label,
"prompt_n": tm["prompt_n"], "prompt_n": tm["prompt_n"],
"prefill_tps": round(1000 * tm["prompt_n"] / tm["prompt_ms"], 1), "prefill_tps": round(1000 * tm["prompt_n"] / tm["prompt_ms"], 1),
@@ -171,18 +183,26 @@ def run_shape(base, prompt, max_tokens, label):
"decode_tps": round(tm["predicted_per_second"], 2), "decode_tps": round(tm["predicted_per_second"], 2),
"draft_n": tm.get("draft_n", 0), "draft_n": tm.get("draft_n", 0),
"accepted": tm.get("draft_n_accepted", 0), "accepted": tm.get("draft_n_accepted", 0),
"answer": (t["choices"][0]["message"].get("content") or "").strip()} "answer": content,
"reasoning_len": len(msg.get("reasoning_content") or "")}
if label.startswith("echo"):
# How much of the answer really is the passage: a config that "wins" by
# answering something else is not a win.
want = {l.strip() for l in echo_passage().splitlines()}
got = [l.strip() for l in content.splitlines() if l.strip()]
row["copy_ratio"] = round(sum(1 for l in got if l in want) / len(got), 3) if got else 0.0
return row return row
def measure(base): def measure(base):
run_shape(base, FILLER * (4000 // TOKENS_PER_REP) + PROSE_INSTRUCTION, 32, "warmup") run_shape(base, FILLER * (4000 // TOKENS_PER_REP) +
rows = [ "\n\nReply with a single short sentence.", 32, "warmup")
run_shape(base, FILLER * (1000 // TOKENS_PER_REP) + PROSE_INSTRUCTION, 512, "prose 1k/512"), return [
run_shape(base, echo_passage() + ECHO_INSTRUCTION, 400, "echo 1.5k/400"), run_shape(base, PROSE, 512, "prose 1k/512"),
run_shape(base, IDENTITY_PROMPT, 192, "identity"), run_shape(base, ECHO, 400, "echo 1.5k/400", NO_THINK),
run_shape(base, STRUCTURED, 400, "structured/400", NO_THINK),
run_shape(base, IDENTITY, 192, "identity"),
] ]
return rows
def main(): def main():
@@ -219,22 +239,26 @@ def main():
continue continue
rows = measure(base) rows = measure(base)
results[name] = {"vram_gb": vram(), "rows": rows, results[name] = {"vram_gb": vram(), "rows": rows,
"spec_log": sh("grep -E '" + SPEC_GREP + "' " + logf + " | head -6")} "spec_log": sh("grep -E '" + SPEC_GREP + "' " + logf + " | tail -4")}
for r in rows: for r in rows:
log(name + " | " + r["shape"] + " prefill " + str(r["prefill_tps"]) + log(name + " | " + r["shape"] + " prefill " + str(r["prefill_tps"]) +
" tok/s | decode " + str(r["decode_tps"]) + " tok/s | draft " + " tok/s | decode " + str(r["decode_tps"]) + " tok/s | draft " +
str(r["draft_n"]) + "/" + str(r["accepted"]) + str(r["draft_n"]) + "/" + str(r["accepted"]) +
(" (" + str(round(100.0 * r["accepted"] / r["draft_n"])) + " %)" if r["draft_n"] else ""), (" (" + str(round(100.0 * r["accepted"] / r["draft_n"])) + " %)" if r["draft_n"] else "") +
logfile) (" | copy " + str(r["copy_ratio"]) if "copy_ratio" in r else "") +
(" | EMPTY ANSWER" if not r["answer"] else ""), logfile)
log(name + " | vram " + str(results[name]["vram_gb"]), logfile) log(name + " | vram " + str(results[name]["vram_gb"]), logfile)
stop_unit() stop_unit()
with open(args.json, "w") as fh: with open(args.json, "w") as fh:
json.dump(results, fh, indent=2) json.dump(results, fh, indent=2)
for shape in ("prose 1k/512", "echo 1.5k/400", "identity"): for shape in ("prose 1k/512", "echo 1.5k/400", "structured/400", "identity"):
texts = {v["rows"][i]["answer"] for v in results.values() if "rows" in v texts = [r["answer"] for v in results.values() if "rows" in v
for i, r in enumerate(v["rows"]) if r["shape"] == shape} for r in v["rows"] if r["shape"] == shape]
if len(texts) > 1: if any(not t for t in texts):
log("WARNING: '" + shape + "' produced an empty answer in some configuration -- "
"its identity check proves nothing", logfile)
elif len(set(texts)) > 1:
log("WARNING: configurations disagree on greedy text for '" + shape + log("WARNING: configurations disagree on greedy text for '" + shape +
"' -- speculation is changing answers", logfile) "' -- speculation is changing answers", logfile)
log("restoring " + UNIT, logfile) log("restoring " + UNIT, logfile)