Skip to content

Benchmark: keep each attempt's draft counts beside the receipt - #1045

Open
ThinkOffApp wants to merge 5 commits into
ashhart:mainfrom
ThinkOffApp:benchmark-drafter-counts
Open

ThinkOffApp wants to merge 5 commits into
ashhart:mainfrom
ThinkOffApp:benchmark-drafter-counts

Conversation

@ThinkOffApp

Copy link
Copy Markdown
Contributor

For #1001 (1.1.1). Branch ThinkOffApp:benchmark-drafter-counts, on main at 76aafcf2. One commit. It does not depend on the other two #1001 PRs, and all three merge cleanly together.

What and why

Issue #1001 points out that equal timings do not show that DFlash2 ran: a run has to show that its drafter ran. The server already reports this. Every reply ends with a speculative object (rounds, drafted, accepted, from the engine's Stats). These are the same numbers as the server's done ... rounds=14 accepted=10/13 log line. The pinned gfx1151 receipt (docs/receipts/gfx1151-qwen-833bbfcb) counted drafted and accepted tokens from that same telemetry. But the benchmark client dropped it, and a managed run's server log is deleted as soon as the server starts. So nothing kept the counts.

The board's receipt has exactly the community-v1 fields. protocol.js rejects any extra key in a sample, and so does the client's own save check. The counts therefore cannot go in the receipt without a board change. This PR keeps them in a separate local file:

  • The client reads speculative from each attempt's last chunk. It keeps the counts only if all three are non-negative integers and accepted <= drafted.
  • After the receipt is saved, RUN_ID.drafts.json is written next to it (for --output FILE.json, FILE.drafts.json). It is written the same way as the receipt: mode 0600, to a temp file that is then renamed. It holds the run ID, the receipt's name, drafter_repo_id, drafter_passed (true or false for a managed run, null for an attached one), totals, and one row per receipt sample: fixture, temperature, repeat, status, completion tokens, rounds, drafted, accepted.
  • The run prints Draft counts: A of D drafted tokens accepted over R rounds (k of n attempts reported them); saved: PATH, or the server reported none. When a drafter was passed and the server drafted nothing, it adds: A drafter was passed, but the server drafted no tokens: this run does not show that the drafter ran.
  • The receipt does not change, so its bytes, its checks and what gets uploaded are the same as before. The counts are never uploaded.

Open question for the maintainer: should the board get a field for these counts in a later receipt schema? And should a run with a passed drafter and zero drafted tokens fail (for example with exit 2) rather than only print the note? We left the exit codes unchanged.

Tests

Host tests on an M-series Mac, CPU only, Zig 0.17.0, zig build test -Dcpu=apple_m1 --summary all:

Tree Build steps Server unit tests
main 76aafcf2 292/292; 4305 of 4321 passed, 16 skipped 428
this branch 1b4b2d7b 292/292, all passed 432 (+4)

Full count on this branch: 4313 of 4329, with the same 16 skips. The server binary runs twice in zig build test. All three PRs for #1001 also build and pass together on one merged tree.

  • benchmark_sse_test: a last chunk with rounds 14, drafted 13, accepted 10 reaches the sample. No speculative, accepted past drafted, a missing field, a negative or fractional count, or a non-object all give no counts, and the sample still passes.

  • benchmark_drafts (two tests): the file name next to the receipt; totals over only the attempts that reported counts; the note shows only when a passed drafter drafted nothing; the JSON rows, including null counts for a failed attempt.

  • benchmark_run_test."the draft counts beside the receipt show...": managed runs against a fake server, with a cached DFlash2 for a 27B-shaped config:

    • drafted (3 rounds, 4 drafted, 3 accepted per reply): 6 of 8 ... over 6 rounds, drafter_passed: true, no note;
    • drafter passed, 0 drafted: the note is printed;
    • --drafter none, 0 drafted: drafter_passed: false, no note;
    • no speculative, or accepted past drafted: reported none.

    In every case the counts file is mode 0600, and the saved receipt still loads under the board's rules with no extra sample keys.

Negative controls. With the SSE read removed, the SSE test and the run test fail. With the zero-drafted note removed, the drafts test and the run test fail. With them back, all tests pass.

GPU receipts

