Skip to content

mtp: the draft route's DEFAULT tau=0.6 is measured — 78.8 % hit rate on a 1.83 tokens/pass chain - #1300

Merged
bong-water-water-bong merged 3 commits into
mainfrom
npu/mtp-tau06-row
Nov 7, 2026
Merged

bong-water-water-bong merged 3 commits into
mainfrom
npu/mtp-tau06-row

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

The last unmeasured row of #805 (plan P6): Options::tau defaults to 0.6 (npu/mtp/draft.h:162), so that is what a serve driving mtp::run() gets without asking — and every acceptance row published before this one was at tau = 0.

The window

8 prompts × 64 tokens, k = 4, tau = 0.6, --live --match, one prompt per npu-hold ticket (2026-11-06 16:42–18:06 AST, array exclusive, amdxdna errors 0 in every arm). Greedy text was byte-identical to plain decode on 8 of 8 prompts with 0 stream mismatches, and prompt 0 was measured twice (16:29 and 16:42, both 31/35) as the determinism control.

Measured — all chains, 277 chain iterations, 292 drafts offered

tau = 0 tau = 0.6 (default)
hit rate (accepted / offered) 46.69 % (338/724) 78.77 % (230/292)
depth 1 acceptance 75.14 % (136/181) 78.34 % (217/277)
depth 2 acceptance 47.51 % (86/181) 3.97 % (11/277)
depth 3 acceptance 34.81 % (63/181) 0.72 % (2/277)
depth 4 29.28 % (53/181) never offered
tokens per pass (live) 2.87 1.83
tokens per pass (offline, same positions) 2.82 2.05

The gate does what it was asked to do and still loses. The drafts it keeps are far better (78.8 % against 46.7 % hit rate; depth 1 is identical to the offline protocol at the same positions, +0.00 pp), but it almost never chains — ~1.05 drafts per chain against 4.0 — so the chain collapses from 2.87 to 1.83 tokens/pass, below the measured 3.2–4.1 break-even. At the default the draft route is a slowdown: 0.81× on the prompt-0 pair (plain 0.849 s/token, draft 1.049 s/token). The conditional per-depth comparison reads +27 and +47 pp at depths 2–3 on 13 and 2 live chains — sampling noise, which is why the all-chains view is published and depth 1 is the only comparable cell at this tau.

Contents

  • tests/fixtures/mtp_tau06/ — the eight tickets, extracted verbatim with the same grep the mtp_clause55 README documents (which re-extracts those three fixtures byte-identically), each carrying its own == 8 prompts x 64 tokens, k=4 tau=0.60, hidden=post-norm identity line.
  • bench/npu-leaderboard/27b-mtp-tau06-*.json — six records, generated from the pool tool's own output rather than typed (depth 1/2/3 all-chains acceptance, hit rate, live and offline tokens per pass).
  • tests/mtp_depth_pool_test.py — the records guard gains the tau=0.6 window, so these six rows are re-derived from the fixtures on every CI run.
  • docs/npu.md — the table above, the break-even consequence, and why the 2 pp per-depth clause is a depth-1 clause at this tau.

Verified

