ahkamboh commited on
Commit
eb6136a
·
verified ·
1 Parent(s): ef37586

Add comparison with other models, speed charts and graphs

Browse files
.gitattributes CHANGED
@@ -35,3 +35,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  gguf/tiny-search-reader-60M-Q4_K_M.gguf filter=lfs diff=lfs merge=lfs -text
37
  gguf/tiny-search-reader-60M-Q8_0.gguf filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  gguf/tiny-search-reader-60M-Q4_K_M.gguf filter=lfs diff=lfs merge=lfs -text
37
  gguf/tiny-search-reader-60M-Q8_0.gguf filter=lfs diff=lfs merge=lfs -text
38
+ assets/accuracy_vs_size.png filter=lfs diff=lfs merge=lfs -text
39
+ assets/speed.png filter=lfs diff=lfs merge=lfs -text
40
+ assets/trick_questions.png filter=lfs diff=lfs merge=lfs -text
41
+ assets/versions.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -24,6 +24,12 @@ It does not search and it is not a chatbot. Your app runs the search and passes
24
 
25
  On 258 real Google questions it is right 79.8% of the time (206/258). About one answer in five is wrong.
26
 
 
 
 
 
 
 
27
  ## What it does
28
 
29
  Made-up results, real output (the demo in `searchreader.py`):