Measured on 18 Oct 2026 on a Bosgame M5 (AMD Ryzen AI MAX+ 395, Radeon 8060S, gfx1151). The source was main 76aafcf2 with the three #1001 branches merged (5906a3f7, 920d1348, 1b4b2d7b; merge 7b84f909), built with zig build native-hip -Dhip-arch=gfx1151 ... -Doptimize=ReleaseFast, Zig 0.17.0. tensorfold-native sha256 e279e29a54b56dc81ff70985f8c4cffaa0d9aff4a7456a234eb7247667492247. Checkpoints were pulled into the Hugging Face cache at pinned revisions: TensorFold/Qwen3.8-27B-MLX-4bit@70ae7fac... and TensorFold/Qwen3.6-35B-A3B-MLX-4bit-MTP@f84b054c.... z-lab/Qwen3.8-27B-DFlash2 is cached at 50307d4c.... The machine's own LLM service was stopped for the run.

Every run is community-v1 at defaults: 20 measured attempts, all ok, and every attempt reported counts (reported == attempts == 20).

Run Drafter passed Rounds Drafted Accepted Per attempt
35B-MTP, managed false (MTP head, no external drafter) 2495 4490 2615 (58%) drafted 117 to 293 and accepted 64 to 173 in every attempt
35B-MTP, managed, --serial false 5100 0 0 0 drafted in every attempt, 255 rounds each (one token per round)
35B-MTP, attached to serve null (attached) 2441 4626 2668 above 0 in every attempt
27B, managed, --drafter auto false (HIP refuses DFlash2) 5030 262 70 (27%) 0 in 6 attempts, 15 to 48 in the rest (the 27B's MTP head)
  • The counter is real on this GPU. The same checkpoint shows drafted above 0 in every attempt with drafts on, and exactly 0 with --serial. So the counts follow what the engine did, and the timings are not needed as evidence.
  • The counts match the server's own log. In the attached run, the server's log has 24 done ... rounds=R accepted=A/D lines: 4 warm-ups and 20 measured attempts. The 20 rows of 35b-attached.drafts.json are exactly those 20 lines, in order (for example 106 150/246, 89 166/249, ..., 189 67/145).
  • The receipts are unchanged. All four receipts have only the board's fields. As a schema check only, test copies with the backend changed to cuda pass the board's protocol.js; the real receipts stay rocm and are not publishable until the board accepts rocm. The counts files are mode 0600 next to them.
  • The 27B drafts on HIP. The 27B row shows its MTP head drafting with low acceptance. Without these counts, the earlier "with and without DFlash2" comparison could not show that.

Full local receipts: the four receipts, the four *.drafts.json files, run logs, the 35B serve log and the manifest, with SHA-256 sums (for example 35b.drafts.json 9bb8baa0..., 35b-serial.drafts.json 92fe121f...). They can be attached on request.

Equal timings do not show that a drafter ran. The server already ends each
reply with its speculative counters (verify rounds, drafted and accepted
tokens), the same numbers as its "rounds=14 accepted=10/13" log line, but the
client dropped them and a managed run's server log is unlinked as it starts.
The client now reads them from the finishing chunk and writes RUN_ID.drafts.json
beside the receipt: per attempt and in total, with the drafter's repo ID and
whether the run passed one. The board's receipt holds exactly its community-v1
fields, so the counts stay local and the receipt is unchanged. The run prints
the totals, and says so when a drafter was passed and the server drafted no
tokens.
@ashhart

ashhart commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Thanks @ThinkOffApp; we've reviewed this draft early. Keeping the counters beside the receipt preserves its schema and separates MTP activity from passing an external drafter. The counters accept full unsigned 64-bit values, but the totals add them unchecked; two valid large counters can overflow and abort an otherwise successful run. Please bound or check the sums and add that regression. Please attach the described raw receipts and controls. We'll carry it onto the 1.1.2 line ourselves, keeping the sample's finish reason and API route beside the new counters, after that fix and our gates.

Posted by Ash's AI Assistant Synthena

…rflow panic

The server's counters are full u64 values and the totals added them unchecked, so two valid large counts
overflowed: a panic in a safe build and illegal behaviour in a fast one, after the receipt was saved. The sums
are now checked; when one would pass 64 bits the totals are null in the counts file, as an attempt without counts
is, each attempt keeps its own counts, and the run says so instead of printing totals. The receipt is unchanged.
A run test drives two attempts whose counts sit near 2^64 through the fake server and expects a completed run,
the null totals and the board's receipt; a unit test covers a sum of exactly 2^64 - 1 and one past it in either
the rounds or the drafted tokens. No ratio is computed from the counts, so there is no division to guard.
…r logs, and the host controls

docs/receipts/drafter-counts holds sanitized copies of the 18 October runs on a Bosgame M5 (gfx1151): five
receipts with their .drafts.json files and console logs, the two attached runs' server logs and manifests, and the
run script's log with both binaries' SHA-256. match.py checks every attached sample's rounds, drafted and accepted
tokens, completion count and token hash against the server's own "done" line for that request (the README names
the lines; the route and finish reason are printed beside the counts from the log), every run's totals against its
samples and its printed line, and refuses two controls: one count off by one and the log read one request out of
step. host-controls has the overflow fix's host test runs, the negative control that panics with the fix reverted,
and the lean and fmt checks. Local paths became ~ and trailing spaces were dropped; the receipts and counts files
are byte for byte the raw ones, and raw-SHA256SUMS keeps the raw hashes.
@ThinkOffApp

Copy link
Copy Markdown
Contributor Author

Thanks for the early review @ashhart. Two new commits on this branch address both points.

Overflow: the totals now use checked sums. If any sum would pass 64 bits, the totals in the counts file are null, as for an attempt without counts. Each attempt keeps its own counts, and the run prints "Draft counts: no totals, as a sum passes 64 bits (k of n attempts reported counts)" instead of aborting. The run still completes and the receipt is unchanged. No ratio is computed from the counts, so there is no division to guard. The changes are in zig/src/server/benchmark_drafts.zig and zig/src/server/benchmark_run_test.zig, plus one sentence in docs/community-benchmarks.md.

Regressions: a unit test covers a sum of exactly 2^64 - 1, which still gives a total, and one past it in the rounds, in the drafted tokens, or in all three. A new case in the run test sends two attempts with valid counters near 2^64 through the fake server and expects a completed run, null totals, each attempt's own counts, and a receipt with only the board's fields. With plain additions instead of the checked sums, both tests abort with "panic: integer overflow".

Host tests (Apple silicon, CPU only, Zig 0.17.0, zig build test -Dcpu=apple_m1, uncached): 292 of 292 steps, 4315 of 4331 tests passed, 16 skipped. The lean check finds no new problems against main, and zig fmt --check passes.

Receipts and controls are in docs/receipts/drafter-counts/, and its README describes each file: the five gfx1151 runs from 18 October (receipts, counts files, console logs, and for the two attached runs the server logs and manifests), the build-and-run log with both binary hashes, and match.py, which checks every attached attempt's counts against the server's own log line for that request. host-controls/ holds the test runs, the negative control, and the lean and fmt output. Local paths were shortened to ~; SHA256SUMS covers every file.

One correction to the description: the GPU binary for these runs merged only hip-refuses-drafter (920d134) and this branch (1b4b2d7) onto main 76aafcf. benchmark-rocm-host-identity was added only for the later out-r5b run.

… branches merged

The run offered on ashhart#1001, made after the fixes: main 76aafcf with ashhart#1043 (c949cc9),
ashhart#1044 (f2c78ee) and this branch (134a5f4) merged on the box and built for gfx1151.
R1 to R5 repeat the 18 October set; R6 repeats R3 with HIP_VISIBLE_DEVICES=0.

Every receipt names the CPU, the one GPU and the checkpoint; every run's counts match its
printed line, and R5's 20 samples match the server's own log lines, while a count off by
one and a log read one request out of step are refused. Both --drafter calls are refused
before any weights load. The board's validator refuses each receipt as rocm and accepts
it as cuda, as on 18 October.
@ThinkOffApp
ThinkOffApp force-pushed the benchmark-drafter-counts branch from 61e1fb1 to 390b17e Compare October 19, 2026 19:45
@ThinkOffApp

Copy link
Copy Markdown
Contributor Author

Commits rewritten only to drop tool trailer lines (CONTRIBUTING); every tree is unchanged. Old to new: 1b4b2d7 d1a0817, d2dfb60 70b51ac, 616d306 a32a090, 134a5f4 ca68d81, 61e1fb1 390b17e. The receipts name the old hashes they were built from.

@ThinkOffApp
ThinkOffApp marked this pull request as ready for review October 19, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants