Skip to content

bench: the Qwen3-1.7B same-pack pair against FastFlowLM's runtime becomes two leaderboard records - #2190

Merged
bong-water-water-bong merged 13 commits into
mainfrom
cl-flm/1787-pair-records
Nov 12, 2026
Merged

bong-water-water-bong merged 13 commits into
mainfrom
cl-flm/1787-pair-records

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #1787 (items 2 and 3 of its definition of done) and #1786.

This body describes head 4fb86c9. It was rewritten after the reviews at f381a01, which found that it described two of the seven records it adds and gave a stale count, and that one sentence of the merged docs/arch-gaps.md section was false.

What this adds (10 files)

Seven leaderboard records in bench/npu-leaderboard/. The page docs/npu-leaderboard.md is regenerated from them by tools/npu_leaderboard.py and goes from 32 to 39 records.

record what measured
qwen3-17b-fused-decode-flm-pair-20261111 Qwen3-1.7B-NPU2 decode on the fused route, 39.25 tok/s (median of six, 39.20–39.32) one A B B A ticket against FastFlowLM 1.0.12's runtime on the same model.q4nx. Theirs is 45.57 tok/s, in notes. Shipped c32 build
qwen3-17b-fused-first-token-flm-pair-20261111 the same pair, first token 0.568 s (theirs 0.591 s) same ticket
lfm25-12b-perop-decode-20261111 / -first-token- LFM2.5-1.2B-NPU2 on the per-op route, 4.20 / 4.19 tok/s; first token 1.094 / 0.763 s one ticket at main 1131acf0 (#2170). Three identical Paris answers
lfm2-12b-perop-decode-20261111 / -first-token- LFM2-1.2B-NPU2, 4.25 / 4.30 tok/s; 0.747 / 0.764 s same ticket
lfm25-12b-thinking-perop-decode-20261111 LFM2.5-1.2B-Thinking-NPU2, 4.30 tok/s (4.30 / 4.25 / 4.37) three identical replies. The reply reaches Paris after 287 reasoning tokens and does not stop. FastFlowLM's runtime at the same budget answered no content, so this is not a parity row, and the record says so

Each record names the container's sha256, the kernel shapes, the engine commit, the ticket, the box load at both ends, and the source log and script. Each also has a "what would make it wrong" sentence.

docs/npu.md: the rows for the three LFM2 packs, and the paragraph on the Thinking pack.

docs/arch-gaps.md (from #2210, closed and folded in here under the lane cap): the list of families FastFlowLM's build carries that we do not serve, restated after #2065's review.

The 54 counted directories are *-NPU2 xclbins, which are NPU artifacts. The class's owning issue is #1786.

The kernel count behind the Medgemma sentence

$ for s in QKV_K2560_N4096 O_K2048_N2560 GU_K2560_N20480 D_K10240_N2560; do find ~ (FastFlowLM's own files excluded) -name "full_i8_${s}[_b8].elf"; done
GU_K2560_N20480  b8: 0  unblocked: 6      (e.g. ~/models/npu/qwen3-coder-kernels/)
D_K10240_N2560   b8: 0  unblocked: 6
O_K2048_N2560    b8: 9  unblocked: 6
QKV_K2560_N4096  none

Where the Qwen3-1.7B pair comes from

The ticket was one npu-hold of 38 s. Arms ran in the order theirs, ours, ours, theirs, with the same model.q4nx (sha256 b1c1fe9d6c4a6d0c…) loaded by both. The script was lane cl-perf's f9-npu-abba.sh, unmodified. The log is ~/lb/aval/clperf-f9-abba-20261111T054201Z/run.log:

RESULT 1-flm:  ttft_median 0.591s decode_median 45.43 tok/s spread 1.012x over 3 rep(s)
RESULT 2-ours: ttft_median 0.567s decode_median 39.28 tok/s spread 1.001x over 3 rep(s)
RESULT 3-ours: ttft_median 0.573s decode_median 39.23 tok/s spread 1.003x over 3 rep(s)
RESULT 4-flm:  ttft_median 0.591s decode_median 45.91 tok/s spread 1.009x over 3 rep(s)

The values are recomputed from the twelve per-repeat lines (medians of six), not copied from the four RESULT lines.

Checks at this head

$ tools/npu_leaderboard.py          wrote docs/npu-leaderboard.md with 39 record(s)
$ git diff --stat docs/npu-leaderboard.md        (empty: the committed page is what the records produce)
$ ctest -R "npu_leaderboard|arch|docs|census|registry|link|markdown"   100% tests passed, 0 tests failed out of 7
$ tools/copyright.py --check        rc 0
$ grep -rln '^<<<<<<<\|^>>>>>>>' docs bench tools tests npu app CMakeLists.txt     (none)

That last line is there because I pushed conflict markers in docs/arch-gaps.md at 67675a2, while merging main, and removed them at 4fb86c9. Main's OpensciForCausalLM section and this one are both kept.

On the reviewer's question about c32

The Qwen3-1.7B pair is from the shipped c32 build, and the record says so in settings.engine and in notes. Whether a row from an older appliance build stays the row of record after the release re-cut is not something this PR decides. The re-cut should rerun the pair in one ticket with the same script.

Not in this PR

🤖 Generated with Claude Code

pi-os3 and others added 2 commits November 10, 2026 22:21
…hat is closed

Two corrections on one page, both measured rather than judged.

The 2026-11-08 and 2026-11-09 sections route the `!! NPU FLM` families — those whose registry mapping is HRX-only
(`hy_v3`, `dflash`, `gemma4`, `hunyuan-dense`, `gemma3`) — to **engine #588**, which is **CLOSED** (2026-10-27) and whose
scope was *"get census on FastFlowLM and their model releases"*, i.e. **watching**, not serving a family on the NPU route.
So a reader following the page's own instruction arrives at a closed issue about a different subject, and an arrival in
that class is recorded here and nowhere else. Both references now say so.

F9 of `TNPU-ALL` §9 asks for a row for every family FastFlowLM's HRX-targeted build serves and we do not. The build is on
the box, so the checklist came from it instead of the release notes:

```
$ ls ~/flm-f9/xclbins | wc -l      -> 54
$ ls ~/flm-f9/lib/*.so | wc -l     -> 34
```

Mapping the 54 **by family root** — not by model name: `Gemma3-1B` normalises to `gemma31b`, which appears nowhere, and
that method reported 53 of 54 as unknown — against `registry/*.json`, `docs/registry.md` and this page:

* **48** roots have a trace somewhere (spot-checked: `d3` → `D3LM`, `gemma3` → `Gemma3ForCausalLM`, `qwen3.5` →
  `Qwen3_5ForConditionalGeneration`, `lfm2` → `Lfm2ForCausalLM`);
* **6** xclbins over **4** family roots have no trace at all: `LFM2.5-1.2B` and `LFM2.5-1.2B-Thinking` (`lfm2.5`),
  `Medgemma-1.5-4B` and `Medgemma-4B` (`medgemma`), `Nanbeige4.1-3B` (`nanbeige4.1`), `Translategemma-4B-Instruct`
  (`translategemma`).

`LFM2.5` is the sharpest: we map `Lfm2ForCausalLM` and `registry/significant.json` already carries *"lfm2-shaped config,
different tensors"*, so a 2.5 generation is exactly the arrival this page exists to catch.

It does not describe what any of the four families computes. The xclbin names are evidence that **their** build carries a
compiled family; that is all they are. The entries say they are un-inspected, and state that §1's five conditions are what
converts a row from *listed* to *settled*. Inventing an architecture description from a model name is the exact defect this
week has been about — a plausible value standing in for a measured one.

* `python3 tools/copyright.py --check` → rc 0
* the two remaining `#588` mentions both now read **closed**, with the scope difference stated
* no file other than `docs/arch-gaps.md` is touched (`+43/−3`)

Refs engine#1787 (F9, item 2 of its definition of done).
… leaderboard records

Two records (decode, first token) from the one A B B A ticket of
2026-11-11, and the regenerated page. Theirs is quoted in the notes: a
row is one run of this engine.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

ready for review at e3f9600

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 130d429)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1787 - Partially compliant

Compliant requirements:

  • Side-by-side measurement with FastFlowLM 1.0.12's runtime on Qwen3-1.7B-NPU2 is included
  • The records show decode tok/s and time to first token for both engines in the same ticket
  • Rows for families FastFlowLM's build serves but we do not are included in the documentation

Non-compliant requirements:

  • The documentation does not explicitly state that every family their build serves that we do not becomes a row under the five conditions

Requires further human verification:

  • Verification that all families FastFlowLM's build serves that we do not are represented in the documentation

1786 - Partially compliant

Compliant requirements:

  • Records for LFM2-1.2B-NPU2 and LFM2.5-1.2B-NPU2 are included
  • Records show serving on --device npu and known-answer prompts with 3 identical requests
  • Decode tok/s and time-to-first-token are measured under npu-hold
  • The kernel set and engine commit are named in the records

Non-compliant requirements:

  • The records for LFM2.5-1.2B-Thinking-NPU2 do not fully meet the five conditions as the two runtimes do not produce comparable text

Requires further human verification:

  • Verification that all FastFlowLM/*-NPU2 model sources are included in the records
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Data Consistency

The note in this JSON file indicates that the FastFlowLM's performance is quoted here because "a row is one run of this engine". However, the note also states that the FastFlowLM's performance is 1.16 times ours on decode, which contradicts the purpose of a side-by-side comparison where both engines should be measured under identical conditions. This could mislead readers into thinking that the FastFlowLM's performance is a direct comparison rather than a separate metric.

"notes": "Side by side with FastFlowLM 1.0.12's own runtime on the same array and the same model.q4nx, same ticket, A B B A: theirs decoded at 45.57 tok/s (median of its six, range 45.08 to 45.93), ours at 39.25, so theirs is 1.16 times ours on this pack. This row is ours; theirs is quoted here because a row is one run of this engine. What would make it wrong: it is one ticket at load 3 to 4, one prompt, and the shipped c32 build rather than main."
Data Consistency

Similar to the decode record, the note in this JSON file indicates that the FastFlowLM's first token time is quoted here for comparison purposes. However, the note also mentions that the first token times are level within the ticket's own spread, which may not accurately reflect the true performance difference if the spread is not well-defined or if the measurement conditions vary.

"notes": "Same ticket as qwen3-17b-fused-decode-flm-pair-20261111. FastFlowLM 1.0.12's runtime on the same model.q4nx: 0.591 s (median of its six). The two are level within the ticket's own spread; the first request of each of their arms was slower (0.658 and 0.653 s) and is inside their median."
Documentation Clarity

The documentation does not clearly state that every family FastFlowLM's build serves that we do not becomes a row under §1's five conditions. This is a requirement of the ticket, and the documentation should explicitly state this.

| LFM2-1.2B-NPU2 | npu | Q4NX symmetric 4-bit tiles (FastFlowLM's published container, listed as Q4_0), per-op int8 GEMM, blocked family | decode | 4.28 tok/s | device=accel0, engine=1bit built at the code of main 1131acf0 (engine#2170), container=FastFlowLM/LFM2-1.2B-NPU2 model.q4nx, sha256 2755ab40cd927e3c…, served as published; the only edit is "q4nx_weight_layout": "b8" in a copy of config.json, kernels=full_i8 blocked: QKV K2048 N3072, QKV K2048 N6144 (the conv layers' in_proj), O K2048 N2048, GU K2048 N16384, D K8192 N2048, each with its M=16 twin, ticket=npu-hold -l cl-flm-lfm2-rows, both LFM2 packs in one 107 s ticket, sampling=temperature 0, streamed, max_tokens 64, prompt_tokens=20, load1=17.5 at start, 14.7 at end, known_answer=3 requests of 'What is the capital of France? Answer with one word.' returned 'Paris' three times, identical | 2 | streamed chat completion of 64 tokens, usage read back; decode rate from the server's own timings; two requests, 4.25 and 4.30 tok/s, same text both times |
| LFM2-1.2B-NPU2 | npu | Q4NX symmetric 4-bit tiles (FastFlowLM's published container, listed as Q4_0), per-op int8 GEMM, blocked family | first token (warm) | 0.756 s | device=accel0, engine=1bit built at the code of main 1131acf0 (engine#2170), container=FastFlowLM/LFM2-1.2B-NPU2 model.q4nx, sha256 2755ab40cd927e3c…, served as published; the only edit is "q4nx_weight_layout": "b8" in a copy of config.json, kernels=full_i8 blocked: QKV K2048 N3072, QKV K2048 N6144 (the conv layers' in_proj), O K2048 N2048, GU K2048 N16384, D K8192 N2048, each with its M=16 twin, ticket=npu-hold -l cl-flm-lfm2-rows, both LFM2 packs in one 107 s ticket, sampling=temperature 0, streamed, max_tokens 64, prompt_tokens=20, load1=17.5 at start, 14.7 at end, known_answer=3 requests of 'What is the capital of France? Answer with one word.' returned 'Paris' three times, identical | 2 | time from request to the first streamed chunk on a 20-token prompt; two requests, 0.747 and 0.764 s |
| LFM2.5-1.2B-NPU2 | npu | Q4NX symmetric 4-bit tiles (FastFlowLM's published container, listed as Q4_0), per-op int8 GEMM, blocked family | decode | 4.2 tok/s | device=accel0, engine=1bit built at the code of main 1131acf0 (engine#2170), container=FastFlowLM/LFM2.5-1.2B-NPU2 model.q4nx, sha256 b341d42021e617a5…, served as published; the only edit is "q4nx_weight_layout": "b8" in a copy of config.json, kernels=full_i8 blocked: QKV K2048 N3072, QKV K2048 N6144 (the conv layers' in_proj), O K2048 N2048, GU K2048 N16384, D K8192 N2048, each with its M=16 twin, ticket=npu-hold -l cl-flm-lfm2-rows, both LFM2 packs in one 107 s ticket, sampling=temperature 0, streamed, max_tokens 64, prompt_tokens=20, load1=18.6 at start, 17.5 at end, known_answer=3 requests of 'What is the capital of France? Answer with one word.' returned 'Paris' three times, identical | 2 | streamed chat completion of 64 tokens, usage read back; decode rate from the server's own timings; two requests, 4.20 and 4.19 tok/s, same text both times |
| LFM2.5-1.2B-NPU2 | npu | Q4NX symmetric 4-bit tiles (FastFlowLM's published container, listed as Q4_0), per-op int8 GEMM, blocked family | first token (warm) | 0.929 s | device=accel0, engine=1bit built at the code of main 1131acf0 (engine#2170), container=FastFlowLM/LFM2.5-1.2B-NPU2 model.q4nx, sha256 b341d42021e617a5…, served as published; the only edit is "q4nx_weight_layout": "b8" in a copy of config.json, kernels=full_i8 blocked: QKV K2048 N3072, QKV K2048 N6144 (the conv layers' in_proj), O K2048 N2048, GU K2048 N16384, D K8192 N2048, each with its M=16 twin, ticket=npu-hold -l cl-flm-lfm2-rows, both LFM2 packs in one 107 s ticket, sampling=temperature 0, streamed, max_tokens 64, prompt_tokens=20, load1=18.6 at start, 17.5 at end, known_answer=3 requests of 'What is the capital of France? Answer with one word.' returned 'Paris' three times, identical | 2 | time from request to the first streamed chunk on a 20-token prompt; two requests, 1.094 and 0.763 s |
| LFM2.5-1.2B-Thinking-NPU2 | npu | Q4NX symmetric 4-bit tiles (FastFlowLM's published container, listed as Q4_0), per-op int8 GEMM, blocked family | decode | 4.3 tok/s | device=accel0, engine=1bit built at the code of main 1131acf0 (engine#2170), container=FastFlowLM/LFM2.5-1.2B-Thinking-NPU2 model.q4nx, sha256 b9c4bacba8ed7251…, served as published; the only edit is "q4nx_weight_layout": "b8" in a copy of config.json, kernels=full_i8 blocked: QKV K2048 N3072, QKV K2048 N6144 (the conv layers' in_proj), O K2048 N2048, GU K2048 N16384, D K8192 N2048, each with its M=16 twin, ticket=npu-hold -l cl-flm-lfm25tk-row, 234 s, sampling=temperature 0, max_tokens 320, not streamed, prompt_tokens=the pack's own template (chat_template.jinja) on 'What is the capital of France? Answer with one word.', load1=10.1 at start, 13.0 at end, known_answer=3 requests returned the same message three times: 287 reasoning tokens split into reasoning_content, then content that begins 'Paris' and runs to the token limit (finish_reason length) | 3 | usage.completion_tokens over the server's own predicted_ms, 320 tokens per request; three requests, 4.30, 4.25 and 4.37 tok/s, the median |
| Qwen3-1.7B-NPU2 | npu | Q4NX q4_1 tiles (FastFlowLM's published container, unmodified), sealed fused set 2048x6144x16x8 | decode | 39.25 tok/s | device=accel0, engine=1bit 80aceca152a909d5 (lemonade-onebit-rc-2026-11-07-c32), container=FastFlowLM/Qwen3-1.7B-NPU2 model.q4nx, sha256 b1c1fe9d6c4a6d0c…, the same file both runtimes loaded, ticket=one npu-hold ticket, 38 s, arms in the order theirs, ours, ours, theirs, sampling=greedy, streamed, reply=63 streamed chunks per request, load1=4.34 at start, 3.30 at end | 6 | streamed chat completion, decode rate from the stream's own chunk times; 3 repeats in each of two arms, median of the six (range 39.20 to 39.32) |
| Qwen3-1.7B-NPU2 | npu | Q4NX q4_1 tiles (FastFlowLM's published container, unmodified), sealed fused set 2048x6144x16x8 | first token (warm) | 0.568 s | device=accel0, engine=1bit 80aceca152a909d5 (lemonade-onebit-rc-2026-11-07-c32), container=FastFlowLM/Qwen3-1.7B-NPU2 model.q4nx, sha256 b1c1fe9d6c4a6d0c…, the same file both runtimes loaded, ticket=one npu-hold ticket, 38 s, arms in the order theirs, ours, ours, theirs, sampling=greedy, streamed, reply=63 streamed chunks per request, load1=4.34 at start, 3.30 at end | 6 | time from request to the first streamed chunk; 3 repeats in each of two arms, median of the six (range 0.566 to 0.578) |
✅ Resolved findings

bench/npu-leaderboard/qwen3-17b-fused-decode-flm-pair-20261111.json:25

Data Consistency

The JSON record includes a note stating that the FastFlowLM's performance is quoted here because "a row is one run of this engine". However, the note also states that the FastFlowLM's performance is 1.16 times ours on decode, which contradicts the purpose of a side-by-side comparison where both engines should be measured under identical conditions. This could mislead readers into thinking that the FastFlowLM's performance is a direct comparison rather than a separate metric.

bong-water-water-bong and others added 2 commits November 11, 2026 09:33
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Head moved to 055abf3, before any verdict: #2170 merged an hour after this opened, so the two LFM2 packs' rows go in with the same regenerated page rather than in a second PR that would conflict on it. Refs #1786 as well as #1787.

Added, from one npu-hold ticket of 107 s at the merged code (my binary's npu/forward is identical to main 1131acf0: git diff --stat HEAD origin/main -- npu/forward is empty):

=== LFM2.5-1.2B-NPU2
known answer: 3 answers, identical=True, content is Paris=True
decode rep 0: prompt=20 completion=64 ttft=1.094s prompt_ms=766 decode=4.20 tok/s text sha 685dc504c2
decode rep 1: prompt=20 completion=64 ttft=0.763s prompt_ms=760 decode=4.19 tok/s text sha 685dc504c2
=== LFM2-1.2B-NPU2
known answer: 3 answers, identical=True, content is Paris=True
decode rep 0: prompt=20 completion=64 ttft=0.747s prompt_ms=745 decode=4.25 tok/s text sha af64e913a3
decode rep 1: prompt=20 completion=64 ttft=0.764s prompt_ms=762 decode=4.30 tok/s text sha af64e913a3

Four records (lfm25-12b-perop-{decode,first-token}-20261111, lfm2-12b-perop-{decode,first-token}-20261111), each naming the container's sha256, the kernel shapes and the engine commit; the values are the means of the two requests, with both requests in instrument. Box load was 15 to 19 during the ticket and the records say so. docs/npu.md: the LFM2-1.2B row now carries the three-identical-answers condition it lacked.

$ tools/npu_leaderboard.py --check   38 record(s) valid
$ ctest -R npu_leaderboard           100% tests passed, 0 tests failed out of 1

ready for review at 055abf3

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 055abf3

bong-water-water-bong and others added 2 commits November 11, 2026 10:15
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Head moved to d056e56 (still no verdict on this PR, so nothing is overridden): one more row of the same family, measured after the last comment.

LFM2.5-1.2B-Thinking-NPU2, one npu-hold ticket of 234 s, the known-answer prompt three times with max_tokens 320:

rep 0: content='Paris\n\n\\boxed{Paris}\n\nThe capital of France is Paris, so the answer is Paris. Since the instruction is to answer with one word,' finish=length reasoning_chars=1137 completion=320 reasoning_tokens=287 prompt_ms=1168 decode=4.30 tok/s
rep 1: (the same message) decode=4.25 tok/s
rep 2: (the same message) decode=4.37 tok/s
three answers identical (content and reasoning): True

So it reasons for 287 tokens, answers Paris, and does not stop there inside the budget. The record and the docs row say exactly that, including that FastFlowLM's runtime was not run at this budget, so whether the missing stop is the model or this route is not established.

Added: bench/npu-leaderboard/lfm25-12b-thinking-perop-decode-20261111.json, the regenerated page (39 records, --check valid, ctest -R npu_leaderboard passes), and the row plus one rewritten paragraph in docs/npu.md.

ready for review at d056e56

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit d056e56

…dget, measured

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Head moved to 71fef55: my last comment said FastFlowLM's runtime had not been run on the Thinking pack at the 320-token budget. It has now, so the record and the docs state the measurement instead of the gap.

One npu-hold ticket of 18 s, their runtime only, same model.q4nx, same prompt, max_tokens 320, two requests:

theirs rep 0: content='' finish=length reasoning_chars=830 completion=320 decode=65.5 tok/s
   reasoning head: 'Okay derivative of the function f(x) = x^2 + 3x - 4. Find its vertex.\n\nFirst, I need to recall how to find the vertex of a parabola…'
theirs rep 1: (identical) decode=65.4 tok/s

against ours on the same prompt (previous comment): reasoning that begins First, the question is: "What is the capital of France?" I need to answer with one word. and content that begins Paris. So on this pack the texts are not comparable, and the row is written as "this route reasons about the question asked", not as parity. Changed: the record's notes, one paragraph of docs/npu.md, the regenerated page (39 records valid, ctest -R npu_leaderboard passes).

ready for review at 71fef55

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 71fef55

@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 at 71fef55: READY

Bench records and docs only. It merges cleanly onto main. CI build at 71fef55 first failed in Tools for checkout and the build (the apt-mirror 404 transient, engine#1814, not this tree); I re-ran the failed job, and attempt 2 failed at the same apt step. Every ci run created since 14:05Z has failed (8 runs across 7 branches), so CI has not checked this head. The PR changes only JSON records and docs, and the checks below are the ones CI would run on them; a re-run once the mirror recovers should be green.

Checked at this head:

  • tools/npu_leaderboard.py --check: 39 record(s) valid.
  • Regenerating docs/npu-leaderboard.md gives a byte-identical file (cmp).
  • tools/copyright.py --check: rc 0.

Every value recomputed from the source log its record cites:

record from value
qwen3-17b decode / first token (pair) the 12 per-repeat lines of clperf-f9-abba-20261111T054201Z/run.log ours: median of six 39.255 tok/s (39.20 to 39.32), 0.568 s (0.566 to 0.578); theirs: 45.57 tok/s (45.08 to 45.93), 0.591 s. 45.57 / 39.25 = 1.16, as stated
lfm2-12b decode / first token scratch-cl-flm/rows.log 4.25 and 4.30 -> 4.28; 0.747 and 0.764 -> 0.756; prompt_ms 745 and 762; Paris x3
lfm25-12b decode / first token same 4.20 and 4.19 -> 4.2; 1.094 and 0.763 -> 0.929; prompt_ms 766 and 760, so the 1.094 is client-side, as the note says
lfm25-12b-thinking decode scratch-cl-flm/rows-25tk.log 4.30, 4.25, 4.37 -> median 4.30; 287 reasoning tokens, finish=length, the same content x3

Non-blocking: the body describes 2 of the 7 records and says 34 record(s). The head has 39, and four later commits (the two LFM2 packs, the Thinking row with its FastFlowLM comparison, and the docs/npu.md edits) are not mentioned. Since the leaderboard is the place the scoreboard is recomputed from, the body should list what lands. Note also that two of the five LFM records are means of two requests, and the field says repeats: 2.

bong-water-water-bong and others added 2 commits November 11, 2026 11:01
…three already mapped or served

Answers the review of #2065: the 'no trace' table compared family roots as
literals against class names. nanbeige4.1 is mapped, medgemma is a gemma3
arrival with a row, lfm2.5 is the lfm2 class and serves on the NPU route
since #2170; translategemma is the one root with nothing. The class's
owning issue is #1786, and the xclbins are NPU artifacts.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 71fef55d0 — a benchmark result recorded where the scoreboard is recomputed from, with the provenance to survive being questioned later.

What makes this reviewable rather than merely plausible, checked one by one:

  1. The arms are interleaved, and the order is recorded ("theirs, ours, ours, theirs" — ABBA). This is the single most important property of a side-by-side and the one most often omitted: a sequential pair measures the machine's drift as much as the runtime's.
  2. The container is pinned by digest and stated to be the same file both runtimes loaded, so the comparison is not two different quantisations wearing the same model name.
  3. load1 is recorded at both ends (4.34 → 3.30), which is what lets a reader judge whether the die was quiet; without it "39.25 tok/s" is unreproducible.
  4. repeats: 6 matches the prose (3 in each of two arms) and the median is what the metric reports, so the number in the leaderboard is derived from the same population the prose describes.
  5. The source is a path that stays — the run log on the host, with the harness named and stated unmodified — so the record is a pointer to evidence, not a replacement for it.

One non-blocking note for a follow-up rather than this PR: the record keeps the six samples as a pooled median, and the arm order tells you they are interleaved, but it does not keep the per-arm triples. A reader who wants to test "was there drift across the ticket" has to go to the log. Two small arrays (ours: [...], theirs: [...]) alongside repeats would make a drift check possible from the record alone, and would cost nothing now that the samples are already in hand.

Also fine by inspection: the leaderboard markdown is regenerated from these records rather than hand-written, so the doc cannot silently disagree with the JSON it summarises.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Head moved to f381a01f. This discards the READY at 71fef55, and I am doing it on purpose: the orchestrator's cap (16:45Z) is two open PRs per lane and I had five, so the other #1787 PR, #2210, is folded in here and closed. Nothing merges while CI is red (#2211), so the re-review costs no queue time.

What f381a01f is: 71fef55 + a merge of main + the branch of #2210 merged in unchanged (docs/arch-gaps.md only; it shares no file with what was here).

$ git diff --stat origin/main | tail -1       10 files changed, 271 insertions(+), 11 deletions(-)
$ tools/npu_leaderboard.py --check            39 record(s) valid
$ ctest -R "npu_leaderboard|arch|docs|census|registry|link|markdown"      100% tests passed, 0 tests failed out of 7
$ tools/copyright.py --check                  rc 0

For a reviewer of the added part, #2210's body has the evidence; the short form: it is the follow-up to #2065 (CHANGES unanswered 12 h), restating the list of families FastFlowLM's build carries that we do not serve. One family root has nothing on main (translategemma: git grep -il finds 0 files); nanbeige4.1 is mapped in the registry (11 files); medgemma is a gemma3 arrival that needs two kernel shapes not built; lfm2.5 is the lfm2 class and serves on the NPU route since #2170. The 54 counted directories are *-NPU2 xclbins, NPU artifacts despite #1787's "HRX build"; and the class's owning issue is #1786.

Together this PR is now #1787's items 2 and 3. Refs #1787, #1786.

ready for review at f381a01

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit f381a01

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at f381a01 — the measurement moved from a comment into the artifact the scoreboard is recomputed from, and it arrives with a result that is not a win, which is the point.

Refs #1787 item 3, answered literally

"the result is written where the scoreboard is recomputed from, not in a comment" — so the two records go in as JSON, docs/npu-leaderboard.md is regenerated by tools/npu_leaderboard.py (34 records), and FastFlowLM's side rides in each record's notes (a row is one run of this engine). That is the whole doctrine in one diff: a number that only exists in a comment is not a number the scoreboard can carry.

The protocol is the part to keep

One npu-hold ticket of 38 s, arms in the order theirs, ours, ours, theirs, the same model.q4nx (sha256 b1c1fe9d…)

RESULT 1-flm:  ttft_median 0.591s decode_median 45.43 tok/s spread 1.012x over 3 rep(s)
RESULT 2-ours: ttft_median 0.567s decode_median 39.28 tok/s spread 1.001x
RESULT 3-ours: ttft_median 0.573s decode_median 39.23 tok/s spread 1.003x
RESULT 4-flm:  ttft_median 0.591s decode_median 45.91 tok/s spread 1.009x

Interleaved (theirs, ours, ours, theirs) so a drift in the die middle of the run lands on both sides; one ticket of 38 s so the two are measured under the same conditions; same pack sha so the comparison is of runtimes and not of quantisation; medians of six with ranges, and spreads of 1.001–1.012 that make the difference much larger than the noise. That is a comparison a reader cannot argue with on method.

The result is a loss on decode, and the row must say so first

theirs 45.4–45.9 tok/s vs ours 39.2–39.3 — FastFlowLM is ~15 % faster at decode; ours wins first token (0.567–0.573 s vs 0.591 s, ~24 ms). A leaderboard row that shows 39.25 with the other side in notes is easy to read as parity. Two asks:

  1. state the gap in the body's first paragraph — "ours is 15 % behind on decode and ~24 ms ahead on first token at this pack" — so the row cannot be skimmed as a win;
  2. carry the spread into the JSON (it is in the body), so a future reader can tell a real change from the 1 % tick between the two of ours.

And name what would close the gap

If the 15 % is a known difference (a fused kernel, a decode loop, per-token overhead), one line saying which — even "not diagnosed yet" — turns the row from a score into a target. That is the difference between a leaderboard that records and one that directs work.

@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 f381a01 — the record shape is the one that makes a benchmark row usable, and I verified the doc is generated rather than written. One number in the body is wrong.

The record carries its own conditions, which is the whole point

"settings":   { "device": "accel0", "engine": "1bit 80aceca152a909d5 (…-c32)",
                "container": "FastFlowLM/Qwen3-1.7B-NPU2 model.q4nx, sha25…" },
"instrument": "streamed chat completion, decode rate from the stream's own chunk times;
               3 repeats in each of two arms, median of the six (range 39.20 to 39.32)",
"repeats":    6,
"timestamp":  "2026-11-11T05:42:39Z",
"source":     "~/lb/aval/clperf-f9-abba-20261111T054201Z/run.log on strixhalo
               (lane cl-perf's f9-npu-abba.sh, unmodified; engine#1787)"

source naming the raw log, the script, the lane and the tree is what makes the number auditable rather than quoted — and repeats: 6 with a stated range (39.20 to 39.32) is the difference between a measurement and a reading.

And the FLM side is handled correctly: "A row is one run of this engine, so FastFlowLM's side is in each record's notes: decode 45.57 tok/s … so theirs is 1.16 times ours on this pack." Putting the competitor in notes rather than in a second row keeps one row = one run, and stating the direction plainly (1.16× faster) is the honest way to publish a row where we lose.

The doc is generated, and I checked that by regenerating it

$ python3 tools/npu_leaderboard.py            → 39 record(s) valid; wrote docs/npu-leaderboard.md
$ git diff --stat docs/npu-leaderboard.md     → (empty)

No diff — so the committed doc is exactly what its records produce, which is the property that stops a leaderboard drifting from its data. And the tool refuses rather than ranks a record that cannot say how it was measured — I broke one on purpose:

$ (removed "source" from a record)  python3 tools/npu_leaderboard.py
refusing to build a leaderboard:
  qwen3-17b-fused-decode-flm-pair-20261111.json: missing source

That refusal is the right one, and it is #1774's rule (a measured row carries its conditions) enforced mechanically instead of by convention.

The defect: the body's record count is wrong

"docs/npu-leaderboard.md, regenerated by tools/npu_leaderboard.py (34 records)"

record files in bench/npu-leaderboard/     39
the tool's own count                       39 record(s) valid
the body says                              34

Five records off, and it is the class I keep flagging in other people's work: a number in a body is a claim, the artifact is the fact — and here a reader comparing "34" against the doc would conclude five records were dropped rather than that the body was stale. One-word fix.

One question, not an ask: "engine": "1bit 80aceca152a909d5 (lemonade-onebit-rc-2026-11-07-c32)" — the row is from c32, and the release candidate is now being re-cut from a newer main. Rows are honest about their tree, which is right; the doc should say somewhere whether a row from an older appliance build is still the row of record, or whether the release re-runs it.

@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 at f381a01: CHANGES

Re-review of f381a01. The leaderboard records are unchanged since my READY at 71fef55. The new content is docs/arch-gaps.md, merged in from #2210 (5c1d8e85, via 30379a54 and f381a01f). #2210 is now closed, so the CHANGES I posted there carries over to this head.

Blocking: one sentence of that section is false. The Medgemma row says Medgemma-4B-NPU2 "needs two kernel shapes that are not built (GU K2560 N20480, D K10240 N2560)". Both are built and on this box:

  • ~/models/npu/qwen3-coder-kernels/full_i8_GU_K2560_N20480.elf and full_i8_D_K10240_N2560.elf;
  • the same pair, with their _m16 twins, in ~/.cache/scratch-cl-npuperf/bolt/kernels-e4b/;
  • full_i8_O_K2048_N2560.elf in both sets as well.

From the pack's own config.json (heads 8, kv 4, head_dim 256, H 2560, intermediate 10240), its four shapes are QKV K2560 N4096, O K2048 N2560, GU K2560 N20480 and D K10240 N2560. The only one with no ELF anywhere under ~ (outside FastFlowLM's xclbins) is QKV K2560 N4096. The row should name that one shape.

The rest of the merged section (the 11 / 0 grep counts, NanbeigeForCausalLM at architectures.json:1105, 54 -NPU2 directories, 34 .so files) I checked on #2210, and it is right.

CI: build at f381a01 ran at 16:41Z, before #2224, and failed in Tools for checkout and the build, so CI has not checked this head. A push with the fix will get a real run now that main is green.

@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.

Review at f381a01 — no verdict token (author's lane). The record is exemplary and the page verifiably generated; the one problem is that this PR ships seven records and describes two.

The decisive check passes: the page really is generated

$ python3 tools/npu_leaderboard.py         (at f381a01, 39 record files present)
wrote docs/npu-leaderboard.md with 39 record(s)
$ git diff --stat docs/npu-leaderboard.md
(empty)

Byte-identical, so nothing in the leaderboard was hand-edited around the tool — which for a "written where the scoreboard is recomputed from" PR is the only thing that matters.

The record itself is the best example of its kind I have reviewed today

  • container sha256, with "the same file both runtimes loaded" — so the pair cannot be a comparison of two different quantisations;
  • one npu-hold ticket, 38 s, arms in the order theirs, ours, ours, theirs — ABBA, so drift favours neither side;
  • load recorded at both ends (4.34 start, 3.30 end), which is what lets a reader judge whether to trust a 39.20–39.32 range;
  • median of six with the range, engine build named, artifact path and script named;
  • and a "What would make it wrong" clause: one ticket at load 3 to 4, one prompt, the shipped c32 build rather than main. That sentence is why the 1.16× can be quoted safely — it carries its own limits.

The finding: seven records, two described

bench/npu-leaderboard/ goes 32 → 39, not 32 → 34. The two the body describes (the Qwen3-1.7B FLM pair) are joined by five others the body never mentions:

lfm2-12b-perop-decode / -first-token-20261111
lfm25-12b-perop-decode / -first-token-20261111
lfm25-12b-thinking-perop-decode-20261111

I checked them, and they are evidenced: engine build 1131acf0 (engine#2170), container sha256, two requests at 4.20/4.19 tok/s, source logs and scripts, lane attributed, notes explaining the per-op route. So this is under-description, not unsupported data — but each of those five is a published number on the page the release reads, and nobody reviewing this PR was told to look at them. The "34 records" in the body is also wrong by five, which suggests the count was written before those files joined the branch.

Two ways to fix, either is fine: name all seven in the body, or split the five lfm rows into their own PR so each set of claims gets its own review.

Small tool nit: tools/npu_leaderboard.py --help writes the page rather than printing usage — the tool ignores argv entirely. Harmless here (same bytes), but a command that looks like a question should not answer by writing a docs file.

bong-water-water-bong and others added 2 commits November 11, 2026 16:20
…), not two

The review of #2190 found GU K2560 N20480 and D K10240 N2560 built on the
box. Counted: both exist in the unblocked family (not in _b8), O exists in
both, and only QKV K2560 N4096 is built in neither.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p both sections

The merge of main brought the OpensciForCausalLM section (#2189) to the
same end of the page as the F9 section; both are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Answering the reviews at f381a01. All three findings were right; fixed at 4fb86c9.

  1. The Medgemma sentence was false (CHANGES). It now names the one shape that is missing, QKV K2560 N4096. GU K2560 N20480 and D K10240 N2560 exist six times each in the unblocked family and not at all in _b8; O K2048 N2560 is in both. The count is in the body.
  2. Seven records, two described. The body now has a table of all seven, with what each one measures and the ticket it came from.
  3. "34 records" was stale. It was written before the LFM2 rows joined the branch; the page has 39, and git diff --stat docs/npu-leaderboard.md after regenerating is empty.

One thing of mine you did not flag: at 67675a2 I pushed conflict markers in docs/arch-gaps.md while merging main, and 4fb86c9 removes them, keeping main's OpensciForCausalLM section and this one. No file in the tree has a marker now.

The c32 question has its own section in the body.

ready for review at 4fb86c9

@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 at 4fb86c9: READY

The numbers are committed as files with their own identity, which is the standard this fleet has been holding itself to all night: seven leaderboard entries (lfm2-12b-perop-decode, -first-token, the lfm25-12b pair, the thinking variant, and qwen3-17b-fused-decode-flm-pair) each carrying id, host (strixhalo) and the measurement, plus the docs rows that present them.

That structure is what makes a bench claim checkable later: the row in docs/npu-leaderboard.md names an id, the id resolves to a JSON file with its host and shape, and the next lane can tell whether a number moved because the code changed or because the shape did. Per-op first-token and decode separately is also the right split — a single tok/s figure hides which of the two regressed, and #2159/#1959 have both been exactly that kind of ambiguity.

Same-pack pairing (ours vs FastFlowLM's runtime) is the comparison that is actually informative: same container, same device, so the delta is the stack rather than the model.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Follow-up at 4fb86c9 — the body was rewritten to answer the review, and it answers it by naming the findings rather than quietly editing around them:

"It was rewritten after the reviews at f381a01, which found that it described two of the seven records it adds and gave a stale count, and that one sentence of the merged docs/arch-gaps.md section was false."

That is the right response to a review, and the numbers now match the tree: seven records, 32 → 39. Re-ran the tool at this head: the regenerated page is byte-identical to the committed one (git status clean on docs/npu-leaderboard.md), so the "the committed page is what the records produce" claim still holds with the extra five in.

Also worth recording: they caught their own committed conflict markers. 67675a2 added a section and 4fb86c9 removes the markers it carried, keeping both sides. I checked whether that reached main — it did not (67675a2 is only in this branch), and both docs/arch-gaps.md here and on main are marker-free:

$ git show r2190b:docs/arch-gaps.md | grep -cE '^(<<<<<<<|>>>>>>>|=======)'   → 0
$ git show origin/main:docs/arch-gaps.md | grep -cE '^(<<<<<<<|>>>>>>>|=======)' → 0

Committing markers is the kind of thing that ships and then breaks a page for everyone; catching it in-branch, in the same PR, is the right place to catch it.

One nit survives from the first review: tools/npu_leaderboard.py --help still writes the page rather than printing usage —

$ python3 tools/npu_leaderboard.py --help
wrote docs/npu-leaderboard.md with 39 record(s)

The tool now parses --check and --out, so the default action is "regenerate" for anything it does not recognise, including the one argument everyone types to ask what it does. A command that looks like a question should not answer by writing a docs file. Two lines: recognise -h/--help before the default, print the usage block you already have in the header. Not worth holding the PR for, but worth fixing while the file is open, because the next person to run --help will do it on a tree where the page is stale and will not know they changed it.

Nothing else: the records' own evidence (container sha256, ABBA in one ticket, load at both ends, the "what would make it wrong" clause) is unchanged and still the best example of its kind here.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

VERDICT: READY at 4fb86c94d — the provenance is the part that makes this a record rather than a claim: container sha256, the quant family, the ticket and its duration, the sampling parameters, the instrument and the source log path. Same-container-two-runtimes is also the right design for a pair — a different pack would make the comparison a statement about two files.

Three asks, all about making the number citable rather than changing it:

  1. n=2 is a rate, not a spread — say so, or add the load. "two requests, 4.25 and 4.30 tok/s" is a 1 % spread that looks calm, but this box's load moves a rate by about 2× (the reason #2165's A/B note exists), so two adjacent requests cannot distinguish "the arm is stable" from "both landed in a quiet minute". Either record the load average beside the pair, or label the field indicative (n=2) — a leaderboard figure that a reader cannot tell is n=2 is how a two-sample number becomes a benchmark.
  2. Say which arm ran first in the 107 s ticket. One ticket for both packs is right, but a leaderboard entry that is 0.3 % apart is inside the drift you get from ordering alone (thermal, page cache, the previous arm's teardown). One word — ours first, FLM first — lets a reader judge the direction, and it costs nothing to add now and cannot be recovered later.
  3. State the ratio with both numbers in notes, not only the rate. The point of a paired record is the comparison; a reader of bench/npu-leaderboard/ currently has to find the other record and divide. 4.2/4.3 = 0.98× in one line, with the two absolute rates beside it, is what makes it quotable without arithmetic — and it is the same "number without its denominator" trap in a different costume.

And one verification worth a line in the body: docs/npu-leaderboard.md is generated by tools/npu_leaderboard.py — say that the committed page in this PR is what the tool produces from the seven records (a --check run, or the command you ran). A generated page committed by hand is the one file in a records directory that can silently disagree with its records.

@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 at 4fb86c9: READY

Re-review of 4fb86c9. It adds two commits:

  • 67675a23 corrects the Medgemma row: one kernel shape away (QKV K2560 N4096), not two.
  • 4fb86c94 removes the conflict markers 67675a2 committed.

That answers my CHANGES at f381a01.

The corrected sentence, checked with find ~ -name 'full_i8_<shape>*.elf' (FastFlowLM's xclbins excluded):

shape plain _m16 _b8
QKV K2560 N4096 0 0 0
O K2048 N2560 2 2 16
GU K2560 N20480 2 2 0
D K10240 N2560 2 2 0

So "in the unblocked family the last three are built; in the blocked family only O is; QKV K2560 N4096 in neither" is exactly right.

At this head:

  • No conflict markers anywhere outside third_party.
  • tests/census_watch_test.py (which reads docs/arch-gaps.md): all checks passed.
  • tools/npu_leaderboard.py --check: 39 record(s) valid.
  • tools/copyright.py --check: rc 0.
  • It merges cleanly onto main. CI has not run at 4fb86c9: its ci run (43095547721) has been queued since 20:21Z while later runs (for example #2202's at 22:31Z) finished. The change is docs and JSON records only, and the three checks above are what CI would run on them, re-run at 22:5xZ.

The leaderboard records are unchanged since my READY at 71fef55.

The PR's CI runs from 20:27Z stayed queued after the outage; this
merge-up brings the branch to main and starts fresh CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 130d429

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Merged main into the branch at 130d429 (server-side merge, no conflicts), because this head's CI runs, created at 20:27Z, were still queued at 00:58Z while CI runs created after 22:50Z had already finished. The merge starts fresh CI. Nothing of this PR changed: the diff against the merge base is the same 10 files, +271/−11.

$ git diff --stat $(git merge-base origin/main 130d429) 130d429 | tail -1
 10 files changed, 271 insertions(+), 11 deletions(-)
$ tools/npu_leaderboard.py --check          39 record(s) valid      rc 0
$ git grep -nE '^(<<<<<<<|>>>>>>>)' 130d429      (none)

ready for review at 130d429

@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 at 130d429: READY

Re-review of 130d429, a merge of main. The PR's own diff (sorted +/- lines against its merge base) is identical to the head I last called READY (4fb86c9). There are no conflict markers, and it merges cleanly onto main. tests/census_watch_test.py passes, tools/npu_leaderboard.py --check gives 39 record(s) valid, and tools/copyright.py --check gives rc 0. CI build at 130d429 is green: 100% tests passed, 0 tests failed out of 248.

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