Skip to content

Commit 898e2be

Browse files
committed
Publish benchmark review and automate performance diagnostics
1 parent fa551fb commit 898e2be

8 files changed

Lines changed: 99 additions & 21 deletions

File tree

‎.github/workflows/performance.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
name: performance-diagnostic
22

33
on:
4+
push:
5+
branches: [main]
46
workflow_dispatch:
57

68
permissions:

‎README.md‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ Details: [`docs/Architecture.md`](docs/Architecture.md).
3939

4040
| Question | Answer |
4141
|---|---|
42-
| Does Synapse write as fast as **llama.cpp**? | **Yes.** 102% of its speed on 2 threads, 114% on 8 threads. |
43-
| Is a whole short request faster? | **Yes, 3.2× faster.** The model loads almost instantly. |
44-
| Does it use less memory? | **Yes, 2.3× less.** |
45-
| Does it start answering fast? | **Short prompts: yes. Long prompts: no, about 3× slower.** Main thing to fix. |
42+
| Does Synapse write as fast as **llama.cpp**? | **In the 8-token smoke, yes.** The native kernel reached 102% of llama.cpp at 2 threads and 114% at 8 threads. |
43+
| Is a whole short request faster? | **In that smoke, 3.2× faster.** Synapse's median fresh-process wall time was 216 ms versus 690 ms. |
44+
| Does it use less memory? | **In that smoke, 2.3× less peak RSS.** 555 MiB versus 1,256 MiB. |
45+
| Does it start answering fast? | **For the 5-token prompt, yes.** The older 274-token run was about 3× slower to first token; the new batched-prefill path still needs the same long test. |
4646
| Is it as fast as **MLX** on the Apple GPU? | **No.** MLX writes 225 tokens/s on the GPU. Synapse has no GPU code yet. |
47-
| How does **Microsoft Foundry Local** do? | **A separate test, 6 models.** With its own ONNX weights on about 6 cores, Qwen2.5 0.5B writes 231 tokens/s and the 7B models 22–25 tokens/s. |
48-
| Is it faster than the other .NET engine, dotLLM? | **Yes, 14× faster.** |
47+
| How does **Microsoft Foundry Local** do? | **Fast, but quality-limited in this test.** With separate ONNX weights on about 6 cores, Qwen2.5 0.5B reached 231 tokens/s and the 7B models 22–25 tokens/s; none of the 7 model/device variants fully completed the 128-token instruction. |
48+
| Is it faster than the other .NET engine, dotLLM? | **In the same 8-token smoke, 14× faster.** |
4949

5050
### Where it is tested
5151

@@ -68,6 +68,20 @@ Details: [`docs/Architecture.md`](docs/Architecture.md).
6868

6969
![Synapse vs the reference engines on a MacBook Pro M2 Pro](docs/images/benchmark.svg)
7070

71+
What the result says:
72+
73+
- The Rust kernel produced the same eight tokens as llama.cpp in every
74+
measured round and reached 136.6 versus 133.7 decode tokens/s at two
75+
threads, then 190.6 versus 167.2 at eight threads.
76+
- The portable SIMD C# kernel reached 124.4 tokens/s at two threads, or 88%
77+
of llama.cpp's 141.8. The Rust boundary is therefore useful in this measured
78+
hotspot, while the C# path remains the tested portable implementation.
79+
- This is three warm-ups plus five measurements of an eight-token answer. It
80+
proves short parity and supplies a diagnostic speed signal; it is not the
81+
30-pair release verdict and does not establish long-answer quality.
82+
- The published long-prompt gap comes from an older build. Batched prefill is
83+
implemented now, but its fresh GitHub long-answer result is still pending.
84+
7185
### GitHub Actions, committed code
7286

7387
![GitHub Actions performance run on macOS, Ubuntu, and Windows](docs/images/github-actions.svg)
@@ -88,14 +102,20 @@ Details: [`docs/Architecture.md`](docs/Architecture.md).
88102
up front (Phi-3.5-mini: 98 GiB, 12 s to the first token).
89103
- On GitHub, every runner and model pair is a separate job, and a model runs
90104
only where its file fits in half the RAM.
105+
- Manual review of the preserved 128-token outputs found no fully compliant
106+
result among the seven model/device variants. Qwen3 and DeepSeek-R1 spent the
107+
budget on reasoning and emitted no final answer; Qwen2.5 CPU and Phi-3.5
108+
contained factual errors; Phi-4, Mistral, and Qwen2.5 WebGPU left required
109+
sections or the three-line recap incomplete. These bars measure throughput,
110+
not answer quality, and are never ranked against the GGUF or MLX cohorts.
91111

92112
### What to improve, and where
93113