tools/mtp_depth_pool.py tests/fixtures/mtp_tau06/*.txt
  pooled 14 scored cells = 8 prompts x 3 depths (…, k=4, tau=0.6, vocab 248320, max_batch_rows 16, hidden post-final-norm, 8 ticket(s))
  depth 1: 217/277 = 78.34 %  (offline same positions 78.34 %, +0.00 pp)
  depth 2: 11/13 = 84.62 %    depth 3: 2/2 = 100.00 %    depth 4: unmeasured
  all chains (277 iterations, drafts offered 292, accepted 230): 1: 78.34 %  2: 3.97 %  3: 0.72 %
  hit rate 78.77 % (230/292); tokens per pass live 1.83, offline 2.05
tests/mtp_depth_pool_test.py      -> all checks passed (13 MTP records, both windows, 0 uncovered)
red arm (hit rate 78.78)          -> "record says 78.78 %, the fixtures derive 78.77", 1 CHECK(S) FAILED; restored green
tools/npu_leaderboard.py          -> 27 record(s) valid; a second run is byte-identical
ctest -R 'mtp_draft|mtp_depth_pool|npu_leaderboard' -> 3/3 passed
tools/copyright.py --check        -> rc 0

refs #805 — the protocol's measurable gate rows are now all published at both taus; what remains on that issue is leaf fill (needs per-row positions/attention in the forward, i.e. a kernel-side change on top of P4) and the Hexagon target rows (P5, #804).

…on a 1.83 tokens/pass chain

`Options::tau` defaults to 0.6 (npu/mtp/draft.h:162), so that is what a serve driving `mtp::run()` gets without asking,
and every published acceptance row was at `tau = 0`. This lands the missing window: 8 prompts x 64 tokens, k=4,
tau=0.6, `--live --match`, one prompt per npu-hold ticket (16:42-18:06 AST, array exclusive, amdxdna errors 0 in every
arm), greedy text byte-identical to plain decode on 8 of 8 prompts with 0 stream mismatches, and prompt 0 measured
twice (31/35 both times) as the determinism control.

Measured, all-chains view, 277 chains / 292 drafts offered:
  hit rate            78.77 % (230/292)      vs 46.69 % at tau=0
  depth 1 acceptance  78.34 % (217/277)      vs 75.14 %
  depth 2 acceptance   3.97 % (11/277)       vs 47.51 %
  depth 3 acceptance   0.72 % (2/277)        vs 34.81 %
  depth 4             never offered          vs 29.28 %
  tokens per pass      1.83 (live) / 2.05 (offline, same positions)   vs 2.87

So the gate does what it was asked and still loses: the drafts it keeps are far better (78.8 % hit rate, depth 1
identical to the offline protocol at +0.00 pp), but it almost never chains -- ~1.05 drafts per chain against 4.0 -- so
the chain collapses from 2.87 to 1.83 tokens/pass, below the measured 3.2-4.1 break-even. At the default the draft
route is a slowdown: 0.81x on the prompt-0 pair (plain 0.849 s/token, draft 1.049 s/token). The conditional per-depth
comparison reads +27 and +47 pp at depths 2-3 on 13 and 2 live chains, which is why the all-chains view is the one
published and depth 1 is the only comparable cell.

Contents: eight fixtures (tests/fixtures/mtp_tau06, extracted verbatim with the same grep the mtp_clause55 README
documents), six leaderboard records generated from the pool tool's own output rather than typed, the guard extended
with the tau=0.6 window, and docs/npu.md carrying the table above plus the reason the per-depth clause is a depth-1
clause at this tau.

Verified: `tools/mtp_depth_pool.py` over the fixtures reproduces the run to the count (277 chains, 292/230,
78.34/3.97/0.72, 78.77 %, 1.83/2.05); `tests/mtp_depth_pool_test.py` -> all checks passed, including `tau=0.6 (the
default)... all 6 records equal what its tickets derive: []`; red arm: hit rate set to 78.78 -> `record says 78.78 %,
the fixtures derive 78.77`, 1 CHECK(S) FAILED, restored green; `npu_leaderboard.py --check` 27 valid and regeneration
byte-identical on a second run; `ctest -R 'mtp_draft|mtp_depth_pool|npu_leaderboard'` 3/3; copyright rc 0.

refs #805.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#1300 ready for review at b0224e5

…so the next window does not need a code edit

The tau=0.6 window's eight ticket fixtures reddened CI's `Copyright notice on every file` step:

    no comment style known (add one to LINE_STYLES/BLOCK_STYLES or EXEMPT): tests/fixtures/mtp_tau06/p0.txt
    ... p1..p7 ...
    ##[error]Process completed with exit code 1.

Cause: EXEMPT carried the clause-55 window's own path (`tests/fixtures/mtp_clause55/*.txt`, engine#1146) rather than
the kind of file it is. An acceptance ticket is data a parser reads byte-verbatim, so a notice comment would make the
fixture no longer the log it claims to be -- true of every such window, not of one directory. The exemption is now
`tests/fixtures/*/*.txt`, and each fixture directory's README still carries the notice (it is `.md` and unaffected).

Why this got past my own pre-push check, measured rather than guessed: `tools/copyright.py --check` enumerates
TRACKED files, so it reports success on files that are not yet added.

    untracked probe file, no notice:              python3 tools/copyright.py --check -> rc 0
    the same file after `git add -A`:             python3 tools/copyright.py --check -> rc 1
                                                  "missing copyright notice: tools/zzz_untracked_probe.py"

That is the "a check that passes on data it cannot see" family this repo keeps closing (engine#1146, #1184, #1213).
The pre-flight for this lane is now: `git add -A` FIRST, then `copyright.py --check`.

Verified after the change, with the tree fully staged (the way CI sees it): `tools/copyright.py --check` rc 0;
`python3 tests/mtp_depth_pool_test.py` -> all checks passed; `ctest -R 'mtp_draft|mtp_depth_pool|npu_leaderboard'`
3/3; `tools/npu_leaderboard.py --check` 27 record(s) valid.

refs #805.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#1300 ready for review at 6fc93fa

build reddened on Copyright notice on every file, and it was a real defect of mine, not flake: EXEMPT in tools/copyright.py carried the clause-55 window's own path (tests/fixtures/mtp_clause55/*.txt, engine#1146) instead of the kind of file it is, so my eight new ticket fixtures had no exemption. Generalised to tests/fixtures/*/*.txt — an acceptance ticket is data a parser reads byte-verbatim in any window, and each fixture directory's README still carries the notice.

Worth recording how it got past my own pre-push check, measured rather than guessed: copyright.py --check enumerates tracked files, so it passes on files that are not yet added —

untracked probe file, no notice:   python3 tools/copyright.py --check -> rc 0
same file after `git add -A`:      python3 tools/copyright.py --check -> rc 1  "missing copyright notice: …"

i.e. another check that reports success on data it cannot see. New pre-flight for this lane: git add -A first, then copyright.py --check. Verified with the tree fully staged: rc 0; mtp_depth_pool tests all checks passed; ctest 3/3; npu_leaderboard --check 27 valid.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 6fc93fa0.

Every number in the table re-derives from the counts you cite, which is the part a reader would otherwise take on trust:

tau=0.6   230/292 = 78.77 %   217/277 = 78.34 %   11/277 = 3.97 %   2/277 = 0.72 %
tau=0     338/724 = 46.69 %   136/181 = 75.14 %   86/181 = 47.51 %   63/181 = 34.81 %   53/181 = 29.28 %
derived   292/277 = 1.054 drafts per chain   0.849/1.049 = 0.809x   (= the 0.81x you publish)

The guards are real, and they re-derive your records rather than trusting them:

tests/mtp_depth_pool_test.py     all checks passed   (the tau=0.6 window is covered, and "the record comparison
                                                      bites" fires: 27b-mtp-tau06-depth1 +0.01 is reported)
npu_leaderboard.py --check       27 record(s) valid   (21 + your six)
regenerate vs committed page     BYTE-IDENTICAL
copyright --check                rc 0                  (your last commit)

The fixtures carry their own provenance — == ticket 'tau06 p0 try1' 20261106-164254 hold=9min … --tau 0.6 — so the
window claim (16:42–18:06 AST) is checkable against the evidence rather than asserted, and each ticket states the identity
line the pool tool reads.

What I want to say about the finding itself: publishing a row that says the shipped default is a slowdown is the
right kind of PR, and the doc says it without softening — 1.83 tokens/pass against the measured 3.2–4.1 break-even, the chain
collapsing because ~1.05 drafts are offered per chain instead of 4.0, and the depth-2/3 "+27/+47 pp" called out as 13-and-2
live chains, i.e. noise. That is the difference between a benchmark and an advertisement.

One thing to surface rather than bury, since npu/ has no diff here: this PR measures the tau=0.6 default, it does not
change it, so the engine currently ships a default that this evidence says is 0.81x on the prompt-0 pair. Whether the default
becomes tau=0 (or a lower gate) is an owner decision, not a lane one — TISSUES-ALL rule 6 shape, and the natural next
line on #805. refs #805 is the right reference for a roadmap issue, and the "what remains" note on the issue should say
this row is now measured and the default is the open question.

Device rows stay yours: the window is seven npu-hold tickets with amdxdna errors 0, which I cannot re-run tonight (the
lock is still pinned by another lane's idle host-GEMM serve), and the fixtures are what make that acceptable — the CI guard
re-derives every record from them on each run.

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

805 - Partially compliant

Compliant requirements:

  • The PR implements the default tau=0.6 setting and provides measured performance data
  • The chain behavior matches the specification with draft probability >= 0.6
  • The verification and spare row logic is implemented as described
  • GDN commits are deferred to the next pass
  • The performance metrics are measured on the specified hardware and conditions
  • The greedy text is byte-identical to plain decode

Non-compliant requirements:

  • The PR does not include the full implementation of the pi-bonsai and pi-hexagon lanes as specified

Requires further human verification:

  • The hardware-specific performance measurements need confirmation from the actual hardware

804 - Partially compliant

Compliant requirements:

  • The PR includes the necessary test fixtures and documentation for the new tau=0.6 window
  • The performance data is measured on the specified hardware

Non-compliant requirements:

  • The full implementation of the pi-hexagon lane and the standalone harness is not included in this PR

Requires further human verification:

  • The kernel bit-exactness against the oracle in QEMU needs to be verified
  • The AI Hub profile job IDs and effective GB/s need to be confirmed
  • The cost model implementation needs to be verified
⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Test Configuration

The test configuration for the new tau=0.6 window is added, but it's important to verify that all the test parameters match the requirements of the ticket. Specifically, the window settings (8 prompts x 64 tokens, k=4, tau=0.6) should be validated to ensure they are correctly implemented and tested.

WINDOWS = [
    {
        "name": "tau=0.6 (the default), k=4, 8 prompts x 64 tokens",
        "dir": "mtp_tau06",
        "files": ("p0.txt", "p1.txt", "p2.txt", "p3.txt", "p4.txt", "p5.txt", "p6.txt", "p7.txt"),
        "records": {
            "27b-mtp-tau06-depth1-20261106": lambda e: e["depth_rate"][1],
            "27b-mtp-tau06-depth2-20261106": lambda e: e["depth_rate"][2],
            "27b-mtp-tau06-depth3-20261106": lambda e: e["depth_rate"][3],
            "27b-mtp-tau06-hit-rate-20261106": lambda e: e["hit"],
            "27b-mtp-tau06-tokens-per-pass-20261106": lambda e: e["tpp_live"],
            "27b-mtp-tau06-offline-tokens-per-pass-20261106": lambda e: e["tpp_offline"],
        },
    },
Data Consistency

The JSON files for the benchmark data include detailed metrics, but it's crucial to ensure that the data is consistent with the actual measurements. The hit rate of 78.77% and tokens per pass of 1.83 should be validated against the hardware measurements to confirm accuracy.

{
  "host": "strixhalo",
  "model": "Qwen3.8-27B-NPU2",
  "route": "npu",
  "quant": "Q4NX pb16i (int8 GEMM) + MTP head pb16i-exact",
  "settings": {
    "device": "accel0",
    "model_dir": "bonsai2-27b-pb16i",
    "head": "pb16i-exact",
    "k": 4,
    "tau": 0.6,
    "prompts": 8,
    "tokens_per_prompt": 64,
    "batch_rows": 16,
    "vocab": 248320,
    "hidden": "post-final-norm"
  },
  "instrument": "tools/mtp_depth_pool.py over the eight in-tree tickets tests/fixtures/mtp_tau06/p*.txt; counts summed over the window's 277 chain iterations",
  "repeats": 1,
  "timestamp": "2026-11-06T22:06:22Z",
  "source": "tests/fixtures/mtp_tau06/p0..p7.txt, verbatim extracts of npu-hold tickets tau06 p0..p7 (20261106-164254/165612/170407/172013/172718/173228/174605/175540), one prompt per ticket, tau=0.6",
  "id": "27b-mtp-tau06-hit-rate-20261106",
  "metric": {
    "name": "draft hit rate (live loop, k=4, tau=0.6)",
    "value": 78.77,
    "unit": "%"
  },
  "notes": "8 prompts x 64 tokens, k=4, tau=0.6 (the draft route's DEFAULT), one prompt per npu-hold ticket; greedy text byte-identical to plain decode on 8 of 8 prompts, 0 stream mismatches, 0 amdxdna errors during any arm. The window is the union of eight single-prompt tickets, not eight repeats: each prompt was measured once, and prompt 0 was measured twice (16:29 and 16:42) with identical counts 31/35, the only repeat in the set. 230 accepted of 292 drafts. The gate buys confidence per draft: 78.77 % here against 46.69 % at tau=0, on 1.05 drafts per chain against 4.0."
}
Documentation Accuracy

The documentation in docs/npu.md provides detailed information about the tau=0.6 setting and its performance. It's important to verify that the documentation accurately reflects the implementation and that the performance claims are substantiated by the measurements.

  721-1187 ms; the `[bl]` core term 10.2 ms against 162-307 ms).
* **`tau` redistributes cost, it does not move it.** `tau = 0.6` (the head's published setting) stops the chain
  after the first draft and trades base passes for lm-head calls almost exactly one-for-one: 0.598-0.623 s/token
  across arms that differ 2× in chain and 4× in head calls. Measured over 8 prompts × 64 tokens it leaves **depth 1
  as the only depth with a usable denominator** (277 chains), depth 2 with 13 chains that reached it, depth 3 with 2,
  and depth 4 never offered at all — so a per-depth comparison at this `tau` is a depth-1 comparison.

**The default `tau = 0.6` is now measured, and it is not the regime to serve.** `Options::tau` defaults to 0.6, so
this is what a serve driving `mtp::run()` gets without asking, and until now no acceptance row existed at it
(every row above is `tau = 0`). Over the same 8 prompts × 64 tokens, `k = 4`, one prompt per `npu-hold` ticket, greedy
text byte-identical to plain decode on 8 of 8 prompts with 0 stream mismatches:

| | `tau = 0` | `tau = 0.6` (default) |
|---|---|---|
| hit rate (accepted / offered) | 46.69 % (338/724) | **78.77 % (230/292)** |
| depth 1 acceptance, all chains | 75.14 % (136/181) | **78.34 % (217/277)** |
| depth 2 acceptance, all chains | 47.51 % (86/181) | **3.97 % (11/277)** |
| depth 3 acceptance, all chains | 34.81 % (63/181) | **0.72 % (2/277)** |
| depth 4 | 29.28 % (53/181) | **never offered** |
| tokens per pass (live) | 2.87 | **1.83** |

The gate does exactly what it was asked to do — **the drafts it keeps are far better** (78.8 % against 46.7 % hit
rate, and depth 1 is identical to the offline protocol at the same positions, +0.00 pp) — and it does it by **almost
never chaining**: 292 drafts over 277 chains, i.e. ~1.05 per chain against 4.0, so the chain collapses from 2.87 to
**1.83 tokens/pass**. That is below the measured 3.2-4.1 break-even, so at the default the draft route is a
**slowdown**: 0.81× on the prompt-0 pair ticket (plain 0.849 s/token, draft 1.049 s/token). The conditional
per-depth comparison at this `tau` reads +27 and +47 pp at depths 2-3, on 13 and 2 live chains — sampling noise, which
is why the table above is the all-chains view and why depth 1 is the only cell here worth comparing.

**`tau` is part of the window.** Because the gate stops the chain, a `tau = 0.6` ticket offers one to three depths where
a `tau = 0` one offers four, so the two must never be pooled into one table — and `mtp_acceptance --live` used to print
its window nowhere (`--offline` always stated it; the live path only echoed it in the harness' argv). `--live` now
prints `== N prompts x M tokens, k=K tau=T, hidden=H` at **init** (so a ticket killed at its hold still states its
window), and `tools/mtp_depth_pool.py` reads it, reports the gate in the window header, names a pool that mixes two
gates (`MIXED chain gate (tau)`), and says `tau unstated` for a log that predates the line rather than assuming 0.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at f091af3ef408847d87f455b765e618b14531f478. The tau=0.6 measurement for #805 checks out end to end, from the raw ticket logs to the fixtures, the records and the guard. One nit, on the width of the copyright exemption.

Provenance, re-derived here

  • The fixtures are the logs. Re-running the README's grep -E "$pat" over the eight ticket logs in ~/wt/mtp-window/ (ticket-tau06-p{0..7}-try1-20261106-{164254,165612,170407,172013,172718,173228,174605,175540}.log) reproduces tests/fixtures/mtp_tau06/p0..p7.txt byte-identically, 8/8 (cmp).
  • Text identity: all 8 logs say TEXT IDENTICAL, 64 tokens … 0 mismatch. The determinism control holds: p0 at 16:29 and 16:42 both read accepted 31/35, passes/token 1.016, with depth 1 87.50 % (28/32) both times.
  • "amdxdna errors 0 in every arm" holds. Each log's header carries the boot-cumulative counter, which steps 105 → 114 between p2 (17:04) and p3 (17:20). journalctl -k puts those 9 at 17:12:43-44, after p2's run ended and before p3's ticket. All 8 logs report [array] amdxdna ERROR lines during the run: 0 (during the queue wait, NOT ours: 0).

The numbers

  • tools/mtp_depth_pool.py tests/fixtures/mtp_tau06/*.txt reproduces the body exactly: 277 iterations, 292 offered, 230 accepted. Depth 1 is 78.34 % (217/277), depth 2 3.97 % (11/277), depth 3 0.72 % (2/277), depth 4 never offered. Hit rate 78.77 %, tokens/pass live 1.83 (1 + 230/277) and offline 2.05 (1 + 291/277).
  • They are internally consistent: 217+11+2 = 230, and 277+13+2 = 292. The 0.81× slowdown is 0.849/1.049.
  • tests/mtp_depth_pool_test.py → all checks passed. Red arm, mine: the hit-rate record edited to 78.78 → 1 CHECK(S) FAILED; restored → green. So the six records are tied to the fixtures, not typed in.
  • tools/npu_leaderboard.py --check → 27 record(s) valid. Regenerating the docs leaves the tree clean. copyright.py --check rc 0.

Nit, not blocking: EXEMPT widens from tests/fixtures/mtp_clause55/*.txt to tests/fixtures/*/*.txt, and fnmatch's * also crosses /. Today it newly exempts exactly the 8 tau06 tickets (fflm/ has no .txt). But any future hand-written .txt under tests/fixtures/ will skip the notice check silently. A name convention for extracted tickets would keep the "by kind" intent without the blanket glob, for example tests/fixtures/mtp_*/p*.txt, or a *.ticket.txt suffix.

refs #805 and not Fixes is right: leaf fill and the P5 Hexagon rows remain, as the body says.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant