File size: 13,292 Bytes
a028f12
 
 
 
 
c97f9cc
 
a028f12
 
 
 
 
c97f9cc
 
a028f12
 
729bd87
c97f9cc
 
 
 
 
 
 
 
 
 
 
 
a739c7e
c97f9cc
 
a739c7e
c97f9cc
 
 
 
 
 
 
 
 
a739c7e
c97f9cc
 
a739c7e
c97f9cc
 
 
 
 
 
 
 
 
a739c7e
c97f9cc
 
a739c7e
a028f12
 
729bd87
 
 
a028f12
729bd87
b89a598
729bd87
a028f12
729bd87
a028f12
729bd87
 
 
 
 
a028f12
729bd87
 
6f8d8d2
a028f12
c97f9cc
 
6f8d8d2
c97f9cc
e60e429
c97f9cc
 
 
 
 
a739c7e
 
 
c97f9cc
 
 
a739c7e
 
 
 
 
 
 
 
 
 
6f8d8d2
a739c7e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c97f9cc
 
 
 
 
 
a739c7e
 
c97f9cc
 
 
a739c7e
c97f9cc
 
 
a739c7e
c97f9cc
 
 
 
 
 
 
 
 
 
 
 
 
 
a739c7e
3f7cc27
c97f9cc
 
 
 
 
 
 
 
 
a739c7e
 
c97f9cc
 
 
 
 
 
 
a028f12
729bd87
 
a028f12
 
729bd87
 
 
 
a028f12
c97f9cc
a028f12
 
 
729bd87
 
 
 
 
 
 
 
a028f12
 
 
 
729bd87
a028f12
 
729bd87
a028f12
729bd87
a028f12
c97f9cc
a028f12
729bd87
 
 
 
a028f12
c97f9cc
a028f12
 
 
729bd87
 
 
a028f12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729bd87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c97f9cc
a028f12
 
 
 
 
 
 
 
 
729bd87
a028f12
729bd87
6f8d8d2
729bd87
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
---
library_name: onnx
pipeline_tag: text-generation
datasets:
  - corbt/enron-emails
  - HuggingFaceFW/fineweb-edu
  - HuggingFaceTB/smoltalk
language:
  - en
tags:
  - autocomplete
  - gru
  - transformer
  - fill-in-the-middle
  - onnxruntime-web
  - in-browser
  - offline-page
model-index:
- name: tinynext-v2
  results:
  - task:
      type: text-generation
      name: Autocomplete (typing saved at a 50% cutoff)
    dataset:
      type: wikimedia/wikipedia
      name: Wikipedia articles (held out)
    metrics:
    - type: typing_saved
      name: Typing saved (%)
      value: 3.9
    - type: suggestion_precision
      name: Suggestions that were right (%)
      value: 78
  - task:
      type: text-generation
      name: Autocomplete (typing saved at a 50% cutoff)
    dataset:
      type: OpenAssistant/oasst1
      name: OpenAssistant messages, validation split
    metrics:
    - type: typing_saved
      name: Typing saved (%)
      value: 8.8
    - type: suggestion_precision
      name: Suggestions that were right (%)
      value: 75
  - task:
      type: text-generation
      name: Autocomplete (typing saved at a 50% cutoff)
    dataset:
      type: corbt/enron-emails
      name: Enron emails from held-out senders
    metrics:
    - type: typing_saved
      name: Typing saved (%)
      value: 4.0
    - type: suggestion_precision
      name: Suggestions that were right (%)
      value: 78
---

<p align="center">
  <img src="banner.png" alt="tinynext: suggests the next few words, Tab to take them" width="880">
</p>

<p align="center">
  <img style="display: inline-block; vertical-align: middle; margin: 2px;" alt="params: 944K" src="https://img.shields.io/badge/params-944K-6e6e73?style=flat-square&labelColor=1d1d1f"> <img style="display: inline-block; vertical-align: middle; margin: 2px;" alt="size: 0.98 MB" src="https://img.shields.io/badge/size-0.98%20MB-6e6e73?style=flat-square&labelColor=1d1d1f"> <img style="display: inline-block; vertical-align: middle; margin: 2px;" alt="speed: ~4 ms" src="https://img.shields.io/badge/speed-~4%20ms-6e6e73?style=flat-square&labelColor=1d1d1f"> <img style="display: inline-block; vertical-align: middle; margin: 2px;" alt="trained: $0.35" src="https://img.shields.io/badge/trained-$0.35-6e6e73?style=flat-square&labelColor=1d1d1f"> <img style="display: inline-block; vertical-align: middle; margin: 2px;" alt="data: Enron email" src="https://img.shields.io/badge/data-Enron%20email-6e6e73?style=flat-square&labelColor=1d1d1f">
</p>

<h3 align="center">A tiny email autocomplete that runs in your browser.<br>It learned to write from Enron's emails.</h3>

<p align="center">
  <a href="https://huggingface.co/spaces/cfahlgren1/enron-autocomplete"><b>Try the demo&nbsp;→</b></a>
  &nbsp;&nbsp;·&nbsp;&nbsp;
  <a href="https://github.com/cfahlgren1/offline-page"><b>Write with it in offline.page&nbsp;→</b></a>
</p>

<br>