94114
| | Problem | Fix | Where |
95115
|---|---|---|---|
96-
| 🔴 | Long prompts: first token about 3× slower | Read the prompt in batches (task CPU.2) | `Qwen2CpuExecutor.Prefill` |
116+
| 🔴 | Older long-prompt run: first token about 3× slower | Rerun the same matrix on the new batched-prefill path | `performance.yml` |
97117
| 🔴 | No GPU | Metal kernels | not started |
98-
| 🟡 | GitHub still measures the old scalar build | Commit the new kernels, rerun the performance workflow | `.github/workflows/performance.yml` |
118+
| 🟡 | Hosted evidence for the new kernels is pending | Complete and review the current performance workflow | `.github/workflows/performance.yml` |
99119
| 🟡 | Takes token IDs, not text | Tokenizer and chat templates | not started |
100120
| 🟡 | Only Qwen2 runs | More model families | `src/Synapse.Runtime/Features/TextGeneration` |
101121
| 🟢 | Speed, memory, startup | Confirm with the 30-run release gate | `experiments/Synapse.ReferenceBenchmarks` |

‎benchmarks/README.md‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ runs A–K.
6363
Each model and scenario ran in a fresh process. The model stayed loaded for
6464
1 warm-up and 3 measured requests; every request was a new chat session with
6565
the full transcript. Context was capped at 1,024 tokens (see below). Greedy
66-
decoding, answer quality not reviewed. AC power; other desktop apps were open
67-
(load average about 3.4).
66+
decoding. AC power; other desktop apps were open (load average about 3.4).
67+
The raw schema still says `quality_unreviewed`; the manual review below was
68+
performed afterward from the preserved text and reasoning fields and does not
69+
retroactively change measurement eligibility.
6870

6971
128-token answer (`capitals-single-long`), median of 3:
7072

@@ -100,6 +102,29 @@ turn 3, median of 3:
100102
- Prompt token counts differ because each model has its own tokenizer and
101103
chat template.
102104

105+
### Manual output review
106+
107+
No model/device variant fully satisfied the 128-token instruction. This makes
108+
the throughput table useful for runtime diagnosis, not for a quality-adjusted
109+
model ranking.
110+
111+
| Variant | Review of the deterministic measured output |
112+
|---|---|
113+
| Qwen2.5 0.5B CPU | Incorrect and incomplete: it names all three capitals, but calls Fort Knox the federal headquarters and misidentifies Paris landmarks; it hits the 128-token cap before the recap. |
114+
| Qwen2.5 0.5B WebGPU | Mostly factual but incomplete: it stops at 99 tokens without the requested three-line recap. |
115+
| Qwen3 0.6B CPU | Incorrect and incomplete: all 128 tokens are reasoning, it emits no final answer, and the reasoning calls Paris, London, and Madrid France's capitals. |
116+
| Phi-3.5-mini CPU | Incorrect and incomplete: it corrupts Washington, D.C., places Mount Rushmore near it, and reaches the cap during the recap. |
117+
| Phi-4-mini CPU | The three main country-capital sections are substantially correct, but the generated recap is malformed and truncated at the cap. |
118+
| Mistral 7B v0.2 CPU | The France section is correct, but the output reaches the cap during the United States section before covering the United Kingdom or recap. |
119+
| DeepSeek-R1 7B CPU | Incomplete: all 128 tokens are reasoning and no final answer is emitted, although the reasoning recalls the three capitals. |
120+
121+
The three-turn outputs have the same constraint: every CPU turn reaches its
122+
64-token cap. DeepSeek-R1 and Qwen3 remain reasoning-only. Phi-3.5, Phi-4, and
123+
Mistral contain mostly correct briefing content but truncate sections; the
124+
Qwen2.5 variants additionally contain errors such as placing Paris in the
125+
center or south of France and describing Washington transit as free. No
126+
instruction-compliance claim is made from those dialogue timings.
127+
103128
**Context cap.** Every Foundry package sets its context length to the model's
104129
full window, and ONNX Runtime GenAI reserves the KV memory for that whole
105130
window at the first request. With the package default, Qwen3 0.6B reached

‎docs/ADR/ADR-005-benchmark-timing-provenance.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ invalid. A missing platform metric remains null with its scope recorded.
3737

3838
Functional CI and performance evidence are separate workflows. Functional
3939
TUnit checks can validate rendering against recorded raw evidence, but do not
40-
run the measured matrix or assert throughput. The manually
41-
dispatched performance workflow runs its own macOS/Linux/Windows matrix and
42-
retains every measured round as an artifact with a descriptive report. An
43-
output mismatch fails the job after writing the report, while the raw artifact
44-
is still uploaded. Hosted
45-
runners are variable hardware, so their results are never pooled across OSes
46-
or compared to the owner's Mac as a release winner verdict.
40+
run the measured matrix or assert throughput. The performance workflow runs
41+
automatically for every push to `main` and also supports manual dispatch. It
42+
runs its own macOS/Linux/Windows matrix and retains every measured round as an
43+
artifact with a descriptive report. An output mismatch fails the job after
44+
writing the report, while the raw artifact is still uploaded. Hosted runners
45+
are variable hardware, so their results are never pooled across OSes or
46+
compared to the owner's Mac as a release winner verdict.
4747

4848
## Consequences
4949