@@ -47,6 +53,7 @@ result names a recent winner. On the tests it catches 41 of 50 trick questions (
47
  | `gguf/tiny-search-reader-60M-Q4_K_M.gguf` | 41.0 MB | for llama.cpp; smaller, slightly different answers |
48
  | `searchreader.py` | | prompt builder and answer helpers for transformers and llama-cpp-python |
49
  | `eval/` | | the test questions, gold answers, this model's answers, grading code and full results |
 
50
  | `LICENSE` | | Apache-2.0 |
51
 
52
  ## How to run
@@ -210,6 +217,8 @@ Earlier versions from this project, same tests (Colab GPU runs, bf16):
210
  | v3a | 77.1% (199) | 74.7% | 65.0% |
211
  | v3.1a | 79.8% (206) | 75.9% | 61.7% |
212
 
 
 
213
  Paired tests (McNemar): against v2a, v3.1a fixes 17 google_big questions and breaks 6 (p=0.035), and on the "latest"
214
  questions fixes 13 and breaks 1 (p=0.002). Against v3a it fixes 22 and breaks 15 of the 258 (p=0.32, CPU fp32), so
215
  that gain is within noise.
@@ -239,6 +248,63 @@ Caveats:
239
  - About a quarter of the test questions (65 of 258) are about Pakistan.
240
  - Nothing was measured on a phone.
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  ## Phone files
243
 
244
  | file | size | all 258 (llama.cpp, CPU) | answers same as fp32 | 449-token prompt + 24 tokens | average test question | peak RSS |
@@ -252,6 +318,40 @@ roughly 0.5 to 0.9 s per question; that is an estimate, not a measurement. Q8_0
252
  difference matters: it gave the same answers under Homebrew llama.cpp and llama-cpp-python 0.3.36, while Q4_K_M
253
  changed 4 of 258 answers between the two.
254
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  ## Training
256
 
257
  ### Architecture
@@ -318,7 +418,8 @@ Synthetic categories:
318
  `not found` once and the other five times gave a nearby time or number, such as `8` for "How many books does the
319
  Harbor Street library have?" from a result saying the library is open from 8 am to 8 pm.
320
  - "Latest" and "current" answers depend on the dates in the results. The model does not know today's date; it picks
321
- the result that looks newest. Latest-version (6/14) and most-recent-winner (14/26) questions are its weakest kinds.
 
322
  - 512 tokens of context, about five Google-length results.
323
  - No chat and no instructions: one question in, a few words out, in the prompt format above only.
324
 
 
24
 
25
  On 258 real Google questions it is right 79.8% of the time (206/258). About one answer in five is wrong.
26
 
27
+ That is level with Qwen3-0.6B (207/258), a model ten times its size, and on a laptop CPU it answers about 5 times
28
+ faster. A SmolLM2-135M fine-tuned on the same training data scores a little higher (214/258). Details are in
29
+ [Compared with other models](#compared-with-other-models).
30
+
31
+ ![Accuracy against model size](https://huggingface.co/ahkamboh/tiny-search-reader-60M/resolve/main/assets/accuracy_vs_size.png)
32
+
33
  ## What it does
34
 
35
  Made-up results, real output (the demo in `searchreader.py`):
 
53
  | `gguf/tiny-search-reader-60M-Q4_K_M.gguf` | 41.0 MB | for llama.cpp; smaller, slightly different answers |
54
  | `searchreader.py` | | prompt builder and answer helpers for transformers and llama-cpp-python |
55
  | `eval/` | | the test questions, gold answers, this model's answers, grading code and full results |
56
+ | `assets/` | | the charts on this page and `make_charts.py`, which draws them |
57
  | `LICENSE` | | Apache-2.0 |
58
 
59
  ## How to run
 
217
  | v3a | 77.1% (199) | 74.7% | 65.0% |
218
  | v3.1a | 79.8% (206) | 75.9% | 61.7% |
219
 
220
+ ![Accuracy of each version](https://huggingface.co/ahkamboh/tiny-search-reader-60M/resolve/main/assets/versions.png)
221
+
222
  Paired tests (McNemar): against v2a, v3.1a fixes 17 google_big questions and breaks 6 (p=0.035), and on the "latest"
223
  questions fixes 13 and breaks 1 (p=0.002). Against v3a it fixes 22 and breaks 15 of the 258 (p=0.32, CPU fp32), so
224
  that gain is within noise.
 
248
  - About a quarter of the test questions (65 of 258) are about Pakistan.
249
  - Nothing was measured on a phone.
250
 
251
+ ## Compared with other models
252
+
253
+ Same 258 questions and the same grading, all on Colab GPU in bf16 with greedy decoding.
254
+
255
+ - Zero-shot models got the question and the numbered results in their own chat template (Qwen3 with thinking off),
256
+ with one instruction: "Answer the question using only the numbered search results below. Reply with just the
257
+ answer in a few words. If the results do not answer the question, reply exactly: not found".
258
+ - The two extractive models (deepset's SQuAD 2.0 readers) pick an answer span from the five results.
259
+ - SmolLM2-135M (fine-tuned) is the base SmolLM2-135M trained for 2 epochs on exactly the rows this model was
260
+ trained on.
261
+
262
+ The last column compares each model with this one question by question: questions only that model got right
263
+ against questions only this model got right, with the exact McNemar test (p below 0.05 means a real difference).
264
+
265
+ | model | how it was run | parameters | all 258 | trick questions caught (of 50) | "latest" questions (of 60) | only it right vs only this model right |
266
+ |---|---|---:|---:|---:|---:|---|
267
+ | Qwen3-4B | zero-shot | 4.02B | 91.5% (236) | 44 | 52 | 42 vs 12, p<0.001 |
268
+ | Qwen3-1.7B | zero-shot | 1.72B | 84.5% (218) | 26 | 54 | 39 vs 27, p=0.175 |
269
+ | SmolLM2-135M, fine-tuned on this model's data | fine-tuned | 135M | 82.9% (214) | 32 | 47 | 33 vs 25, p=0.358 |
270
+ | Qwen3-0.6B | zero-shot | 596M | 80.2% (207) | 30 | 45 | 37 vs 36, p=1.000 |
271
+ | **tiny-search-reader-60M** | trained for this task | 62M | **79.8% (206)** | **41** | 37 | |
272
+ | RoBERTa-base SQuAD2 | extractive | 124M | 71.3% (184) | 29 | 39 | 26 vs 48, p=0.014 |
273
+ | MiniLM SQuAD2 | extractive | 33M | 69.4% (179) | 27 | 35 | 22 vs 49, p=0.002 |
274
+ | SmolLM2-360M-Instruct | zero-shot | 362M | 65.9% (170) | 0 | 44 | 25 vs 61, p<0.001 |
275
+ | Qwen2.5-0.5B-Instruct | zero-shot | 494M | 65.1% (168) | 0 | 40 | 26 vs 64, p<0.001 |
276
+ | SmolLM2-135M-Instruct | zero-shot | 135M | 59.7% (154) | 0 | 40 | 21 vs 73, p<0.001 |
277
+
278
+ What the table shows:
279
+
280
+ - It is clearly better than both SQuAD 2.0 readers and the three instruct models below 0.6B. It ties Qwen3-0.6B
281
+ (37 questions against 36). Qwen3-1.7B gets 12 more right, which is not a significant gap on 258 questions.
282
+ Qwen3-4B, 65 times larger, is clearly better.
283
+ - It catches 41 of the 50 trick questions, more than every model here except Qwen3-4B (44). The three instruct
284
+ models below 0.6B never replied `not found`.
285
+ - It is weak on the 60 "latest / current / most recent" questions: 37 right, fewer than every generative model
286
+ tested. Qwen3-1.7B (54) and Qwen3-4B (52) are significantly better there (p of 0.001 or less).
287
+ - SmolLM2-135M fine-tuned on the same rows scores 214 against 206. That gap is not significant (p=0.358), but on the
288
+ latest questions it is (47 against 37, p=0.021), and it catches fewer trick questions (32 against 41). It has 2.2
289
+ times the parameters, and its base was pretrained on about 2 trillion tokens, against 10 billion here. So most of
290
+ the result comes from the training data, and a base model with more pretraining makes better use of it.
291
+
292
+ ![Trick questions caught](https://huggingface.co/ahkamboh/tiny-search-reader-60M/resolve/main/assets/trick_questions.png)
293
+
294
+ ![Accuracy by question kind](https://huggingface.co/ahkamboh/tiny-search-reader-60M/resolve/main/assets/by_kind.png)
295
+
296
+ Caveats for this comparison:
297
+
298
+ - The zero-shot models were pretrained on the open web and may know some answers without reading the results.
299
+ This model's training held out every test question and topic.
300
+ - One fixed prompt was used for every zero-shot model. Another prompt could raise or lower their scores.
301
+ - The Qwen and SmolLM2 model cards suggest sampling; greedy decoding was used for every model so the run is
302
+ repeatable.
303
+ - google25 is this model's dev set, and google_fresh and google_big guided the v3.1 data (see the caveats above),
304
+ which favours this model a little.
305
+
306
+ Per-set scores, google_big by kind and every paired test are in [`eval/results.md`](eval/results.md).
307
+
308
  ## Phone files
309
 
310
  | file | size | all 258 (llama.cpp, CPU) | answers same as fp32 | 449-token prompt + 24 tokens | average test question | peak RSS |
 
318
  difference matters: it gave the same answers under Homebrew llama.cpp and llama-cpp-python 0.3.36, while Q4_K_M
319
  changed 4 of 258 answers between the two.
320
 
321
+ ### Speed in tokens per second
322
+
323
+ Measured with llama.cpp on an Apple M1 Pro, CPU only (no GPU, no Metal), on a real 449-token test prompt plus a
324
+ 24-token answer, median of 5 runs. "Reading" is the prompt pass, "writing" is generating the answer.
325
+
326
+ | file | threads | reading (tokens/s) | writing (tokens/s) | 449-token prompt + 24-token answer |
327
+ |---|---:|---:|---:|---:|
328
+ | `tiny-search-reader-60M-Q8_0.gguf` | 1 | 558 | 135 | 982 ms |
329
+ | `tiny-search-reader-60M-Q8_0.gguf` | 2 | 1,100 | 210 | 522 ms |
330
+ | `tiny-search-reader-60M-Q8_0.gguf` | 4 | 1,884 | 305 | 317 ms |
331
+ | `tiny-search-reader-60M-Q4_K_M.gguf` | 1 | 455 | 154 | 1,142 ms |
332
+ | `tiny-search-reader-60M-Q4_K_M.gguf` | 2 | 894 | 244 | 601 ms |
333
+ | `tiny-search-reader-60M-Q4_K_M.gguf` | 4 | 1,559 | 362 | 354 ms |
334
+
335
+ A typical test question has about 317 prompt tokens and a 5-token answer, so most of the time goes into reading the
336
+ results: 168 ms per question with Q8_0 at 4 threads. These numbers come from Homebrew's prebuilt llama.cpp; in one
337
+ check a llama.cpp built on this machine was about twice as fast, so treat them as a floor for this Mac.
338
+
339
+ ### Compared with Qwen3-0.6B
340
+
341
+ Same Mac, same llama.cpp build (Homebrew build 11146), `llama-bench -p 449 -n 24 -r 5 -ngl 0` (all layers on the CPU),
342
+ both files Q8_0. llama-bench measures writing from an empty context, so its writing numbers are higher than in the
343
+ table above, where the answer is written after the 449-token prompt.
344
+
345
+ | model | file | parameters | reading (tokens/s), 1 / 4 threads | writing (tokens/s), 1 / 4 threads | 449-token prompt + 24-token answer, 4 threads |
346
+ |---|---:|---:|---:|---:|---:|
347
+ | tiny-search-reader-60M | 70 MB | 62M | 575 / 1,900 | 310 / 561 | 0.28 s |
348
+ | Qwen3-0.6B | 634 MB | 596M | 89 / 340 | 82 / 132 | 1.50 s |
349
+
350
+ On this machine it answers about 5 times faster than Qwen3-0.6B and its file is 9 times smaller. Accuracy against
351
+ Qwen3-0.6B is in [Compared with other models](#compared-with-other-models).
352
+
353
+ ![Speed against Qwen3-0.6B](https://huggingface.co/ahkamboh/tiny-search-reader-60M/resolve/main/assets/speed.png)
354
+
355
  ## Training
356
 
357
  ### Architecture
 
418
  `not found` once and the other five times gave a nearby time or number, such as `8` for "How many books does the
419
  Harbor Street library have?" from a result saying the library is open from 8 am to 8 pm.
420
  - "Latest" and "current" answers depend on the dates in the results. The model does not know today's date; it picks
421
+ the result that looks newest. Latest-version (6/14) and most-recent-winner (14/26) questions are its weakest kinds, and every other
422
+ generative model tested did better on the 60 "latest" questions.
423
  - 512 tokens of context, about five Google-length results.
424
  - No chat and no instructions: one question in, a few words out, in the prompt format above only.
425
 
assets/accuracy_vs_size.png ADDED

Git LFS Details

  • SHA256: 337802815aebcb73953fd27c875ab23444e5d7486fc197e2cbef8cef7e21568f
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
assets/by_kind.png ADDED
assets/make_charts.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model-card charts for tiny-search-reader-60M, in the style of the Hugging Face blog charts: warm page, bold rounded
2
+ title, grey subtitle, a white rounded card with a soft shadow, a mono source line. Every number is copied from
3
+ runs/baselines-summary.txt (4 Oct 2026) or the README tables; nothing is computed here except percentages."""
4
+ import os
5
+ import sys
6
+
7
+ import matplotlib
8
+ matplotlib.use("Agg")
9
+ import matplotlib.pyplot as plt
10
+ from matplotlib import font_manager as fm
11
+ from matplotlib.lines import Line2D
12
+ from matplotlib.patches import FancyBboxPatch
13
+ from matplotlib.ticker import FixedLocator, FuncFormatter, NullLocator
14
+
15
+ OUT = sys.argv[1] if len(sys.argv) > 1 else "assets"
16
+ os.makedirs(OUT, exist_ok=True)
17
+
18
+ TITLE = fm.FontProperties(fname="/System/Library/Fonts/Supplemental/Arial Rounded Bold.ttf")
19
+ for f in ("/System/Library/Fonts/Avenir Next.ttc", "/System/Library/Fonts/Menlo.ttc"):
20
+ fm.fontManager.addfont(f)
21
+ BODY, MONO = "Avenir Next", "Menlo"
22
+ plt.rcParams.update({"font.family": BODY, "font.size": 11, "axes.unicode_minus": False})
23
+
24
+ PAGE, CARD, EDGE = "#F7F5EF", "#FFFFFF", "#E6E1D6"
25
+ INK, GREY, LIGHT, GRID = "#1F1D1A", "#77726A", "#B9B3A8", "#EEEAE2"
26
+ OURS, OURS_SOFT = "#FF9D00", "#FFD9A0"
27
+ QWEN, OTHER, EXTRACT = "#5B6B8C", "#A9A396", "#8FA7A0"
28
+ W, H, DPI = 10.0, 6.0, 170
29
+ SOURCE = "source: 258 real Google questions with their top results · greedy decoding · 4 Oct 2026"
30
+
31
+
32
+ def card_figure(title, subtitle, source=SOURCE, legend=None):
33
+ """Page + title + card. Returns (fig, card box in figure fractions: x0, y0, x1, y1)."""
34
+ fig = plt.figure(figsize=(W, H), dpi=DPI, facecolor=PAGE)
35
+ fig.text(0.5 / W, 1 - 0.42 / H, title, fontproperties=TITLE, fontsize=21, color=INK, va="top")
36
+ fig.text(0.5 / W, 1 - 0.92 / H, subtitle, fontsize=12.5, color=GREY, va="top")
37
+ x0, y0, x1, y1 = 0.42, 0.62, W - 0.42, H - 1.42 # inches
38
+ t = fig.dpi_scale_trans
39
+ for i, a in enumerate((0.035, 0.025, 0.015)): # soft shadow, three layers
40
+ g = 0.03 + 0.03 * i
41
+ fig.patches.append(FancyBboxPatch((x0 - g / 2, y0 - 0.06 - g / 2), x1 - x0 + g, y1 - y0 + g,
42
+ boxstyle="round,pad=0,rounding_size=0.2", transform=t,
43
+ facecolor="#3A2E1A", edgecolor="none", alpha=a, zorder=-3))
44
+ fig.patches.append(FancyBboxPatch((x0, y0), x1 - x0, y1 - y0, boxstyle="round,pad=0,rounding_size=0.18",
45
+ transform=t, facecolor=CARD, edgecolor=EDGE, linewidth=1.1, zorder=-2))
46
+ fig.text(0.5 / W, 0.24 / H, source, fontfamily=MONO, fontsize=8.6, color=GREY, va="center")
47
+ if legend:
48
+ x = x1 - 0.25
49
+ for label, color, marker in reversed(legend):
50
+ tx = fig.text(x / W, (y1 - 0.32) / H, label, fontsize=10.5, color=INK, ha="right", va="center")
51
+ fig.canvas.draw()
52
+ wpx = tx.get_window_extent().width / DPI
53
+ m = {"●": "o", "◆": "D", "■": "s"}[marker]
54
+ fig.lines.append(Line2D([(x - wpx - 0.17) / W], [(y1 - 0.32) / H], transform=fig.transFigure,
55
+ marker=m, markersize=8 if m != "D" else 7, linestyle="none",
56
+ markerfacecolor="white" if m == "D" else color, markeredgecolor=color,
57
+ markeredgewidth=1.8 if m == "D" else 0))
58
+ x -= wpx + 0.62
59
+ return fig, (x0 / W, y0 / H, x1 / W, y1 / H)
60
+
61
+
62
+ def style(ax, grid="y"):
63
+ ax.set_facecolor("none")
64
+ for s in ("top", "right", "left"):
65
+ ax.spines[s].set_visible(False)
66
+ ax.spines["bottom"].set_color(LIGHT)
67
+ ax.tick_params(colors=GREY, length=0, labelsize=10.5)
68
+ if grid:
69
+ ax.grid(axis=grid, color=GRID, linewidth=1)
70
+ ax.set_axisbelow(True)
71
+
72
+
73
+ def save(fig, name):
74
+ path = os.path.join(OUT, name)
75
+ fig.savefig(path, dpi=DPI, facecolor=PAGE)
76
+ plt.close(fig)
77
+ print(path)
78
+
79
+
80
+ # ---------- 1. accuracy against size ----------
81
+ MODELS = [ # name, params (M), right of 258, traps caught of 50, kind
82
+ ("tiny-search-reader-60M", 62.0, 206, 41, "ours"),
83
+ ("SmolLM2-135M fine-tuned\non the same data", 134.5, 214, 32, "ft"),
84
+ ("SmolLM2-135M-Instruct", 134.5, 154, 0, "zs"),
85
+ ("SmolLM2-360M-Instruct", 361.8, 170, 0, "zs"),
86
+ ("Qwen2.5-0.5B-Instruct", 494.0, 168, 0, "zs"),
87
+ ("Qwen3-0.6B", 596.0, 207, 30, "qwen"),
88
+ ("Qwen3-1.7B", 1720.6, 218, 26, "qwen"),
89
+ ("Qwen3-4B", 4022.5, 236, 44, "qwen"),
90
+ ("MiniLM SQuAD2", 33.2, 179, 27, "ex"),
91
+ ("RoBERTa-base SQuAD2", 124.1, 184, 29, "ex"),
92
+ ]
93
+ pct = lambda n, d=258: 100 * n / d
94
+
95
+ fig, (cx0, cy0, cx1, cy1) = card_figure(
96
+ "Qwen3-0.6B accuracy at a tenth of the size",
97
+ "Right answers on 258 real Google questions, given the question and its search results",
98
+ legend=[("this model", OURS, "●"), ("trained on the same data", OURS, "◆"),
99
+ ("zero-shot LLM", QWEN, "●"), ("extractive QA", EXTRACT, "■")])
100
+ ax = fig.add_axes([cx0 + 0.075, cy0 + 0.12, cx1 - cx0 - 0.11, cy1 - cy0 - 0.25])
101
+ style(ax, grid="both")
102
+ ax.set_xscale("log")
103
+ ax.set_xlim(24, 6500)
104
+ ax.set_ylim(55, 95)
105
+ ax.xaxis.set_major_locator(FixedLocator([30, 60, 100, 300, 600, 1000, 3000]))
106
+ ax.xaxis.set_minor_locator(NullLocator())
107
+ ax.xaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v / 1000:g}B" if v >= 1000 else f"{v:g}M"))
108
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v:g}%"))
109
+ ax.set_xlabel("parameters (log scale)", color=GREY, fontsize=10.5, labelpad=6)
110
+ q = [m for m in MODELS if m[4] == "qwen"]
111
+ ax.plot([m[1] for m in q], [pct(m[2]) for m in q], color=QWEN, alpha=0.35, linewidth=2, zorder=2)
112
+ LABEL_POS = { # offsets in points, alignment
113
+ "tiny-search-reader-60M": ((0, 17), "center"), "SmolLM2-135M fine-tuned\non the same data": ((10, 4), "left"),
114
+ "SmolLM2-135M-Instruct": ((9, -3), "left"), "SmolLM2-360M-Instruct": ((8, 6), "left"),
115
+ "Qwen2.5-0.5B-Instruct": ((8, -12), "left"), "Qwen3-0.6B": ((9, -8), "left"), "Qwen3-1.7B": ((-9, 7), "right"),
116
+ "Qwen3-4B": ((-9, 6), "right"), "MiniLM SQuAD2": ((9, -4), "left"), "RoBERTa-base SQuAD2": ((9, 2), "left"),
117
+ }
118
+ for name, p, right, _, kind in MODELS:
119
+ y = pct(right)
120
+ if kind == "ours":
121
+ ax.scatter(p, y, s=340, color=OURS, alpha=0.25, zorder=3, linewidths=0)
122
+ ax.scatter(p, y, s=120, color=OURS, zorder=4, edgecolors="white", linewidths=1.5)
123
+ elif kind == "ft":
124
+ ax.scatter(p, y, s=110, marker="D", color="white", edgecolors=OURS, linewidths=2, zorder=4)
125
+ elif kind == "ex":
126
+ ax.scatter(p, y, s=80, marker="s", color=EXTRACT, zorder=4, edgecolors="white", linewidths=1)
127
+ else:
128
+ ax.scatter(p, y, s=85, color=QWEN if kind == "qwen" else OTHER, zorder=4, edgecolors="white", linewidths=1)
129
+ (dx, dy), ha = LABEL_POS[name]
130
+ shown = "this model" if kind == "ours" else name
131
+ ax.annotate(f"{shown} {y:.1f}%", (p, y), xytext=(dx, dy), textcoords="offset points", ha=ha, va="center",
132
+ fontsize=10 if kind != "ours" else 11, color=INK if kind in ("ours", "ft") else GREY,
133
+ fontweight="bold" if kind == "ours" else "normal")
134
+ ax.annotate("", xy=(560, pct(207)), xytext=(70, pct(206)), arrowprops=dict(arrowstyle="-", color=OURS,
135
+ linestyle=(0, (3, 3)), linewidth=1.3), zorder=2)
136
+ ax.text(230, pct(206) - 1.0, "same score, 10x fewer parameters", color="#C77700", fontsize=10, ha="center",
137
+ va="top")
138
+ save(fig, "accuracy_vs_size.png")
139
+
140
+ # ---------- 2. trick questions ----------
141
+ traps = sorted(MODELS, key=lambda m: (m[3], m[2]))
142
+ fig, (cx0, cy0, cx1, cy1) = card_figure(
143
+ "Says \"not found\" when the answer is not there",
144
+ "Trick questions caught, out of 50: the search results do not hold the answer")
145
+ ax = fig.add_axes([cx0 + 0.27, cy0 + 0.06, cx1 - cx0 - 0.33, cy1 - cy0 - 0.11])
146
+ style(ax, grid="x")
147
+ ax.spines["bottom"].set_visible(False)
148
+ names = ["SmolLM2-135M fine-tuned (same data)" if m[4] == "ft" else m[0] for m in traps]
149
+ colors = [OURS if m[4] == "ours" else OURS_SOFT if m[4] == "ft" else QWEN if m[4] == "qwen"
150
+ else EXTRACT if m[4] == "ex" else OTHER for m in traps]
151
+ bars = ax.barh(range(len(traps)), [m[3] for m in traps], color=colors, height=0.62, zorder=3)
152
+ ax.set_yticks(range(len(traps)), names, fontsize=10.5, color=INK)
153
+ for t, m in zip(ax.get_yticklabels(), traps):
154
+ if m[4] == "ours":
155
+ t.set_fontweight("bold")
156
+ ax.set_xlim(0, 50)
157
+ ax.xaxis.set_major_locator(FixedLocator([0, 10, 20, 30, 40, 50]))
158
+ for i, m in enumerate(traps):
159
+ label = f"{m[3]}" if m[3] else "0 (always answers something)"
160
+ ax.text(m[3] + 0.7, i, label, va="center", fontsize=10.5, color=INK if m[3] else GREY,
161
+ fontweight="bold" if m[4] == "ours" else "normal")
162
+ save(fig, "trick_questions.png")
163
+
164
+ # ---------- 3. speed ----------
165
+ fig, (cx0, cy0, cx1, cy1) = card_figure(
166
+ "About 5x faster than Qwen3-0.6B, from a 9x smaller file",
167
+ "llama.cpp on an Apple M1 Pro, CPU only, 4 threads, both files Q8_0",
168
+ source="source: llama-bench -p 449 -n 24 -r 5 -ngl 0 · Homebrew llama.cpp build 11146 · 4 Oct 2026",
169
+ legend=[("tiny-search-reader-60M", OURS, "■"), ("Qwen3-0.6B", QWEN, "■")])
170
+ PANELS = [("reading the results", "tokens per second, higher is better", 1900, 340, "{:,.0f}"),
171
+ ("writing the answer", "tokens per second, higher is better", 561, 132, "{:,.0f}"),
172
+ ("one question, 449 + 24 tokens", "seconds, lower is better", 0.28, 1.50, "{:.2f} s"),
173
+ ("model file", "megabytes, lower is better", 70, 634, "{:,.0f} MB")]
174
+ pw, ph = (cx1 - cx0 - 0.08) / 2, (cy1 - cy0 - 0.13) / 2
175
+ for i, (head, unit, ours, qwen, fmt) in enumerate(PANELS):
176
+ col, row = i % 2, i // 2
177
+ ax = fig.add_axes([cx0 + 0.04 + col * pw + 0.012, cy0 + 0.03 + (1 - row) * ph, pw - 0.05, ph - 0.03])
178
+ style(ax, grid=None)
179
+ ax.spines["bottom"].set_visible(False)
180
+ ax.set_xticks([])
181
+ ax.set_yticks([])
182
+ top = max(ours, qwen) * 1.32
183
+ ax.set_xlim(0, top)
184
+ ax.barh([1, 0], [ours, qwen], color=[OURS, QWEN], height=0.62)
185
+ for y, v in ((1, ours), (0, qwen)):
186
+ ax.text(v + top * 0.015, y, fmt.format(v), va="center", fontsize=12, color=INK,
187
+ fontweight="bold" if y == 1 else "normal")
188
+ ax.set_ylim(-0.5, 2.5)
189
+ ax.text(0, 2.2, head, fontsize=12.5, color=INK, fontweight="bold", va="center")
190
+ ax.text(0, 1.62, unit, fontsize=9.5, color=GREY, va="center")
191
+ save(fig, "speed.png")
192
+
193
+ # ---------- 4. versions ----------
194
+ fig, (cx0, cy0, cx1, cy1) = card_figure(
195
+ "Four versions, 69% to 80%",
196
+ "Each version of this model on the same 258 questions (Colab GPU runs, bf16)",
197
+ legend=[("all 258 questions", OURS, "●"), ("the 60 latest / current / most recent questions", QWEN, "●")])
198
+ ax = fig.add_axes([cx0 + 0.07, cy0 + 0.1, cx1 - cx0 - 0.2, cy1 - cy0 - 0.23])
199
+ style(ax)
200
+ xs = [0, 1, 2, 3]
201
+ ax.set_xticks(xs, ["v1.1a", "v2a", "v3a", "v3.1a\n(this release)"], fontsize=11, color=INK)
202
+ ax.set_xlim(-0.35, 3.35)
203
+ ax.set_ylim(30, 95)
204
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v:g}%"))
205
+ allv, latest = [69.4, 73.3, 77.1, 79.8], [41.7, 65.0, 61.7]
206
+ ax.plot(xs, allv, color=OURS, linewidth=3, marker="o", markersize=9, markeredgecolor="white", markeredgewidth=1.5,
207
+ zorder=4)
208
+ ax.plot(xs[1:], latest, color=QWEN, linewidth=3, marker="o", markersize=9, markeredgecolor="white",
209
+ markeredgewidth=1.5, zorder=4)
210
+ for x, v in zip(xs, allv):
211
+ ax.text(x, v - 4.6, f"{v:.1f}%", ha="center", fontsize=11, color=INK, fontweight="bold" if x == 3 else "normal")
212
+ for x, v in zip(xs[1:], latest):
213
+ ax.text(x, v - 5.2, f"{v:.1f}%", ha="center", fontsize=11, color=QWEN)
214
+ ax.axhline(80.2, color=LIGHT, linewidth=1.2, linestyle=(0, (4, 3)), zorder=2)
215
+ ax.text(-0.3, 80.9, "Qwen3-0.6B, zero-shot: 80.2%", ha="left", va="bottom", fontsize=9.5, color=GREY)
216
+ ax.text(0, 36, "not measured", ha="center", fontsize=9.5, color=LIGHT)
217
+ save(fig, "versions.png")
218
+
219
+ # ---------- 5. by kind ----------
220
+ KINDS = ["fact", "unit", "trick\n(\"not found\")", "live value", "latest\nversion", "most recent\nwinner"]
221
+ N = [34, 25, 28, 35, 14, 26]
222
+ ROWS = [("tiny-search-reader-60M", [30, 20, 24, 29, 6, 14], OURS),
223
+ ("SmolLM2-135M fine-tuned on the same data", [31, 23, 21, 32, 9, 18], OURS_SOFT),
224
+ ("Qwen3-0.6B, zero-shot", [31, 23, 19, 31, 11, 16], QWEN)]
225
+ fig, (cx0, cy0, cx1, cy1) = card_figure(
226
+ "Weakest on \"latest\" questions",
227
+ "google_big (162 questions) by kind, % right",
228
+ legend=[(n.split(",")[0].replace(" fine-tuned on the same data", " fine-tuned"), c, "■") for n, _, c in ROWS])
229
+ ax = fig.add_axes([cx0 + 0.07, cy0 + 0.12, cx1 - cx0 - 0.1, cy1 - cy0 - 0.25])
230
+ style(ax)
231
+ ax.set_ylim(0, 105)
232
+ ax.yaxis.set_major_locator(FixedLocator([0, 25, 50, 75, 100]))
233
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v:g}%"))
234
+ bw = 0.26
235
+ for j, (name, right, color) in enumerate(ROWS):
236
+ xs = [i + (j - 1) * bw for i in range(len(KINDS))]
237
+ vals = [100 * r / n for r, n in zip(right, N)]
238
+ ax.bar(xs, vals, width=bw * 0.92, color=color, zorder=3)
239
+ for x, v, r, n in zip(xs, vals, right, N):
240
+ ax.text(x, v + 1.5, f"{r}", ha="center", fontsize=8.8, color=INK if j == 0 else GREY,
241
+ fontweight="bold" if j == 0 else "normal")
242
+ ax.set_xticks(range(len(KINDS)), [f"{k}\nof {n}" for k, n in zip(KINDS, N)], fontsize=10, color=INK)
243
+ save(fig, "by_kind.png")
assets/speed.png ADDED

Git LFS Details

  • SHA256: d63d0bc67ef87dcae7d4b0384f7f722f55c444d2e43c61cb15a1faad7f1b1a86
  • Pointer size: 131 Bytes
  • Size of remote file: 120 kB
assets/trick_questions.png ADDED

Git LFS Details

  • SHA256: 47cd08ef1e8ef2324ed6f7332afaafa604f37d3f914feeb58221a50b9fd8da53
  • Pointer size: 131 Bytes
  • Size of remote file: 130 kB
assets/versions.png ADDED

Git LFS Details

  • SHA256: 3806127027f2e0c0a1376e4e3c72de19da58f5fa1756417ff176b01a822b7808
  • Pointer size: 131 Bytes
  • Size of remote file: 121 kB
eval/results.md CHANGED
@@ -105,6 +105,92 @@ v3a and v3.1a on CPU, PyTorch fp32:
105
 
106
  Both were part of the score used to pick v3.1a, so they are dev numbers.
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  ## Caveats
109
 
110
  - The sets are small. One question is 0.4 points of the 258 total and 0.6 points of google_big.
 
105
 
106
  Both were part of the score used to pick v3.1a, so they are dev numbers.
107
 
108
+ ## Other models on the same questions (Colab bf16, 4 Oct 2026)
109
+
110
+ Run on one RTX PRO 6000, bf16, greedy. v3.1a here is the Colab bf16 run (123/162 on google_big, against 124 on CPU
111
+ fp32), so every row is on the same footing.
112
+
113
+ - Zero-shot: one user message (no system message) in each model's chat template, Qwen3 with `enable_thinking=False`,
114
+ at most 32 new tokens. The message is the instruction "Answer the question using only the numbered search results
115
+ below. Reply with just the answer in a few words. If the results do not answer the question, reply exactly: not
116
+ found", then `Search results:` with the same numbered results v3.1a sees, then `Question:` and the question.
117
+ - Post-processing, the same for every generative model: keep the new tokens up to the first end-of-turn token, drop
118
+ a `<think>...</think>` block, keep the first non-empty line, strip spaces, quotes and one trailing period. A reply
119
+ that contains the words "not found" becomes exactly `not found`.
120
+ - Extractive: the five results as `title: text` lines are the context; the span the QA pipeline picks is the answer,
121
+ and an empty answer is `not found`.
122
+ - Fine-tune: HuggingFaceTB/SmolLM2-135M, 2 epochs on exactly v3.1a's training rows (258,542 examples), AdamW lr 1e-4,
123
+ batch 32, warmup 100 steps then linear decay, context 512, 27 minutes.
124
+
125
+ | model | kind | parameters | google25 | google_heldout | google_fresh | google_big | all 258 | google_big clean (144) | google_big latest (60) |
126
+ |---|---|---:|---:|---:|---:|---:|---:|---:|---:|
127
+ | v3.1a (this model) | trained for this task | 62.0M | 22/24 | 44/53 | 17/19 | 123/162 | 206 (79.8%) | 107 | 37 |
128
+ | Qwen3-4B | zero-shot | 4.02B | 21/24 | 50/53 | 17/19 | 148/162 | 236 (91.5%) | 132 | 52 |
129
+ | Qwen3-1.7B | zero-shot | 1.72B | 19/24 | 47/53 | 15/19 | 137/162 | 218 (84.5%) | 120 | 54 |
130
+ | SmolLM2-135M fine-tuned on v3.1a's rows | fine-tune | 134.5M | 20/24 | 45/53 | 15/19 | 134/162 | 214 (82.9%) | 116 | 47 |
131
+ | Qwen3-0.6B | zero-shot | 596.0M | 17/24 | 46/53 | 13/19 | 131/162 | 207 (80.2%) | 115 | 45 |
132
+ | deepset/roberta-base-squad2 | extractive | 124.1M | 15/24 | 42/53 | 14/19 | 113/162 | 184 (71.3%) | 98 | 39 |
133
+ | deepset/minilm-uncased-squad2 | extractive | 33.2M | 15/24 | 43/53 | 16/19 | 105/162 | 179 (69.4%) | 94 | 35 |
134
+ | SmolLM2-360M-Instruct | zero-shot | 361.8M | 14/24 | 40/53 | 8/19 | 108/162 | 170 (65.9%) | 94 | 44 |
135
+ | Qwen2.5-0.5B-Instruct | zero-shot | 494.0M | 18/24 | 39/53 | 7/19 | 104/162 | 168 (65.1%) | 90 | 40 |
136
+ | SmolLM2-135M-Instruct | zero-shot | 134.5M | 14/24 | 37/53 | 6/19 | 97/162 | 154 (59.7%) | 83 | 40 |
137
+
138
+ google_big clean leaves out the 18 google_big questions that v1.1's training rows ask word for word. google_big
139
+ latest is its 60 latest / current / most recent questions.
140
+
141
+ Verdicts over all 258 questions:
142
+
143
+ | model | answered correct (of 208) | missed (said `not found`) | wrong | trap caught (of 50) | trap fooled |
144
+ |---|---:|---:|---:|---:|---:|
145
+ | v3.1a | 165 | 12 | 31 | 41 | 9 |
146
+ | Qwen3-4B | 192 | 0 | 16 | 44 | 6 |
147
+ | Qwen3-1.7B | 192 | 0 | 16 | 26 | 24 |
148
+ | SmolLM2-135M fine-tuned | 182 | 4 | 22 | 32 | 18 |
149
+ | Qwen3-0.6B | 177 | 10 | 21 | 30 | 20 |
150
+ | roberta-base-squad2 | 155 | 22 | 31 | 29 | 21 |
151
+ | minilm-uncased-squad2 | 152 | 13 | 43 | 27 | 23 |
152
+ | SmolLM2-360M-Instruct | 170 | 0 | 38 | 0 | 50 |
153
+ | Qwen2.5-0.5B-Instruct | 168 | 0 | 40 | 0 | 50 |
154
+ | SmolLM2-135M-Instruct | 154 | 0 | 54 | 0 | 50 |
155
+
156
+ google_big by kind (right / questions):
157
+
158
+ | model | fact (34) | unit (25) | trick (28) | live (35) | version (14) | winner (26) |
159
+ |---|---:|---:|---:|---:|---:|---:|
160
+ | v3.1a | 30 | 20 | 24 | 29 | 6 | 14 |
161
+ | Qwen3-4B | 33 | 25 | 24 | 34 | 12 | 20 |
162
+ | Qwen3-1.7B | 33 | 23 | 13 | 34 | 12 | 22 |
163
+ | SmolLM2-135M fine-tuned | 31 | 23 | 21 | 32 | 9 | 18 |
164
+ | Qwen3-0.6B | 31 | 23 | 19 | 31 | 11 | 16 |
165
+ | roberta-base-squad2 | 26 | 19 | 16 | 29 | 8 | 15 |
166
+ | minilm-uncased-squad2 | 26 | 20 | 14 | 26 | 8 | 11 |
167
+ | SmolLM2-360M-Instruct | 32 | 20 | 0 | 30 | 8 | 18 |
168
+ | Qwen2.5-0.5B-Instruct | 31 | 21 | 0 | 31 | 8 | 13 |
169
+ | SmolLM2-135M-Instruct | 30 | 18 | 0 | 28 | 5 | 16 |
170
+
171
+ Paired against v3.1a, question by question. Each cell is "only v3.1a right / only the other model right, exact
172
+ McNemar p"; `*` marks p < 0.05.
173
+
174
+ | model | google25 | google_heldout | google_fresh | google_big | all 258 | google_big clean | google_big latest |
175
+ |---|---|---|---|---|---|---|---|
176
+ | Qwen3-4B | 2/1, 1.000 | 1/7, 0.070 | 2/2, 1.000 | 7/32, 0.000* | 12/42, 0.000* | 5/30, 0.000* | 2/17, 0.001* |
177
+ | Qwen3-1.7B | 4/1, 0.375 | 3/6, 0.508 | 4/2, 0.688 | 16/30, 0.054 | 27/39, 0.175 | 15/28, 0.066 | 0/17, 0.000* |
178
+ | SmolLM2-135M fine-tuned | 3/1, 0.625 | 5/6, 1.000 | 3/1, 0.625 | 14/25, 0.108 | 25/33, 0.358 | 14/23, 0.188 | 3/13, 0.021* |
179
+ | Qwen3-0.6B | 6/1, 0.125 | 5/7, 0.774 | 5/1, 0.219 | 20/28, 0.312 | 36/37, 1.000 | 18/26, 0.291 | 8/16, 0.152 |
180
+ | roberta-base-squad2 | 7/0, 0.016* | 7/5, 0.774 | 4/1, 0.375 | 30/20, 0.203 | 48/26, 0.014* | 27/18, 0.233 | 7/9, 0.804 |
181
+ | minilm-uncased-squad2 | 7/0, 0.016* | 6/5, 1.000 | 2/1, 1.000 | 34/16, 0.015* | 49/22, 0.002* | 28/15, 0.066 | 11/9, 0.824 |
182
+ | SmolLM2-360M-Instruct | 8/0, 0.008* | 8/4, 0.388 | 9/0, 0.004* | 36/21, 0.063 | 61/25, 0.000* | 32/19, 0.092 | 4/11, 0.118 |
183
+ | Qwen2.5-0.5B-Instruct | 5/1, 0.219 | 9/4, 0.267 | 10/0, 0.002* | 40/21, 0.020* | 64/26, 0.000* | 36/19, 0.030* | 7/10, 0.629 |
184
+ | SmolLM2-135M-Instruct | 8/0, 0.008* | 10/3, 0.092 | 11/0, 0.001* | 44/18, 0.001* | 73/21, 0.000* | 40/16, 0.002* | 7/10, 0.629 |
185
+
186
+ (0.000 means below 0.0005.)
187
+
188
+ Caveats for this comparison: the zero-shot models were pretrained on the open web and may know some answers without
189
+ reading the results, while v3.1a's training held out every test question and topic. google25 is v3.1a's dev set
190
+ and google_fresh / google_big guided the v3.1 data, which favours v3.1a a little. The Qwen and SmolLM2 model cards
191
+ suggest sampling; greedy is used for every model so the run is deterministic. Qwen3-4B is a reference at about 65
192
+ times v3.1a's size.
193
+
194
  ## Caveats
195
 
196
  - The sets are small. One question is 0.4 points of the 258 total and 0.6 points of google_big.