tinynext is the autocomplete in [offline.page](https://github.com/cfahlgren1/offline-page). The app uses **v2**, in [`v2/`](https://huggingface.co/paperweight/tinynext/tree/main/v2). The files at the root are v1, the original character model, the same as [`cfahlgren1/enron-autocomplete`](https://huggingface.co/cfahlgren1/enron-autocomplete).

## v2: the next few words, with context

<sub>In [`v2/`](https://huggingface.co/paperweight/tinynext/tree/main/v2). The files at the root of this repo are still v1.</sub>

v1 guesses one letter at a time, so its suggestions are short word endings. v2 is a **12.5M-parameter transformer that predicts word pieces**, so one step can be a whole word, and it reads up to about 1,500 characters before the cursor (and, when there is any, text after it). It's the autocomplete in offline.page, running in a Web Worker.

**How much typing it saves.** Every model is scored by [`v2/eval.py`](v2/eval.py) on the same held-out text: type it character by character, pause every 12 characters, and count a suggestion only if the text really continues that way. Same decoding rule and 50% cutoff as the app, suggestions of 4+ characters.

| Writing | v1 | v2 | SmolLM2-135M (ceiling, ~100 MB) |
|---|---|---|---|
| Articles (Wikipedia) | 1.5% | **3.9%** | 8.1% |
| Messages (OpenAssistant) | 2.0% | **8.8%** | 16.8% |
| Email (Enron, unseen senders) | 3.0% | **4.0%** | 5.7% |

| Writing (v2) | Suggests at | Right | 2+ words suggested at | 2+ words right |
|---|---|---|---|---|
| Articles (Wikipedia) | 10% | 78% | 4% | 71% |
| Messages (OpenAssistant) | 19% | 75% | 9% | 70% |
| Email (Enron, unseen senders) | 11% | 78% | 4% | 51% |

**Which v2.** Three runs of the same 12.5M-parameter model, typing saved on articles / messages / email. *Editing* is the same test with text after the cursor, where fill-in-the-middle can use it.

| Run | Training tokens | Fill-in-the-middle | Typing | Editing | Val loss |
|---|---|---|---|---|---|
| **v2 (this one)** | **3B** | half the examples | **3.9 / 8.8 / 4.0** | **3.1 / 7.6 / 4.2** | **2.89** |
| First v2 | 1.5B | half the examples | 4.0 / 7.3 / 4.3 | 2.8 / 6.3 / 3.6 | 2.99 |
| [No FIM](https://huggingface.co/paperweight/tinynext/tree/main/v2/experiments/tinynext-v2-nofim) | 1.5B | none | 3.9 / 8.8 / 4.2 | 3.0 / 6.9 / 4.1 | 2.94 |

At 1.5B tokens, fill-in-the-middle cost the model more than it gave: half its training went to a task it rarely uses. With twice the tokens it stops costing anything on typing and helps when editing. The first v2 is in this repo's history.

<details>
<summary><b>Would a bigger model help?</b></summary>

[Pythia](https://huggingface.co/EleutherAI/pythia-160m) models are trained the same way at every size, so they show what size alone buys. Typing saved, 300 pauses per kind of writing (tinynext: 1,000):

| Model | Parameters | Articles | Messages | Email |
|---|---|---|---|---|
| Pythia | 14M | 3.6% | 6.1% | 2.4% |
| Pythia | 31M | 4.0% | 6.6% | 4.0% |
| Pythia | 70M | 7.0% | 8.0% | 4.9% |
| Pythia | 160M | 7.3% | 10.5% | 7.9% |
| SmolLM2 | 135M | 8.1% | 16.8% | 5.7% |
| **tinynext v2** | **12.5M** | **3.9%** | **8.8%** | **4.0%** |

tinynext matches Pythia models 2–3× its size because it trained on writing like what people type. Size starts to pay off between 30M and 70M parameters, and better data (SmolLM2) matters as much as size.

</details>

**In the browser.**

| | |
|---|---|
| Download | **13.3 MB**: int8 weights with one scale per 64 values, expanded to float32 when the page loads |
| Matches full precision | same next word 96% of the time, same 5-word path 90% |
| Speed | **5 ms** per suggestion (13 ms p95) in a Web Worker, Chrome on an M3 Max; typing stays under one frame |

The browser computes in float32 on purpose: onnxruntime-web's int8 kernels were 8–11× slower than its float kernels for this model, and int8 activations changed the top word too often. Only the download is compressed.

**Training.** 3B tokens (60% [FineWeb-Edu](https://huggingface.co/datasets/HuggingFaceFW/fineweb-edu), 30% [SmolTalk](https://huggingface.co/datasets/HuggingFaceTB/smoltalk), 10% Enron email from training senders), half of it as fill-in-the-middle examples. 56 minutes on one A100 at 920K tokens a second, from data tokenized once on a CPU job. Validation loss 2.89. [Training curves on Trackio](https://huggingface.co/spaces/cfahlgren1/tinynext-trackio).

**Limits.**

- offline.page only suggests at the end of a paragraph, so the text after the cursor is the next paragraph. Mid-line suggestions, where fill-in-the-middle should help most, aren't built yet.
- It still leans toward office email, and it only sees keyboard characters.
- It's English only.

<details>
<summary><b>v2 files</b></summary>

| File | What it is |
|---|---|
| `v2/web/graph.onnx`, `weights.q8`, `weights.json` | The browser model: graph, int8 weights and how to expand them |
| `v2/web/tokenizer.json`, `config.json` | 8K byte-level BPE tokenizer and model shape |
| `v2/model.safetensors` | Float32 weights |
| `v2/train.py` | Data prep, training, eval, export and upload in one script |
| `v2/eval.py`, `v2/eval_set.json` | The eval harness and the fixed text it scores |
| `v2/export.py`, `v2/compress.py` | Re-export to ONNX, and the int8 download format |
| `v2/metrics/*.json` | Eval results for v1, v2, the browser format, SmolLM2-135M and Pythia 14M–160M |
| `v2/experiments/` | The no-FIM run, for comparison |
| `v2/report.json` | Training settings, loss history and export checks |

</details>

<details>
<summary><b>Train it yourself</b></summary>

```bash
# Prepare data once on a CPU job, then train on a GPU from the prepared files.
hf jobs uv run --flavor cpu-xl --secrets HF_TOKEN train.py --stage prepare --tokens 3e9 --fineweb-files 4
hf jobs uv run --flavor a100-large --secrets HF_TOKEN train.py --stage train --tokens 3e9 --fineweb-files 4
```

</details>

## v1: the original character model

### What it does

It finishes the word or short phrase you're typing. Press <kbd>Tab</kbd> to take the suggestion.

| You type | It suggests |
|---|---|
| Please accept my resig | **nation** |
| Please accept my resignation. I will be o | **ut of the office** |
| The books are fine. Please call me i | **f you have any** |
| Enron N | **orth America Corp.** |

### How sure it has to be

The model knows how sure it is of each guess. You pick a cutoff, and it only suggests when it's at least that sure. A higher cutoff means fewer suggestions, and more of them right.

<table>
  <tr>
    <td align="center" width="33%"><h2>80%</h2>of suggestions are right<br><sub>at a 50% cutoff</sub></td>
    <td align="center" width="33%"><h2>11%</h2>of the time it suggests<br><sub>at a 50% cutoff</sub></td>
    <td align="center" width="33%"><h2>~6</h2>characters per right suggestion</td>
  </tr>
</table>

| Cutoff | How often it suggests | How often it's right |
|---|---|---|
| 20% | 30% of the time | 48% |
| 35% | 18% of the time | 66% |
| **50%** *(offline.page)* | **11% of the time** | **80%** |
| 70% | 7% of the time | 90% |

The demo uses 40%, and offline.page uses 50%.

These numbers come from 5,000 places in emails from people the model never saw in training. “Right” means the suggestion exactly matched what the person typed next.

#### Limits

- It only knows office email from 2001. Outside that it has little to say.
- With no cutoff it rambles. “Dear Santa,” becomes “I am attaching a copy of the agreement to the Enron Corp.”
- It learned real names and sign-offs from the emails, so keep suggestions short.
- English only.

### Use it

The model takes one character and its memory of the text so far. It returns its guess for the next character and an updated memory. So each key press is one small step.

<details>
<summary><b>JavaScript example</b></summary>

```js
import * as ort from "onnxruntime-web";

const session = await ort.InferenceSession.create("model_quint8.onnx", {
  executionProviders: ["wasm"],
});
let state = new ort.Tensor("float32", new Float32Array(512), [1, 512]);

async function step(id) {
  const input_id = new ort.Tensor("int64", BigInt64Array.of(BigInt(id)), [1]);
  const { logits, next_state } = await session.run({ input_id, state });
  state = next_state;
  return logits.data;
}

let logits = await step(1); // 1 starts a new email
for (const char of "Let me kn") logits = await step(char.charCodeAt(0) - 29);
```

A character's id is its character code minus 29. A new line is 2 and the end of an email is 1. Only English keyboard characters are supported.

To build a suggestion, keep taking the most likely next character and multiply the probabilities together as you go. Stop at a new line, or when that running product drops below your cutoff. Show the text up to the last finished word.

</details>

<details>
<summary><b>Files</b></summary>

| File | What it is |
|---|---|
| `model_quint8.onnx` | The model to use. 0.98 MB. |
| `model.onnx` | The same model before shrinking. 3.8 MB. |
| `best.pt` | The PyTorch checkpoint. |
| `vocab.json` | The character ids. |
| `report.json` | The full training and test results. |
| `train.py` | The training script. |

I also tried a bigger version with 3.46M parameters and 3.5 MB. It wasn't better. When both models suggest equally often, 9% of the time, the small one is right about 85% of the time and the big one 82%. The big one got the same training settings and may need different ones to do well.

</details>

### Training

It read 40 million characters from 119,859 emails sent from enron.com addresses, 30 times over. Typos were added to what it read, so it keeps predicting when you make one. The run took 14 minutes on one A10G with Hugging Face Jobs and cost 35 cents.

[`train.py`](train.py) is the whole thing: it downloads the emails, trains, tests and exports the model. To train your own:

```bash
hf jobs uv run --flavor a10g-large --secrets HF_TOKEN train.py --repo your-name/your-model
```

<br>

<p align="center">
  <sub>tinynext is one of two tiny models behind <a href="https://github.com/cfahlgren1/offline-page">offline.page</a>, a writing app that works in airplane mode.<br>Its partner, <a href="https://huggingface.co/paperweight/tinyfix">tinyfix</a>, fixes typos from context.<br>Both are made by <a href="https://huggingface.co/paperweight">paperweight</a>, purpose-built small models.</sub>
</p>