‎docs/Features/Benchmarking.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ the functional suite on macOS ARM64, Ubuntu x64, and Windows x64; it does not
6161
run the measured matrix or enforce speed thresholds. Existing recorded JSON
6262
tests report formatting and mismatch rejection without running performance
6363
samples inside TUnit.
64-
`performance.yml` is a separate manually dispatched three-OS CPU matrix. It
65-
fetches only the pinned Qwen smoke GGUF, builds the pinned competitors, runs
64+
`performance.yml` is a separate three-OS CPU matrix that runs on every push
65+
to `main` and supports manual dispatch. It fetches only the pinned Qwen smoke
66+
GGUF, builds the pinned competitors, runs
6667
three warm-ups plus five measured fresh-process rounds with a two-thread cap,
6768
and uploads unmodified per-round JSON for each runner. The C# `report --input
6869
<raw.json> [--summary <path>] [--require-quality]` command renders workload, model digest,
@@ -197,8 +198,13 @@ cohorts. Weights are unchanged.
197198

198199
Limits: the SDK does not expose ONNX Runtime thread settings, so this cohort
199200
is not thread-capped. Its weights and quantization differ from GGUF Q8_0 and
200-
MLX 8-bit, so it is never ranked against those cohorts. Answer quality is
201-
unreviewed, and reasoning text is stored apart from answer text.
201+
MLX 8-bit, so it is never ranked against those cohorts. Raw evidence retains
202+
the original `quality_unreviewed` status and stores reasoning apart from
203+
answer text. A later manual documentation review found that none of the seven
204+
local model/device variants fully completed the 128-token instruction: two
205+
were reasoning-only, two contained material factual errors, and the remaining
206+
three omitted or truncated required sections. That review is descriptive and
207+
does not make the run eligible for a quality-adjusted verdict.
202208

203209
### Requirements
204210

‎synapse.brainstorm.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,16 @@ Date: 2026-09-27. Status: accepted for the bootstrap slice.
138138
model, explicit download, raw JSON per job.
139139
- Rejected: reusing one session per scenario (hidden KV reuse), the OpenAI
140140
client (no TTFT), and adding the SDK to the existing runner (RID leak).
141+
142+
## 2026-09-28 public benchmark interpretation
143+
144+
- Scope every public CPU claim to the preserved eight-token diagnostic: the
145+
native backend reached 102% of llama.cpp at two threads and 114% at eight,
146+
with exact eight-token parity. This is not a long-answer or 30-pair verdict.
147+
- Treat the existing 274-token first-token gap as evidence from the older
148+
pre-batched-prefill build. The implementation changed; only a fresh run can
149+
confirm whether the bottleneck moved.
150+
- Review Foundry output separately from throughput. The manual review found
151+
reasoning-only, factually wrong, and truncated answers; none of the seven
152+
model/device variants completed the 128-token instruction. Keep the raw
153+
evidence status unchanged and publish the review as a descriptive layer.

‎synapse.plan.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ files plus two package-default probes). The full .NET suite passed 189/189
133133
with the local dotLLM and Homebrew llama.cpp `b29c606e2` subjects. The 15
134134
hosted Foundry jobs and the `verify.yml` anchor fetch have not run.
135135

136+
Public-results checkpoint (2026-09-28): README and benchmark documentation
137+
claims are now scoped to the raw evidence. The native CPU result is presented as an
138+
eight-token diagnostic (102% of llama.cpp at two threads, 114% at eight), the
139+
older long-prompt gap is not attributed to the new batched-prefill build, and
140+
the manually reviewed Foundry outputs are reported as quality-ineligible: no
141+
model/device variant fully completed the 128-token instruction. Performance
142+
diagnostics now trigger on every push to `main` as well as manual dispatch.
143+
This does not close `TASK-BMK-001`, `TASK-BMK-002`, or any CPU task; fresh
144+
hosted evidence is still required.
145+
136146
Local direct-native evidence: pinned Homebrew llama.cpp `b29c606e2` reproduced
137147
the Qwen prompt token IDs and continuation, three focused real-process native
138148
regressions passed, and raw 8/128-token samples plus a separate `llama-bench`

‎tests/Synapse.IntegrationTests/Features/Benchmarking/FoundryLocalPlanTests.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ public async Task WorkflowsIsolateFoundryLocalJobs()
7777
var performance = await File.ReadAllTextAsync(Path.Combine(workflows, "performance.yml"));
7878
var verify = await File.ReadAllTextAsync(Path.Combine(workflows, "verify.yml"));
7979

80+
await Assert.That(performance).Contains("push:\n branches: [main]");
81+
await Assert.That(performance).Contains("workflow_dispatch:");
8082
await Assert.That(performance).Contains("foundry-local-plan:");
8183
await Assert.That(performance).Contains("matrix: ${{ fromJSON(needs.foundry-local-plan.outputs.matrix) }}");
8284
await Assert.That(performance).Contains("runs-on: ${{ matrix.runner }}");

0 commit comments

Comments
 (0)