Text Generation
Transformers
Safetensors
English
llama
hand-crafted
compiled
autocomplete
no-training
n-gram
code
browser
text-generation-inference
Instructions to use tmickleydoyle/tiny-autocomplete with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use tmickleydoyle/tiny-autocomplete with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="tmickleydoyle/tiny-autocomplete")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("tmickleydoyle/tiny-autocomplete") model = AutoModelForCausalLM.from_pretrained("tmickleydoyle/tiny-autocomplete", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use tmickleydoyle/tiny-autocomplete with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "tmickleydoyle/tiny-autocomplete" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tmickleydoyle/tiny-autocomplete", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/tmickleydoyle/tiny-autocomplete
- SGLang
How to use tmickleydoyle/tiny-autocomplete with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "tmickleydoyle/tiny-autocomplete" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tmickleydoyle/tiny-autocomplete", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "tmickleydoyle/tiny-autocomplete" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tmickleydoyle/tiny-autocomplete", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use tmickleydoyle/tiny-autocomplete with Docker Model Runner:
docker model run hf.co/tmickleydoyle/tiny-autocomplete
Confidence-gated ghost text for coding prompts: Kneser-Ney compile, 4.5 MB browser export + zero-dep runtime, held-out precision/keystrokes-saved benchmarks
Browse files- README.md +182 -180
- build_weights.py +265 -152
- compile_settings.json +27 -0
- data/SOURCES.md +25 -14
- data/prompts.txt +0 -0
- data/{coding_prompts.txt → replies.txt} +0 -0
- docs/benchmarks.json +107 -25
- docs/experiment_topic.json +41 -0
- docs/how-it-works.md +169 -105
- docs/sweep.json +2181 -0
- export_web.py +102 -0
- model.safetensors +1 -1
- model.ts +3 -59
- scripts/benchmark.py +78 -62
- scripts/experiment_topic.py +112 -0
- scripts/fetch_coding_prompts.py +30 -20
- scripts/sweep.py +185 -0
- suggest.py +139 -0
- test_model.py +65 -4
- tokenizer.json +0 -0
- tokenizer.ts +74 -0
- web/autocomplete.ts +212 -0
- web/cli.ts +41 -0
- web/demo.js +1 -0
- web/demo.ts +84 -0
- web/index.html +53 -0
- web/model.bin +3 -0
README.md
CHANGED
|
@@ -12,97 +12,112 @@ tags:
|
|
| 12 |
- no-training
|
| 13 |
- n-gram
|
| 14 |
- code
|
|
|
|
| 15 |
datasets:
|
| 16 |
- OpenAssistant/oasst2
|
| 17 |
- google-research-datasets/mbpp
|
| 18 |
- neulab/conala
|
| 19 |
widget:
|
| 20 |
-
- text: Once upon a
|
| 21 |
- text: In Python,
|
| 22 |
- text: I hope this email
|
| 23 |
---
|
| 24 |
|
| 25 |
# tiny-autocomplete
|
| 26 |
|
| 27 |
-
|
| 28 |
-
trained**
|
| 29 |
-
Every number comes from a closed-form rule in [`build_weights.py`](build_weights.py) applied to
|
| 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 |
-
computes a known formula.
|
| 100 |
-
- **Standard format:** it's a plain `LlamaForCausalLM`, so it works with `transformers`,
|
| 101 |
-
`pipeline`, and any runtime that loads Llama safetensors.
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
be 10× smaller and faster.
|
| 106 |
|
| 107 |
## Use it with transformers
|
| 108 |
|
|
@@ -110,133 +125,120 @@ be 10× smaller and faster.
|
|
| 110 |
from transformers import pipeline
|
| 111 |
|
| 112 |
gen = pipeline("text-generation", model="tmickleydoyle/tiny-autocomplete")
|
| 113 |
-
gen("
|
| 114 |
-
```
|
| 115 |
-
|
| 116 |
-
Next-word suggestions with probabilities:
|
| 117 |
-
|
| 118 |
-
```python
|
| 119 |
-
import torch
|
| 120 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 121 |
-
|
| 122 |
-
tok = AutoTokenizer.from_pretrained("tmickleydoyle/tiny-autocomplete")
|
| 123 |
-
model = AutoModelForCausalLM.from_pretrained("tmickleydoyle/tiny-autocomplete")
|
| 124 |
-
|
| 125 |
-
ids = tok("Write a python function to", return_tensors="pt")
|
| 126 |
-
with torch.no_grad():
|
| 127 |
-
probs = model(**ids).logits[0, -1].softmax(-1)
|
| 128 |
-
for p, i in zip(*probs.topk(5)):
|
| 129 |
-
print(repr(tok.decode(i)), f"{p:.1%}") # ' find' 48.2%, ' check' 19.0%, ' count' 13.9%, ...
|
| 130 |
-
|
| 131 |
-
out = model.generate(**ids, max_new_tokens=16)
|
| 132 |
-
print(tok.decode(out[0], skip_special_tokens=True))
|
| 133 |
-
```
|
| 134 |
-
|
| 135 |
-
The tokenizer lowercases input, so output is lowercase. A BOS token is prepended
|
| 136 |
-
automatically.
|
| 137 |
-
|
| 138 |
-
## Use it without Python (TypeScript, zero dependencies)
|
| 139 |
-
|
| 140 |
-
`model.ts` reads `config.json`, `model.safetensors` and `tokenizer.json` and runs the Llama
|
| 141 |
-
forward pass in plain TypeScript. It needs no ML library. It runs on [Bun](https://bun.sh)
|
| 142 |
-
or Node ≥ 23.6.
|
| 143 |
-
|
| 144 |
-
```bash
|
| 145 |
-
git lfs install
|
| 146 |
-
git clone https://huggingface.co/tmickleydoyle/tiny-autocomplete
|
| 147 |
-
cd tiny-autocomplete
|
| 148 |
-
|
| 149 |
-
bun ui.ts # web UI at http://localhost:3000
|
| 150 |
-
bun autocomplete.ts # terminal REPL
|
| 151 |
-
bun autocomplete.ts "In Python," "Once upon a"
|
| 152 |
```
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
dense implementation at about 0.4 s per generated word. PyTorch is much faster.
|
| 158 |
-
|
| 159 |
-
## What it computes
|
| 160 |
-
|
| 161 |
-
The network is an exact transformer implementation of an **interpolated Witten-Bell 5-gram
|
| 162 |
-
language model**. It predicts the next token from the current one plus up to 3 before it,
|
| 163 |
-
falling back to shorter contexts when a longer one is unknown. Its log-probabilities match the
|
| 164 |
-
reference n-gram to within 1e-4 nats (checked in `test_model.py`).
|
| 165 |
-
|
| 166 |
-
## How the weights are built (summary)
|
| 167 |
-
|
| 168 |
-
The residual stream is split into named slots: a constant 1, the current token's code, the
|
| 169 |
-
codes of the 3 previous tokens, and a logit slot.
|
| 170 |
|
| 171 |
-
|
| 172 |
-
- **Attention:** 3 heads with constant queries and keys. Their RoPE phases make each head
|
| 173 |
-
look exactly 1, 2 or 3 tokens back, with a margin of ≥ 30 nats. The head copies that
|
| 174 |
-
token's code into its slot.
|
| 175 |
-
- **MLP:** one neuron per kept context of 1–4 tokens (24,774 neurons). A neuron fires only
|
| 176 |
-
when every token matches. It adds `log p(next | context) − log p(next | shorter context)`
|
| 177 |
-
to the logits. These additions telescope to the log-probability under the longest
|
| 178 |
-
known context.
|
| 179 |
-
- **Fitting the budget:** the data contains 434k distinct contexts. Contexts seen at least 8
|
| 180 |
-
times are kept, plus every context from the hand-written phrases. The threshold is chosen
|
| 181 |
-
automatically to fit a 400M-parameter target. Vocabulary is capped at 3,000 words; rarer
|
| 182 |
-
words become `<unk>`, which can still appear inside contexts.
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
`LlamaForCausalLM` with standard modules only, so no `trust_remote_code` is needed. The settings
|
| 190 |
-
are 1 layer, hidden 4560, 3 query heads, 1 KV head, head_dim 384, MLP width 24,774, vocab 3,000,
|
| 191 |
-
rope_theta 2, float32 (1.5 GB).
|
| 192 |
-
|
| 193 |
-
| Tensor | Params |
|
| 194 |
|---|---|
|
| 195 |
-
|
|
| 196 |
-
|
|
| 197 |
-
|
|
| 198 |
-
|
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
## Limitations
|
| 202 |
|
| 203 |
-
- **Short memory:** it sees 4 tokens
|
| 204 |
-
|
| 205 |
-
- **
|
| 206 |
-
|
| 207 |
-
- **
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
| 211 |
|
| 212 |
## Rebuild and verify
|
| 213 |
|
| 214 |
```bash
|
| 215 |
pip install -r requirements.txt
|
| 216 |
-
python scripts/fetch_coding_prompts.py # optional: re-download data/
|
| 217 |
-
python build_weights.py #
|
| 218 |
-
python
|
| 219 |
-
python
|
|
|
|
|
|
|
| 220 |
```
|
| 221 |
|
| 222 |
-
Tested with transformers 4.52.4 and 5.22.1, Bun 1.3
|
| 223 |
|
| 224 |
## Licenses
|
| 225 |
|
| 226 |
-
The model, code and hand-written phrases are MIT. `data/
|
| 227 |
-
excerpts from OpenAssistant oasst2 (Apache-2.0), MBPP (CC-BY-4.0) and CoNaLa (MIT); see
|
| 228 |
[data/SOURCES.md](data/SOURCES.md) for attribution.
|
| 229 |
|
| 230 |
## Files
|
| 231 |
|
| 232 |
| File | Purpose |
|
| 233 |
|---|---|
|
| 234 |
-
| `
|
| 235 |
-
| `
|
| 236 |
-
| `
|
| 237 |
-
| `
|
| 238 |
-
| `
|
| 239 |
-
| `
|
|
|
|
| 240 |
| `test_model.py` | verification suite |
|
| 241 |
-
| `model.ts`, `autocomplete.ts`, `ui.ts`, `ui.html` |
|
| 242 |
| `docs/how-it-works.md` | full construction details |
|
|
|
|
| 12 |
- no-training
|
| 13 |
- n-gram
|
| 14 |
- code
|
| 15 |
+
- browser
|
| 16 |
datasets:
|
| 17 |
- OpenAssistant/oasst2
|
| 18 |
- google-research-datasets/mbpp
|
| 19 |
- neulab/conala
|
| 20 |
widget:
|
|
|
|
| 21 |
- text: In Python,
|
| 22 |
- text: I hope this email
|
| 23 |
---
|
| 24 |
|
| 25 |
# tiny-autocomplete
|
| 26 |
|
| 27 |
+
Ghost-text autocomplete for prompts to a coding assistant. **It's compiled, not
|
| 28 |
+
trained.** No gradient descent was run, and no other model or pretrained weights were used.
|
| 29 |
+
Every number comes from a closed-form rule in [`build_weights.py`](build_weights.py) applied to
|
| 30 |
+
~21,000 lines of human-written text ([sources](data/SOURCES.md)).
|
| 31 |
+
|
| 32 |
+
It's built to be **confident rather than chatty**. It only suggests when its probability is
|
| 33 |
+
high, and otherwise stays quiet.
|
| 34 |
+
|
| 35 |
+
The same compile produces two artifacts:
|
| 36 |
+
|
| 37 |
+
| | Browser export (`web/model.bin`) | Transformers model (`model.safetensors`) |
|
| 38 |
+
|---|---|---|
|
| 39 |
+
| Use it for | embedding in an app: on-device, no server | `transformers`, `pipeline`, research |
|
| 40 |
+
| Size | **4.5 MB** | 1.5 GB (380M params, 93% zeros) |
|
| 41 |
+
| Per keystroke | **0.5 ms median, 0.9 ms p95** (Bun) | 58 ms per next token (PyTorch CPU) |
|
| 42 |
+
| Vocabulary / contexts | 6,612 words / 107,864 contexts | 3,000 words / 24,773 contexts (pruned to fit the dense budget) |
|
| 43 |
+
|
| 44 |
+
Both are the same kind of network. The browser export stores only the non-zero parts of the
|
| 45 |
+
compiled transformer; see [how it works](#how-it-works).
|
| 46 |
+
|
| 47 |
+
## What it suggests
|
| 48 |
+
|
| 49 |
+
Real output of `web/autocomplete.ts` with the shipped confidence threshold (72%). Gray is the
|
| 50 |
+
ghost text; *(quiet)* means it suggests nothing.
|
| 51 |
+
|
| 52 |
+
| Typed | Ghost text | Confidence |
|
| 53 |
+
|---|---|---|
|
| 54 |
+
| `write a python fun` | `ction to` | 98% |
|
| 55 |
+
| `how do i conv` | `ert` | 96% |
|
| 56 |
+
| `I'm getting an err` | `or` | 100% |
|
| 57 |
+
| `can you explain the differ` | `ence between` | 98% |
|
| 58 |
+
| `Write a python function to check whether` | ` the` | 72% |
|
| 59 |
+
| `I hope this email` | ` finds you` | 85% |
|
| 60 |
+
| `what is the best way to` | *(quiet)* | |
|
| 61 |
+
| `how to convert a string to` | *(quiet)* | |
|
| 62 |
+
|
| 63 |
+
## How often it's right
|
| 64 |
+
|
| 65 |
+
Measured with `python scripts/benchmark.py`. The model is compiled **without** 10% of the
|
| 66 |
+
prompts (711 prompts, 42,185 characters), and each of those prompts is then typed
|
| 67 |
+
character by character. After every keystroke the model either shows ghost text or stays
|
| 68 |
+
quiet.
|
| 69 |
+
- **Precision:** the share of shown suggestions that exactly match what the user typed next.
|
| 70 |
+
A wrong word anywhere in the ghost counts as a miss.
|
| 71 |
+
- **Coverage:** the share of keystrokes where something is shown.
|
| 72 |
+
- **Keystrokes saved:** the share of keystrokes saved when every correct suggestion is accepted
|
| 73 |
+
with Tab.
|
| 74 |
+
|
| 75 |
+
| Confidence threshold | Precision | Coverage | Keystrokes saved |
|
| 76 |
+
|---|---|---|---|
|
| 77 |
+
| **0.72 (shipped default)** | **90.3%** | 38.7% | **23.0%** |
|
| 78 |
+
| 0.80 | 93.5% | 34.1% | 20.7% |
|
| 79 |
+
| 0.90 | 96.4% | 26.9% | 16.4% |
|
| 80 |
+
| 0.95 | 97.7% | 20.5% | 11.8% |
|
| 81 |
+
|
| 82 |
+
The threshold is a runtime setting, so an app can trade coverage for precision without
|
| 83 |
+
recompiling. The dense transformers model, with its smaller, more heavily pruned tables,
|
| 84 |
+
gets 92.4% precision / 19.0% saved at 0.72.
|
| 85 |
+
|
| 86 |
+
These numbers come from held-out OpenAssistant, MBPP and CoNaLa prompts. Your users'
|
| 87 |
+
prompts may differ, so measure on your own text if you can.
|
| 88 |
+
|
| 89 |
+
## Use it in your app (browser)
|
| 90 |
+
|
| 91 |
+
`web/autocomplete.ts` and `tokenizer.ts` are zero-dependency TypeScript with no DOM access, so
|
| 92 |
+
they run on the main thread, in a Web Worker, or in Bun/Node.
|
| 93 |
+
|
| 94 |
+
```ts
|
| 95 |
+
import { CompiledAutocomplete } from "./web/autocomplete.ts";
|
| 96 |
+
|
| 97 |
+
const ac = await CompiledAutocomplete.load(
|
| 98 |
+
"https://huggingface.co/tmickleydoyle/tiny-autocomplete/resolve/main/web/model.bin",
|
| 99 |
+
);
|
| 100 |
+
|
| 101 |
+
input.addEventListener("input", () => {
|
| 102 |
+
const { ghost, prob } = ac.suggest(input.value); // "" when not confident
|
| 103 |
+
showGhostText(ghost); // Tab -> input.value += ghost
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
ac.suggest(text, { ...ac.policy, tau: 0.9 }); // stricter: ~96% precision
|
| 107 |
+
```
|
| 108 |
|
| 109 |
+
Try the demo locally. It's a static page with no server code:
|
| 110 |
|
| 111 |
+
```bash
|
| 112 |
+
git lfs install
|
| 113 |
+
git clone https://huggingface.co/tmickleydoyle/tiny-autocomplete && cd tiny-autocomplete
|
| 114 |
+
bun build web/demo.ts --outfile web/demo.js --target browser # demo.js is also committed
|
| 115 |
+
python3 -m http.server 8080 -d web # open http://localhost:8080
|
| 116 |
+
bun web/cli.ts "write a python fun" # or from the terminal
|
| 117 |
+
```
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
+
The demo shows the ghost text, the confidence and the latency per keystroke, the model's best
|
| 120 |
+
guess when it stays quiet, and a slider for the threshold.
|
|
|
|
| 121 |
|
| 122 |
## Use it with transformers
|
| 123 |
|
|
|
|
| 125 |
from transformers import pipeline
|
| 126 |
|
| 127 |
gen = pipeline("text-generation", model="tmickleydoyle/tiny-autocomplete")
|
| 128 |
+
gen("In Python,")[0]["generated_text"] # 'In Python, you can use the following code:'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
```
|
| 130 |
|
| 131 |
+
`model.safetensors` is a plain `LlamaForCausalLM` (no `trust_remote_code`). Its logits are the
|
| 132 |
+
compiled n-gram's log-probabilities, matching the reference to within 2e-5 nats. To get the
|
| 133 |
+
same confidence-gated ghost text in Python, use `suggest.py` (`Suggester.suggest`).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
## Performance: why compile instead of train
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
+
On a 10-core Apple Silicon laptop (32 GB RAM), CPU only, no GPU. Raw numbers are in
|
| 138 |
+
[`docs/benchmarks.json`](docs/benchmarks.json).
|
| 139 |
|
| 140 |
+
| | |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|---|---|
|
| 142 |
+
| Compile the browser export from text | **6.6 s** |
|
| 143 |
+
| Compile the 380M-parameter transformers model | **6.9 s** (peak memory 2.4 GB) |
|
| 144 |
+
| GPU hours / compute cost | **0 / $0** |
|
| 145 |
+
| Reproducible | rebuilds are byte-identical (both artifacts) |
|
| 146 |
+
| Browser export: load / keystroke | 51–64 ms load in Chrome (local); 0.5 ms median, 0.9 ms p95 per keystroke |
|
| 147 |
+
| Transformers model: next token | 58 ms (PyTorch CPU, 8 threads); 18 tokens/s greedy |
|
| 148 |
+
|
| 149 |
+
For scale: the sibling experiment [tiny-research](https://tiny-research.vercel.app/)
|
| 150 |
+
trained a 34M model with a 5-minute budget on an RTX 4090.
|
| 151 |
+
|
| 152 |
+
**Why build a model like this:**
|
| 153 |
+
- **Iteration in seconds:** edit text, recompile, and measure in under a minute.
|
| 154 |
+
- **Calibrated on purpose:** a suggestion only appears when the compiled probability clears a
|
| 155 |
+
threshold whose precision is measured.
|
| 156 |
+
- **Auditable:** every neuron is one context from the source text, so any suggestion can be
|
| 157 |
+
traced to the lines that produced it.
|
| 158 |
+
- **Editable:** delete a line and recompile to remove a behavior.
|
| 159 |
+
- **Deterministic and exact:** the same text always produces the same bytes.
|
| 160 |
+
|
| 161 |
+
**What it costs you:** it doesn't generalize like a trained model. Next-token top-1
|
| 162 |
+
accuracy on held-out prompts is 26.7% (top-3: 40.6%), and 7% of next tokens are outside the
|
| 163 |
+
vocabulary. That's why it only speaks when confident.
|
| 164 |
+
|
| 165 |
+
## How it works
|
| 166 |
+
|
| 167 |
+
- **The network:** the compiled network is an exact transformer implementation of an
|
| 168 |
+
**interpolated Kneser-Ney 5-gram language model**. It predicts the next token from the
|
| 169 |
+
current token plus the 3 before it, backing off to shorter contexts when a longer one is
|
| 170 |
+
unknown.
|
| 171 |
+
- **Attention:** 3 heads with constant queries and keys. Their RoPE phases make each head look
|
| 172 |
+
exactly 1, 2 or 3 tokens back, and each head copies that token's code into its own slot.
|
| 173 |
+
- **MLP:** one neuron per kept context (1–4 tokens). A neuron fires only when every token
|
| 174 |
+
matches. It adds `log p(next | context) − log p(next | shorter context)` to the logits, and
|
| 175 |
+
these additions telescope to the log-probability under the longest known context.
|
| 176 |
+
- **Browser export:** stores, for each neuron, its context tokens and the logit changes it
|
| 177 |
+
writes (float32), plus the always-on unigram neuron. The runtime evaluates only the neurons
|
| 178 |
+
whose context matches: the same math as the dense model, without the zeros. A test exports
|
| 179 |
+
the dense model's own tables and checks the runtime against PyTorch.
|
| 180 |
+
- **Confidence policy** (`suggest.py`, mirrored by `web/autocomplete.ts`):
|
| 181 |
+
- **Partial words:** if the user is mid-word, only words starting with the typed prefix are
|
| 182 |
+
candidates. Their probability is scaled by the chance that a word with this prefix is in
|
| 183 |
+
the vocabulary at all, so rare words don't trigger confident wrong completions.
|
| 184 |
+
- **When to show:** only if that probability is ≥ τ.
|
| 185 |
+
- **Extension:** the suggestion grows word by word while the combined probability stays
|
| 186 |
+
≥ τ, stopping at end-of-text or after `. ? ! :`.
|
| 187 |
+
- **Settings were benchmarked, not tuned by hand:** `scripts/sweep.py` compiled 24
|
| 188 |
+
combinations and picked the one with the most keystrokes saved at ≥ 90% precision. The
|
| 189 |
+
combinations varied context length (4–10 tokens), Witten-Bell vs Kneser-Ney, source weights,
|
| 190 |
+
vocabulary and pruning. The weights themselves are always closed-form counts.
|
| 191 |
+
- **Context length:** 4 tokens won. Longer contexts were slightly worse (22.4–22.8% saved),
|
| 192 |
+
because with ~21k lines they rarely repeat.
|
| 193 |
+
- **Topic head:** an experimental "topic" attention head, conditioning on the last
|
| 194 |
+
language or tool name mentioned, didn't help either (`scripts/experiment_topic.py`).
|
| 195 |
+
|
| 196 |
+
Details, formulas and the proof of exactness are in [docs/how-it-works.md](docs/how-it-works.md).
|
| 197 |
|
| 198 |
## Limitations
|
| 199 |
|
| 200 |
+
- **Short memory and a lookup:** it sees 4 tokens and reproduces frequent patterns. It
|
| 201 |
+
doesn't understand code, and it doesn't write code (code blocks were removed from the data).
|
| 202 |
+
- **Narrow domain:** strongest on how people *phrase requests* about code ("write a python
|
| 203 |
+
function to…", "can you explain the difference between…"). It's quiet on most other text.
|
| 204 |
+
- **Lowercase:** input is lowercased, so ghost text is lowercase.
|
| 205 |
+
- **Limits:** the transformers model has 128 positions. The browser runtime has no length
|
| 206 |
+
limit, but it only ever looks at the last 4 tokens.
|
| 207 |
+
- **Authorship:** ~1,100 everyday phrases in `phrases/` were written by an AI coding assistant
|
| 208 |
+
(Claude) for this project. All coding data is human-written; see
|
| 209 |
+
[data/SOURCES.md](data/SOURCES.md).
|
| 210 |
|
| 211 |
## Rebuild and verify
|
| 212 |
|
| 213 |
```bash
|
| 214 |
pip install -r requirements.txt
|
| 215 |
+
python scripts/fetch_coding_prompts.py # optional: re-download data/prompts.txt, data/replies.txt
|
| 216 |
+
python build_weights.py # transformers model ("hf" in compile_settings.json)
|
| 217 |
+
python export_web.py # browser export ("web" + "policy")
|
| 218 |
+
python test_model.py # HF loading, exactness, TS parity, web runtime == Python == PyTorch
|
| 219 |
+
python scripts/sweep.py # re-run the settings comparison -> docs/sweep.json
|
| 220 |
+
python scripts/benchmark.py # -> docs/benchmarks.json
|
| 221 |
```
|
| 222 |
|
| 223 |
+
Tested with transformers 4.52.4 and 5.22.1, Bun 1.3, Node 24 and Chrome.
|
| 224 |
|
| 225 |
## Licenses
|
| 226 |
|
| 227 |
+
The model, code and hand-written phrases are MIT. `data/prompts.txt` and `data/replies.txt`
|
| 228 |
+
contain excerpts from OpenAssistant oasst2 (Apache-2.0), MBPP (CC-BY-4.0) and CoNaLa (MIT); see
|
| 229 |
[data/SOURCES.md](data/SOURCES.md) for attribution.
|
| 230 |
|
| 231 |
## Files
|
| 232 |
|
| 233 |
| File | Purpose |
|
| 234 |
|---|---|
|
| 235 |
+
| `web/model.bin`, `web/autocomplete.ts`, `tokenizer.ts` | browser export and its runtime |
|
| 236 |
+
| `web/index.html`, `web/demo.ts`, `web/demo.js`, `web/cli.ts` | demo page and CLI |
|
| 237 |
+
| `config.json`, `generation_config.json`, `model.safetensors`, `tokenizer*.json` | transformers model |
|
| 238 |
+
| `build_weights.py`, `export_web.py`, `compile_settings.json` | the compiler and its settings |
|
| 239 |
+
| `suggest.py` | confidence policy (Python reference) |
|
| 240 |
+
| `phrases/*.txt`, `data/*.txt`, `data/SOURCES.md` | source text and attribution |
|
| 241 |
+
| `scripts/sweep.py`, `scripts/benchmark.py`, `scripts/experiment_topic.py`, `docs/*.json` | measurements |
|
| 242 |
| `test_model.py` | verification suite |
|
| 243 |
+
| `model.ts`, `autocomplete.ts`, `ui.ts`, `ui.html` | TypeScript runtime for the dense model, CLI and server UI |
|
| 244 |
| `docs/how-it-works.md` | full construction details |
|
build_weights.py
CHANGED
|
@@ -1,34 +1,40 @@
|
|
| 1 |
-
"""Compile
|
| 2 |
|
| 3 |
-
Every weight is set by a closed-form rule; nothing is fit by
|
| 4 |
-
and no other model is used. The result is an exact transformer
|
| 5 |
-
of an interpolated
|
|
|
|
| 6 |
|
| 7 |
-
Residual stream layout (hidden_size = 1 +
|
| 8 |
-
[0]
|
| 9 |
-
cur slot
|
| 10 |
-
prev1..
|
| 11 |
-
out slot
|
| 12 |
|
| 13 |
Token codes are seeded random unit vectors (nearly orthogonal). Attention head
|
| 14 |
-
a (a = 1..
|
| 15 |
-
peak at exactly a positions back; its value copies that token's code into
|
| 16 |
slot prev_a. Positions before the start fall back to position 0 (BOS).
|
| 17 |
|
| 18 |
-
The MLP has one neuron per context
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
log p(next | ctx) - log p(next | shorter ctx) into the out slot. All suffixes
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
| 24 |
"""
|
| 25 |
|
| 26 |
import json
|
| 27 |
import math
|
|
|
|
| 28 |
from collections import Counter, defaultdict
|
| 29 |
-
from
|
| 30 |
from pathlib import Path
|
| 31 |
|
|
|
|
| 32 |
import torch
|
| 33 |
from safetensors.torch import save_file
|
| 34 |
from tokenizers import Tokenizer, decoders, models, normalizers, pre_tokenizers, processors
|
|
@@ -36,18 +42,13 @@ from transformers import GenerationConfig, LlamaConfig, PreTrainedTokenizerFast
|
|
| 36 |
|
| 37 |
ROOT = Path(__file__).parent
|
| 38 |
PHRASE_DIR = ROOT / "phrases" # hand-written everyday phrases
|
| 39 |
-
DATA_DIR = ROOT / "data" #
|
|
|
|
| 40 |
EOS, UNK = "<|endoftext|>", "<unk>" # EOS doubles as BOS and pad, like GPT-2
|
| 41 |
-
|
| 42 |
-
PHRASE_WEIGHT = 5 # each hand-written phrase counts as 5 occurrences of data lines
|
| 43 |
-
MIN_WORD_COUNT = 3 # rarer words map to <unk> (which may still appear inside contexts)
|
| 44 |
-
MAX_VOCAB = 3000 # every hand-phrase word, then the most frequent data words; each word costs a hidden dim
|
| 45 |
-
PARAM_TARGET = 400_000_000 # contexts below an auto-chosen count are pruned to fit this
|
| 46 |
|
| 47 |
SEED = 1234
|
| 48 |
CODE_DIM = 384
|
| 49 |
-
N_PREV = 3 # previous tokens attended to; context = N_PREV + current
|
| 50 |
-
ORDER = N_PREV + 1
|
| 51 |
MAX_POS = 128
|
| 52 |
ATTN_MARGIN = 30.0 # min attention logit gap (nats) between target and any other position
|
| 53 |
BETA = 100.0 # gate sharpness; matched neurons sit at z = BETA / 2
|
|
@@ -59,16 +60,33 @@ NORMALIZER = normalizers.Sequence([normalizers.NFKC(), normalizers.Lowercase()])
|
|
| 59 |
PRE_TOKENIZER = pre_tokenizers.ByteLevel(add_prefix_space=True)
|
| 60 |
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
def load_phrases() -> list[list[str]]:
|
| 67 |
-
return load_lines(sorted(PHRASE_DIR.glob("*.txt")))
|
| 68 |
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
|
| 74 |
def load_lines(paths: list[Path]) -> list[list[str]]:
|
|
@@ -85,92 +103,222 @@ def load_lines(paths: list[Path]) -> list[list[str]]:
|
|
| 85 |
return out
|
| 86 |
|
| 87 |
|
| 88 |
-
def
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
padded = [0] * ORDER + seq + [0]
|
| 92 |
-
for i in range(ORDER - 1, len(padded) - 1):
|
| 93 |
-
yield [tuple(padded[i - k + 1 : i + 1]) if k else () for k in range(ORDER + 1)], padded[i + 1]
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
for seq, weight in weighted:
|
| 99 |
-
for ctxs, nxt in padded_events(seq):
|
| 100 |
-
if nxt ==
|
| 101 |
continue
|
| 102 |
for k, ctx in enumerate(ctxs):
|
| 103 |
counts[k][ctx][nxt] += weight
|
| 104 |
return counts
|
| 105 |
|
| 106 |
|
| 107 |
-
def kept_contexts(counts: list[defaultdict], min_count: int, forced: set) -> list[dict]:
|
| 108 |
-
"""Contexts that get a neuron: seen >= min_count times, or from the hand-written phrases.
|
| 109 |
-
|
| 110 |
-
A suffix is seen at least as often as any context ending in it, and suffixes of
|
| 111 |
-
hand-written contexts are hand-written, so every kept context's backoff chain is kept.
|
| 112 |
-
"""
|
| 113 |
-
return [counts[0]] + [
|
| 114 |
-
{ctx: t for ctx, t in counts[k].items() if ctx in forced or sum(t.values()) >= min_count}
|
| 115 |
-
for k in range(1, ORDER + 1)
|
| 116 |
-
]
|
| 117 |
-
|
| 118 |
-
|
| 119 |
class NGram:
|
| 120 |
-
"""Interpolated
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
uni[j] = c
|
| 131 |
-
logits =
|
| 132 |
-
logits[
|
| 133 |
self.base_logits = logits - logits[2:].mean() # constant shift; softmax-invariant
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
-
@lru_cache(maxsize=None)
|
| 137 |
def prob(self, j: int, ctx: tuple) -> float:
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
def delta(self, ctx: tuple) -> dict[int, float]:
|
| 148 |
-
"""log p_k - log p_{k-1}, minus the constant log(
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
def contexts(self) -> list[tuple]:
|
| 158 |
-
return [ctx for k in range(1,
|
| 159 |
|
| 160 |
-
def
|
| 161 |
-
"""
|
| 162 |
t = len(ids) - 1
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
|
| 173 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
"""Pick rope_theta maximizing the worst-case attention margin; return (theta, inv_freq, q_amp)."""
|
| 175 |
best = None
|
| 176 |
for theta in ROPE_THETA_CANDIDATES:
|
|
@@ -178,7 +326,7 @@ def rope_attention_scale(head_dim: int) -> tuple[float, torch.Tensor, float]:
|
|
| 178 |
offsets = torch.arange(-MAX_POS, MAX_POS + 1, dtype=torch.float64)
|
| 179 |
kernel = torch.cos(offsets[:, None] * inv_freq[None, :]).sum(1) # K(x), index x + MAX_POS
|
| 180 |
worst = math.inf
|
| 181 |
-
for lag in range(1,
|
| 182 |
for t in range(MAX_POS):
|
| 183 |
s_target = max(t - lag, 0)
|
| 184 |
xs = torch.tensor([t - s - lag for s in range(t + 1)]) + MAX_POS
|
|
@@ -195,51 +343,15 @@ def rope_attention_scale(head_dim: int) -> tuple[float, torch.Tensor, float]:
|
|
| 195 |
return theta, inv_freq, q_amp
|
| 196 |
|
| 197 |
|
| 198 |
-
def
|
| 199 |
-
|
| 200 |
-
for toks, w in [(t, PHRASE_WEIGHT) for t in phrases] + [(t, 1) for t in data]:
|
| 201 |
-
for p in toks:
|
| 202 |
-
freq[p] += w
|
| 203 |
-
by_freq = lambda p: (-freq[p], p) # noqa: E731
|
| 204 |
-
hand_words = {p for t in phrases for p in t}
|
| 205 |
-
data_words = sorted((p for p in freq if p not in hand_words and freq[p] >= MIN_WORD_COUNT), key=by_freq)
|
| 206 |
-
words = hand_words | set(data_words[: max(0, MAX_VOCAB - 2 - len(hand_words))])
|
| 207 |
-
vocab = [EOS, UNK] + sorted(words, key=by_freq)
|
| 208 |
-
index = {p: i for i, p in enumerate(vocab)}
|
| 209 |
-
V, m = len(vocab), CODE_DIM
|
| 210 |
-
|
| 211 |
-
def ids(toks: list[str]) -> list[int]:
|
| 212 |
-
return [index.get(p, 1) for p in toks]
|
| 213 |
-
|
| 214 |
-
counts = count_ngrams([(ids(t), PHRASE_WEIGHT) for t in phrases] + [(ids(t), 1) for t in data])
|
| 215 |
-
forced = {ctx for t in phrases for ctxs, _ in padded_events(ids(t)) for ctx in ctxs[1:]}
|
| 216 |
-
|
| 217 |
-
CONST, CUR = 0, 1
|
| 218 |
-
prev_slot = [1 + a * m for a in range(N_PREV + 1)] # prev_slot[0] is the cur slot
|
| 219 |
-
OUT = 1 + (N_PREV + 1) * m
|
| 220 |
-
n_heads, h = N_PREV, m
|
| 221 |
-
# transformers validates hidden_size % num_heads == 0; pad with unused zero dims.
|
| 222 |
-
D = -(-(OUT + V) // (8 * n_heads)) * (8 * n_heads)
|
| 223 |
-
|
| 224 |
-
def n_params(neurons: int) -> int:
|
| 225 |
-
return 2 * V * D + (2 * n_heads + 2) * h * D + 3 * neurons * D + 3 * D
|
| 226 |
-
|
| 227 |
-
totals = [Counter() for _ in range(ORDER + 1)] # how many contexts have each total count
|
| 228 |
-
for k in range(1, ORDER + 1):
|
| 229 |
-
for ctx, t in counts[k].items():
|
| 230 |
-
totals[k][0 if ctx in forced else sum(t.values())] += 1
|
| 231 |
-
max_total = max(c for k in range(1, ORDER + 1) for c in totals[k])
|
| 232 |
-
min_count = 1
|
| 233 |
-
while True:
|
| 234 |
-
N = 1 + sum(n for k in range(1, ORDER + 1) for c, n in totals[k].items() if c == 0 or c >= min_count)
|
| 235 |
-
if n_params(N) <= PARAM_TARGET:
|
| 236 |
-
break
|
| 237 |
-
if min_count > max_total:
|
| 238 |
-
raise SystemExit(f"hand-written contexts alone need {n_params(N):,} params > PARAM_TARGET; lower MAX_VOCAB")
|
| 239 |
-
min_count += 1
|
| 240 |
-
ngram = NGram(kept_contexts(counts, min_count, forced), V)
|
| 241 |
contexts = ngram.contexts()
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
gen = torch.Generator().manual_seed(SEED)
|
| 245 |
codes = torch.randn(V, m, generator=gen, dtype=torch.float64)
|
|
@@ -253,7 +365,7 @@ def build(phrases: list[list[str]], data: list[list[str]]):
|
|
| 253 |
embed[:, CONST] = 1.0
|
| 254 |
embed[:, CUR : CUR + m] = codes
|
| 255 |
|
| 256 |
-
theta, inv_freq, q_amp = rope_attention_scale(h)
|
| 257 |
half = h // 2
|
| 258 |
q_proj = torch.zeros(n_heads * h, D, dtype=torch.float64)
|
| 259 |
for a in range(n_heads):
|
|
@@ -275,7 +387,7 @@ def build(phrases: list[list[str]], data: list[list[str]]):
|
|
| 275 |
on = (BETA / 2) * torch.sigmoid(torch.tensor(BETA / 2, dtype=torch.float64)).item() # silu(BETA/2)
|
| 276 |
up[:, CONST] = 1.0
|
| 277 |
gate[0, CONST] = BETA / 2 # always-on unigram neuron
|
| 278 |
-
down[OUT : OUT + V, 0] = OUT_SCALE * ngram.base_logits / on
|
| 279 |
for n, ctx in enumerate(contexts, start=1):
|
| 280 |
k = len(ctx)
|
| 281 |
for a, tok in enumerate(reversed(ctx)): # a = 0 current, a = 1 previous, ...
|
|
@@ -288,9 +400,9 @@ def build(phrases: list[list[str]], data: list[list[str]]):
|
|
| 288 |
lm_head[torch.arange(V), OUT + torch.arange(V)] = 1.0 / OUT_SCALE
|
| 289 |
|
| 290 |
# RMSNorm weights undo the norm for the (fixed) input norms: |x|^2 = 2 before
|
| 291 |
-
# attention, 1 + 1 +
|
| 292 |
attn_norm = math.sqrt(2 / D)
|
| 293 |
-
mlp_norm = math.sqrt((2 +
|
| 294 |
state = {
|
| 295 |
"model.embed_tokens.weight": embed,
|
| 296 |
"model.layers.0.input_layernorm.weight": torch.full((D,), attn_norm),
|
|
@@ -325,16 +437,13 @@ def build(phrases: list[list[str]], data: list[list[str]]):
|
|
| 325 |
pad_token_id=0,
|
| 326 |
)
|
| 327 |
info = {
|
| 328 |
-
"vocab": V,
|
| 329 |
"hidden": D,
|
| 330 |
"neurons": N,
|
| 331 |
-
"min_context_count": min_count,
|
| 332 |
-
"contexts_seen": sum(len(counts[k]) for k in range(1, ORDER + 1)),
|
| 333 |
"rope_theta": theta,
|
| 334 |
"max_code_coherence": round(max_coherence, 3),
|
| 335 |
"params": sum(t.numel() for t in state.values()),
|
| 336 |
}
|
| 337 |
-
return
|
| 338 |
|
| 339 |
|
| 340 |
def build_tokenizer(vocab: list[str]) -> PreTrainedTokenizerFast:
|
|
@@ -355,13 +464,17 @@ def build_tokenizer(vocab: list[str]) -> PreTrainedTokenizerFast:
|
|
| 355 |
|
| 356 |
|
| 357 |
def compile_model():
|
| 358 |
-
"""Build
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
|
| 363 |
def main() -> None:
|
| 364 |
-
|
| 365 |
assert info["params"] <= 1_000_000_000, info
|
| 366 |
|
| 367 |
save_file(state, ROOT / "model.safetensors", metadata={"format": "pt"})
|
|
@@ -386,7 +499,7 @@ def main() -> None:
|
|
| 386 |
tok_cfg["model_input_names"] = ["input_ids", "attention_mask"]
|
| 387 |
tok_cfg_path.write_text(json.dumps(tok_cfg, indent=2) + "\n")
|
| 388 |
|
| 389 |
-
print(json.dumps({
|
| 390 |
|
| 391 |
|
| 392 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
"""Compile text into a Llama-format autocomplete model. No training.
|
| 2 |
|
| 3 |
+
Every weight is set by a closed-form rule from n-gram counts; nothing is fit by
|
| 4 |
+
gradient descent and no other model is used. The result is an exact transformer
|
| 5 |
+
implementation of an interpolated n-gram language model (Witten-Bell or
|
| 6 |
+
Kneser-Ney) over the source text.
|
| 7 |
|
| 8 |
+
Residual stream layout (hidden_size = 1 + (n_prev + 1) * CODE_DIM + vocab):
|
| 9 |
+
[0] constant 1 (acts as a bias, Llama has none)
|
| 10 |
+
cur slot (CODE_DIM) code of the current token <- embedding
|
| 11 |
+
prev1..n (CODE_DIM) codes of the 1..n previous tokens <- attention
|
| 12 |
+
out slot (vocab) next-token logits, scaled by OUT_SCALE <- MLP
|
| 13 |
|
| 14 |
Token codes are seeded random unit vectors (nearly orthogonal). Attention head
|
| 15 |
+
a (a = 1..n_prev) has constant query/key vectors whose RoPE phases make the
|
| 16 |
+
score peak at exactly a positions back; its value copies that token's code into
|
| 17 |
slot prev_a. Positions before the start fall back to position 0 (BOS).
|
| 18 |
|
| 19 |
+
The MLP has one neuron per kept context (1..n_prev+1 tokens) plus one always-on
|
| 20 |
+
unigram neuron. A neuron's gate sums code matches across slots minus (k - 0.5),
|
| 21 |
+
so it fires only when all k tokens match. It writes
|
| 22 |
+
log p(next | ctx) - log p(next | shorter ctx) into the out slot. All suffixes of
|
| 23 |
+
the true context fire together, so the sum telescopes to the log probability
|
| 24 |
+
under the longest kept context.
|
| 25 |
+
|
| 26 |
+
Compile settings live in compile_settings.json ("hf" for this dense model,
|
| 27 |
+
"web" for the sparse browser export written by export_web.py).
|
| 28 |
"""
|
| 29 |
|
| 30 |
import json
|
| 31 |
import math
|
| 32 |
+
import zlib
|
| 33 |
from collections import Counter, defaultdict
|
| 34 |
+
from dataclasses import dataclass
|
| 35 |
from pathlib import Path
|
| 36 |
|
| 37 |
+
import numpy as np
|
| 38 |
import torch
|
| 39 |
from safetensors.torch import save_file
|
| 40 |
from tokenizers import Tokenizer, decoders, models, normalizers, pre_tokenizers, processors
|
|
|
|
| 42 |
|
| 43 |
ROOT = Path(__file__).parent
|
| 44 |
PHRASE_DIR = ROOT / "phrases" # hand-written everyday phrases
|
| 45 |
+
DATA_DIR = ROOT / "data" # human-written prompts/replies (scripts/fetch_coding_prompts.py)
|
| 46 |
+
SETTINGS_PATH = ROOT / "compile_settings.json"
|
| 47 |
EOS, UNK = "<|endoftext|>", "<unk>" # EOS doubles as BOS and pad, like GPT-2
|
| 48 |
+
EOS_ID, UNK_ID = 0, 1
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
SEED = 1234
|
| 51 |
CODE_DIM = 384
|
|
|
|
|
|
|
| 52 |
MAX_POS = 128
|
| 53 |
ATTN_MARGIN = 30.0 # min attention logit gap (nats) between target and any other position
|
| 54 |
BETA = 100.0 # gate sharpness; matched neurons sit at z = BETA / 2
|
|
|
|
| 60 |
PRE_TOKENIZER = pre_tokenizers.ByteLevel(add_prefix_space=True)
|
| 61 |
|
| 62 |
|
| 63 |
+
@dataclass(frozen=True)
|
| 64 |
+
class Settings:
|
| 65 |
+
n_prev: int = 3 # previous tokens attended to; context = n_prev + current token
|
| 66 |
+
smoothing: str = "wb" # "wb" (Witten-Bell) or "kn" (interpolated Kneser-Ney)
|
| 67 |
+
discount: float = 0.75 # Kneser-Ney absolute discount
|
| 68 |
+
phrase_weight: float = 5.0 # weight of one hand-written phrase (phrases/*.txt)
|
| 69 |
+
prompt_weight: float = 1.0 # weight of one user prompt (data/prompts.txt)
|
| 70 |
+
reply_weight: float = 1.0 # weight of one assistant reply sentence (data/replies.txt)
|
| 71 |
+
max_vocab: int = 3000
|
| 72 |
+
min_word_count: float = 3
|
| 73 |
+
min_count: float | None = None # None: smallest context count that fits param_target
|
| 74 |
+
param_target: int = 400_000_000
|
| 75 |
|
| 76 |
+
@property
|
| 77 |
+
def order(self) -> int:
|
| 78 |
+
return self.n_prev + 1
|
| 79 |
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
def load_settings(name: str) -> Settings:
|
| 82 |
+
return Settings(**json.loads(SETTINGS_PATH.read_text())[name])
|
| 83 |
|
| 84 |
+
|
| 85 |
+
# ---------- text ----------
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def pieces(text: str) -> list[str]:
|
| 89 |
+
return [p for p, _ in PRE_TOKENIZER.pre_tokenize_str(NORMALIZER.normalize_str(text))]
|
| 90 |
|
| 91 |
|
| 92 |
def load_lines(paths: list[Path]) -> list[list[str]]:
|
|
|
|
| 103 |
return out
|
| 104 |
|
| 105 |
|
| 106 |
+
def load_phrases() -> list[list[str]]:
|
| 107 |
+
return load_lines(sorted(PHRASE_DIR.glob("*.txt")))
|
| 108 |
+
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
+
def load_sources() -> dict[str, list[list[str]]]:
|
| 111 |
+
return {
|
| 112 |
+
"phrases": load_phrases(),
|
| 113 |
+
"prompts": load_lines([DATA_DIR / "prompts.txt"]),
|
| 114 |
+
"replies": load_lines([DATA_DIR / "replies.txt"]),
|
| 115 |
+
}
|
| 116 |
|
| 117 |
+
|
| 118 |
+
def heldout_split(lines: list[list[str]], pct: int = 10) -> tuple[list, list]:
|
| 119 |
+
"""Deterministic split by content hash: (train, test)."""
|
| 120 |
+
test = [t for t in lines if zlib.crc32(" ".join(t).encode()) % 100 < pct]
|
| 121 |
+
train = [t for t in lines if zlib.crc32(" ".join(t).encode()) % 100 >= pct]
|
| 122 |
+
return train, test
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# ---------- counting ----------
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def weighted_sources(s: Settings, sources: dict) -> list[tuple[list[list[str]], float]]:
|
| 129 |
+
weights = {"phrases": s.phrase_weight, "prompts": s.prompt_weight, "replies": s.reply_weight}
|
| 130 |
+
return [(sources[name], w) for name, w in weights.items() if w > 0 and name in sources]
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def build_vocab(s: Settings, sources: dict) -> list[str]:
|
| 134 |
+
freq = Counter()
|
| 135 |
+
for lines, w in weighted_sources(s, sources):
|
| 136 |
+
for toks in lines:
|
| 137 |
+
for p in toks:
|
| 138 |
+
freq[p] += w
|
| 139 |
+
by_freq = lambda p: (-freq[p], p) # noqa: E731
|
| 140 |
+
hand = {p for t in sources["phrases"] for p in t} if s.phrase_weight > 0 else set()
|
| 141 |
+
rest = sorted((p for p in freq if p not in hand and freq[p] >= s.min_word_count), key=by_freq)
|
| 142 |
+
words = hand | set(rest[: max(0, s.max_vocab - 2 - len(hand))])
|
| 143 |
+
return [EOS, UNK] + sorted(words, key=by_freq)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def padded_events(seq: list[int], order: int):
|
| 147 |
+
"""(contexts of length 0..order, next token) for every position of a sequence."""
|
| 148 |
+
# `order` BOS tokens: the last one is the real BOS position the model sees.
|
| 149 |
+
padded = [EOS_ID] * order + seq + [EOS_ID]
|
| 150 |
+
for i in range(order - 1, len(padded) - 1):
|
| 151 |
+
yield [tuple(padded[i - k + 1 : i + 1]) if k else () for k in range(order + 1)], padded[i + 1]
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def count_ngrams(weighted: list[tuple[list[int], float]], order: int) -> list[defaultdict]:
|
| 155 |
+
counts = [defaultdict(Counter) for _ in range(order + 1)]
|
| 156 |
for seq, weight in weighted:
|
| 157 |
+
for ctxs, nxt in padded_events(seq, order):
|
| 158 |
+
if nxt == UNK_ID: # never learn to predict <unk>
|
| 159 |
continue
|
| 160 |
for k, ctx in enumerate(ctxs):
|
| 161 |
counts[k][ctx][nxt] += weight
|
| 162 |
return counts
|
| 163 |
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
class NGram:
|
| 166 |
+
"""Interpolated n-gram over the kept contexts; the reference the weights must reproduce.
|
| 167 |
|
| 168 |
+
Witten-Bell: p_k(j) = (c_j + u * p_{k-1}(j)) / (c + u)
|
| 169 |
+
Kneser-Ney: p_k(j) = (c_j - d_j) / c + (sum_j d_j / c) * p_{k-1}(j), d_j = min(D, c_j),
|
| 170 |
+
with continuation counts below the top order (KenLM-style)
|
| 171 |
+
Both have the form p_k(j) = A_j + gamma * p_{k-1}(j), so each context's neuron can write
|
| 172 |
+
log p_k(j) - log(gamma * p_{k-1}(j)) for its seen tokens and 0 elsewhere.
|
| 173 |
+
"""
|
| 174 |
|
| 175 |
+
def __init__(self, raw: list[dict], keep: list[set], vocab_size: int, s: Settings):
|
| 176 |
+
self.s, self.V, self.order = s, vocab_size, s.order
|
| 177 |
+
self.support = {ctx: sum(raw[k][ctx].values()) for k in range(1, self.order + 1) for ctx in keep[k]}
|
| 178 |
+
if s.smoothing == "kn":
|
| 179 |
+
cont = [defaultdict(Counter) for _ in range(self.order)]
|
| 180 |
+
for k in range(self.order):
|
| 181 |
+
for ctx, table in raw[k + 1].items():
|
| 182 |
+
for j in table:
|
| 183 |
+
cont[k][ctx[1:]][j] += 1
|
| 184 |
+
self.tables = [cont[0][()]] + [{c: cont[k][c] for c in keep[k]} for k in range(1, self.order)]
|
| 185 |
+
self.tables.append({c: raw[self.order][c] for c in keep[self.order]})
|
| 186 |
+
elif s.smoothing == "wb":
|
| 187 |
+
self.tables = [raw[0][()]] + [{c: raw[k][c] for c in keep[k]} for k in range(1, self.order + 1)]
|
| 188 |
+
else:
|
| 189 |
+
raise ValueError(s.smoothing)
|
| 190 |
+
for k in range(2, self.order + 1):
|
| 191 |
+
assert all(c[1:] in self.tables[k - 1] for c in self.tables[k]), "backoff chain broken"
|
| 192 |
+
|
| 193 |
+
uni = np.zeros(vocab_size)
|
| 194 |
+
for j, c in self.tables[0].items():
|
| 195 |
uni[j] = c
|
| 196 |
+
logits = np.log(uni + 1.0) # add-one smoothing
|
| 197 |
+
logits[UNK_ID] = UNK_LOGIT
|
| 198 |
self.base_logits = logits - logits[2:].mean() # constant shift; softmax-invariant
|
| 199 |
+
p0 = np.exp(self.base_logits - self.base_logits.max())
|
| 200 |
+
self.p0 = p0 / p0.sum()
|
| 201 |
+
self._prob, self._delta, self._stats = {}, {}, {}
|
| 202 |
+
|
| 203 |
+
def _table_stats(self, ctx: tuple):
|
| 204 |
+
if ctx not in self._stats:
|
| 205 |
+
table = self.tables[len(ctx)][ctx]
|
| 206 |
+
total = sum(table.values())
|
| 207 |
+
if self.s.smoothing == "wb":
|
| 208 |
+
u = len(table)
|
| 209 |
+
self._stats[ctx] = (table, {j: c / (total + u) for j, c in table.items()}, u / (total + u))
|
| 210 |
+
else:
|
| 211 |
+
d = {j: min(self.s.discount, c) for j, c in table.items()}
|
| 212 |
+
self._stats[ctx] = (table, {j: (c - d[j]) / total for j, c in table.items()}, sum(d.values()) / total)
|
| 213 |
+
return self._stats[ctx]
|
| 214 |
|
|
|
|
| 215 |
def prob(self, j: int, ctx: tuple) -> float:
|
| 216 |
+
key = (j, ctx)
|
| 217 |
+
if key not in self._prob:
|
| 218 |
+
if not ctx:
|
| 219 |
+
p = self.p0[j]
|
| 220 |
+
else:
|
| 221 |
+
lower = self.prob(j, ctx[1:])
|
| 222 |
+
if ctx in self.tables[len(ctx)]:
|
| 223 |
+
_, a, gamma = self._table_stats(ctx)
|
| 224 |
+
p = a.get(j, 0.0) + gamma * lower
|
| 225 |
+
else:
|
| 226 |
+
p = lower
|
| 227 |
+
self._prob[key] = p
|
| 228 |
+
return self._prob[key]
|
| 229 |
|
| 230 |
def delta(self, ctx: tuple) -> dict[int, float]:
|
| 231 |
+
"""log p_k - log p_{k-1}, minus the constant log(gamma) shared by unseen tokens."""
|
| 232 |
+
if ctx not in self._delta:
|
| 233 |
+
table, a, gamma = self._table_stats(ctx)
|
| 234 |
+
out = {}
|
| 235 |
+
for j in table:
|
| 236 |
+
q = gamma * self.prob(j, ctx[1:])
|
| 237 |
+
out[j] = math.log(a[j] + q) - math.log(q)
|
| 238 |
+
self._delta[ctx] = out
|
| 239 |
+
return self._delta[ctx]
|
| 240 |
+
|
| 241 |
+
def delta_arrays(self, ctx: tuple) -> tuple[np.ndarray, np.ndarray]:
|
| 242 |
+
d = self.delta(ctx)
|
| 243 |
+
return np.fromiter(d.keys(), dtype=np.int64, count=len(d)), np.fromiter(d.values(), dtype=np.float64, count=len(d))
|
| 244 |
|
| 245 |
def contexts(self) -> list[tuple]:
|
| 246 |
+
return [ctx for k in range(1, self.order + 1) for ctx in sorted(self.tables[k])]
|
| 247 |
|
| 248 |
+
def window(self, ids: list[int]) -> tuple:
|
| 249 |
+
"""Last order tokens (oldest .. current); positions before 0 read BOS like the attention heads."""
|
| 250 |
t = len(ids) - 1
|
| 251 |
+
return tuple(ids[max(t - a, 0)] for a in range(self.s.n_prev, -1, -1))
|
| 252 |
+
|
| 253 |
+
def predict(self, ids: list[int]) -> tuple[np.ndarray, float, int]:
|
| 254 |
+
"""(logits up to a constant, support of the longest matched context, its length)."""
|
| 255 |
+
window = self.window(ids)
|
| 256 |
+
logits = self.base_logits.copy()
|
| 257 |
+
support, length = 0.0, 0
|
| 258 |
+
for k in range(1, self.order + 1):
|
| 259 |
+
ctx = window[-k:]
|
| 260 |
+
if ctx in self.tables[k]:
|
| 261 |
+
idx, vals = self.delta_arrays(ctx)
|
| 262 |
+
logits[idx] += vals
|
| 263 |
+
support, length = self.support[ctx], k
|
| 264 |
+
return logits, support, length
|
| 265 |
+
|
| 266 |
+
def reference_logits(self, ids: list[int]) -> torch.Tensor:
|
| 267 |
+
return torch.from_numpy(self.predict(ids)[0])
|
| 268 |
|
| 269 |
|
| 270 |
+
def compile_tables(s: Settings, sources: dict):
|
| 271 |
+
"""vocab, NGram and stats for settings s (min_count=None fits the dense param budget)."""
|
| 272 |
+
vocab = build_vocab(s, sources)
|
| 273 |
+
index = {p: i for i, p in enumerate(vocab)}
|
| 274 |
+
weighted = [([index.get(p, UNK_ID) for p in toks], w) for lines, w in weighted_sources(s, sources) for toks in lines]
|
| 275 |
+
raw = count_ngrams(weighted, s.order)
|
| 276 |
+
forced = set()
|
| 277 |
+
if s.phrase_weight > 0:
|
| 278 |
+
for toks in sources["phrases"]:
|
| 279 |
+
for ctxs, _ in padded_events([index[p] for p in toks], s.order):
|
| 280 |
+
forced.update(ctxs[1:])
|
| 281 |
+
|
| 282 |
+
totals = {ctx: sum(t.values()) for k in range(1, s.order + 1) for ctx, t in raw[k].items()}
|
| 283 |
+
min_count = s.min_count
|
| 284 |
+
if min_count is None:
|
| 285 |
+
# Smallest count threshold whose kept neurons (forced + total >= threshold) fit the budget.
|
| 286 |
+
hist = Counter(0 if ctx in forced else c for ctx, c in totals.items())
|
| 287 |
+
thresholds = sorted(c for c in hist if c > 0)
|
| 288 |
+
running, kept_at = hist[0], {}
|
| 289 |
+
for c in reversed(thresholds):
|
| 290 |
+
running += hist[c]
|
| 291 |
+
kept_at[c] = running
|
| 292 |
+
fits = [c for c in thresholds if dense_params(len(vocab), s.n_prev, 1 + kept_at[c]) <= s.param_target]
|
| 293 |
+
if not fits:
|
| 294 |
+
raise SystemExit("hand-written contexts alone exceed param_target; lower max_vocab")
|
| 295 |
+
min_count = fits[0]
|
| 296 |
+
keep = [set()] + [{c for c in raw[k] if c in forced or totals[c] >= min_count} for k in range(1, s.order + 1)]
|
| 297 |
+
ngram = NGram(raw, keep, len(vocab), s)
|
| 298 |
+
stats = {
|
| 299 |
+
"vocab": len(vocab),
|
| 300 |
+
"min_context_count": min_count,
|
| 301 |
+
"contexts_seen": len(totals),
|
| 302 |
+
"contexts_kept": sum(len(k) for k in keep),
|
| 303 |
+
}
|
| 304 |
+
return vocab, ngram, stats
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
# ---------- dense transformer ----------
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def hidden_size(V: int, n_prev: int) -> int:
|
| 311 |
+
# transformers validates hidden_size % num_heads == 0; pad with unused zero dims.
|
| 312 |
+
raw = 1 + (n_prev + 1) * CODE_DIM + V
|
| 313 |
+
return -(-raw // (8 * n_prev)) * (8 * n_prev)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def dense_params(V: int, n_prev: int, neurons: int) -> int:
|
| 317 |
+
D, h = hidden_size(V, n_prev), CODE_DIM
|
| 318 |
+
return 2 * V * D + (2 * n_prev + 2) * h * D + 3 * neurons * D + 3 * D
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def rope_attention_scale(head_dim: int, n_prev: int) -> tuple[float, torch.Tensor, float]:
|
| 322 |
"""Pick rope_theta maximizing the worst-case attention margin; return (theta, inv_freq, q_amp)."""
|
| 323 |
best = None
|
| 324 |
for theta in ROPE_THETA_CANDIDATES:
|
|
|
|
| 326 |
offsets = torch.arange(-MAX_POS, MAX_POS + 1, dtype=torch.float64)
|
| 327 |
kernel = torch.cos(offsets[:, None] * inv_freq[None, :]).sum(1) # K(x), index x + MAX_POS
|
| 328 |
worst = math.inf
|
| 329 |
+
for lag in range(1, n_prev + 1):
|
| 330 |
for t in range(MAX_POS):
|
| 331 |
s_target = max(t - lag, 0)
|
| 332 |
xs = torch.tensor([t - s - lag for s in range(t + 1)]) + MAX_POS
|
|
|
|
| 343 |
return theta, inv_freq, q_amp
|
| 344 |
|
| 345 |
|
| 346 |
+
def build_dense(vocab: list[str], ngram: NGram, s: Settings):
|
| 347 |
+
V, m, n_prev = len(vocab), CODE_DIM, s.n_prev
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
contexts = ngram.contexts()
|
| 349 |
+
N = 1 + len(contexts)
|
| 350 |
+
CONST, CUR = 0, 1
|
| 351 |
+
prev_slot = [1 + a * m for a in range(n_prev + 1)] # prev_slot[0] is the cur slot
|
| 352 |
+
OUT = 1 + (n_prev + 1) * m
|
| 353 |
+
n_heads, h = n_prev, m
|
| 354 |
+
D = hidden_size(V, n_prev)
|
| 355 |
|
| 356 |
gen = torch.Generator().manual_seed(SEED)
|
| 357 |
codes = torch.randn(V, m, generator=gen, dtype=torch.float64)
|
|
|
|
| 365 |
embed[:, CONST] = 1.0
|
| 366 |
embed[:, CUR : CUR + m] = codes
|
| 367 |
|
| 368 |
+
theta, inv_freq, q_amp = rope_attention_scale(h, n_prev)
|
| 369 |
half = h // 2
|
| 370 |
q_proj = torch.zeros(n_heads * h, D, dtype=torch.float64)
|
| 371 |
for a in range(n_heads):
|
|
|
|
| 387 |
on = (BETA / 2) * torch.sigmoid(torch.tensor(BETA / 2, dtype=torch.float64)).item() # silu(BETA/2)
|
| 388 |
up[:, CONST] = 1.0
|
| 389 |
gate[0, CONST] = BETA / 2 # always-on unigram neuron
|
| 390 |
+
down[OUT : OUT + V, 0] = torch.from_numpy(OUT_SCALE * ngram.base_logits / on).float()
|
| 391 |
for n, ctx in enumerate(contexts, start=1):
|
| 392 |
k = len(ctx)
|
| 393 |
for a, tok in enumerate(reversed(ctx)): # a = 0 current, a = 1 previous, ...
|
|
|
|
| 400 |
lm_head[torch.arange(V), OUT + torch.arange(V)] = 1.0 / OUT_SCALE
|
| 401 |
|
| 402 |
# RMSNorm weights undo the norm for the (fixed) input norms: |x|^2 = 2 before
|
| 403 |
+
# attention, 1 + 1 + n_prev after it.
|
| 404 |
attn_norm = math.sqrt(2 / D)
|
| 405 |
+
mlp_norm = math.sqrt((2 + n_prev) / D)
|
| 406 |
state = {
|
| 407 |
"model.embed_tokens.weight": embed,
|
| 408 |
"model.layers.0.input_layernorm.weight": torch.full((D,), attn_norm),
|
|
|
|
| 437 |
pad_token_id=0,
|
| 438 |
)
|
| 439 |
info = {
|
|
|
|
| 440 |
"hidden": D,
|
| 441 |
"neurons": N,
|
|
|
|
|
|
|
| 442 |
"rope_theta": theta,
|
| 443 |
"max_code_coherence": round(max_coherence, 3),
|
| 444 |
"params": sum(t.numel() for t in state.values()),
|
| 445 |
}
|
| 446 |
+
return state, config, info
|
| 447 |
|
| 448 |
|
| 449 |
def build_tokenizer(vocab: list[str]) -> PreTrainedTokenizerFast:
|
|
|
|
| 464 |
|
| 465 |
|
| 466 |
def compile_model():
|
| 467 |
+
"""Build the dense ("hf") model in memory (used by the tests for the n-gram reference)."""
|
| 468 |
+
s = load_settings("hf")
|
| 469 |
+
sources = load_sources()
|
| 470 |
+
vocab, ngram, stats = compile_tables(s, sources)
|
| 471 |
+
state, config, info = build_dense(vocab, ngram, s)
|
| 472 |
+
lines = {name: len(v) for name, v in sources.items()}
|
| 473 |
+
return lines, vocab, ngram, state, config, {**stats, **info}
|
| 474 |
|
| 475 |
|
| 476 |
def main() -> None:
|
| 477 |
+
lines, vocab, _, state, config, info = compile_model()
|
| 478 |
assert info["params"] <= 1_000_000_000, info
|
| 479 |
|
| 480 |
save_file(state, ROOT / "model.safetensors", metadata={"format": "pt"})
|
|
|
|
| 499 |
tok_cfg["model_input_names"] = ["input_ids", "attention_mask"]
|
| 500 |
tok_cfg_path.write_text(json.dumps(tok_cfg, indent=2) + "\n")
|
| 501 |
|
| 502 |
+
print(json.dumps({"lines": lines, **info}, indent=2))
|
| 503 |
|
| 504 |
|
| 505 |
if __name__ == "__main__":
|
compile_settings.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": "Chosen by scripts/sweep.py (docs/sweep.json): best keystrokes saved at >= 90% precision on held-out prompts. hf = dense transformers model (vocab capped for width); web = sparse browser export.",
|
| 3 |
+
"hf": {
|
| 4 |
+
"n_prev": 3,
|
| 5 |
+
"smoothing": "kn",
|
| 6 |
+
"phrase_weight": 1.0,
|
| 7 |
+
"prompt_weight": 1.0,
|
| 8 |
+
"reply_weight": 1.0,
|
| 9 |
+
"max_vocab": 3000,
|
| 10 |
+
"min_count": null,
|
| 11 |
+
"param_target": 400000000
|
| 12 |
+
},
|
| 13 |
+
"web": {
|
| 14 |
+
"n_prev": 3,
|
| 15 |
+
"smoothing": "kn",
|
| 16 |
+
"phrase_weight": 1.0,
|
| 17 |
+
"prompt_weight": 1.0,
|
| 18 |
+
"reply_weight": 1.0,
|
| 19 |
+
"max_vocab": 8000,
|
| 20 |
+
"min_count": 2
|
| 21 |
+
},
|
| 22 |
+
"policy": {
|
| 23 |
+
"tau": 0.72,
|
| 24 |
+
"min_support": 0,
|
| 25 |
+
"max_words": 8
|
| 26 |
+
}
|
| 27 |
+
}
|
data/SOURCES.md
CHANGED
|
@@ -1,19 +1,30 @@
|
|
| 1 |
# Data sources
|
| 2 |
|
| 3 |
-
`
|
| 4 |
-
human-written English prose taken from the datasets below. LLM-generated messages are
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
|
| 9 |
-
| Dataset | What is used | License | Attribution |
|
| 10 |
-
|---|---|---|---|
|
| 11 |
-
| [OpenAssistant/oasst2](https://huggingface.co/datasets/OpenAssistant/oasst2) | English
|
| 12 |
-
| [google-research-datasets/mbpp](https://huggingface.co/datasets/google-research-datasets/mbpp) | task descriptions (`text`), all splits | CC-BY-4.0 | Austin et al., *Program Synthesis with Large Language Models*, 2021 |
|
| 13 |
-
| [neulab/conala](https://huggingface.co/datasets/neulab/conala) | curated intents (`intent`), paired train + test | MIT | Yin et al., *Learning to Mine Aligned Code and Natural Language Pairs from Stack Overflow*, MSR 2018 |
|
|
|
|
| 14 |
|
| 15 |
-
Counts from the last fetch:
|
| 16 |
-
unique lines after removing duplicates.
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Data sources
|
| 2 |
|
| 3 |
+
`prompts.txt` and `replies.txt` are produced by `scripts/fetch_coding_prompts.py`. Every line is
|
| 4 |
+
human-written English prose taken from the datasets below. LLM-generated messages are excluded
|
| 5 |
+
(OASST `synthetic` flag). The script keeps only prose: it removes code blocks, code-looking
|
| 6 |
+
lines and markdown, keeps up to 2 sentences per prompt and 5 per reply, and keeps sentences
|
| 7 |
+
of 3–24 words.
|
| 8 |
|
| 9 |
+
| File | Dataset | What is used | License | Attribution |
|
| 10 |
+
|---|---|---|---|---|
|
| 11 |
+
| `prompts.txt` | [OpenAssistant/oasst2](https://huggingface.co/datasets/OpenAssistant/oasst2) | English **user** messages (all turns, reviewed, not deleted, not synthetic) from conversations whose opening prompt is about code | Apache-2.0 | Köpf et al., *OpenAssistant Conversations: Democratizing Large Language Model Alignment*, 2023 |
|
| 12 |
+
| `prompts.txt` | [google-research-datasets/mbpp](https://huggingface.co/datasets/google-research-datasets/mbpp) | task descriptions (`text`), all splits | CC-BY-4.0 | Austin et al., *Program Synthesis with Large Language Models*, 2021 |
|
| 13 |
+
| `prompts.txt` | [neulab/conala](https://huggingface.co/datasets/neulab/conala) | curated intents (`intent`), paired train + test | MIT | Yin et al., *Learning to Mine Aligned Code and Natural Language Pairs from Stack Overflow*, MSR 2018 |
|
| 14 |
+
| `replies.txt` | [OpenAssistant/oasst2](https://huggingface.co/datasets/OpenAssistant/oasst2) | English **assistant** messages, written by human volunteers, from the same conversations | Apache-2.0 | as above |
|
| 15 |
|
| 16 |
+
Counts from the last fetch:
|
|
|
|
| 17 |
|
| 18 |
+
| Source | Sentences |
|
| 19 |
+
|---|---|
|
| 20 |
+
| OASST user turns | 4,169 |
|
| 21 |
+
| MBPP | 938 |
|
| 22 |
+
| CoNaLa | 2,820 |
|
| 23 |
+
| OASST assistant turns | 13,183 |
|
| 24 |
+
|
| 25 |
+
After removing duplicates: **7,133** lines in `prompts.txt` and **12,841** in `replies.txt`.
|
| 26 |
+
|
| 27 |
+
The weights contain only n-gram statistics derived from these lines, plus the hand-written
|
| 28 |
+
phrases in `../phrases/`. No dataset was used to train anything. 10% of `prompts.txt` (chosen
|
| 29 |
+
by content hash) is held out when measuring precision and keystrokes saved
|
| 30 |
+
(`scripts/sweep.py`, `scripts/benchmark.py`).
|
data/prompts.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/{coding_prompts.txt → replies.txt}
RENAMED
|
The diff for this file is too large to render.
See raw diff
|
|
|
docs/benchmarks.json
CHANGED
|
@@ -7,41 +7,123 @@
|
|
| 7 |
"python": "3.13.4",
|
| 8 |
"torch": "2.15.0"
|
| 9 |
},
|
| 10 |
-
"
|
| 11 |
-
"
|
| 12 |
-
"
|
| 13 |
-
6.
|
| 14 |
-
|
| 15 |
],
|
| 16 |
-
"peak_memory_gb": 2.3,
|
| 17 |
"byte_identical_rebuild": true,
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
| 21 |
-
"
|
|
|
|
|
|
|
| 22 |
"source_lines": 21106,
|
| 23 |
"params": 380290320,
|
| 24 |
"nonzero_params": 26792230,
|
|
|
|
|
|
|
| 25 |
"contexts_seen": 434284,
|
| 26 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
},
|
| 28 |
"pytorch": {
|
| 29 |
-
"load_seconds": 0.
|
| 30 |
-
"next_word_ms":
|
| 31 |
-
"generate_tokens_per_second":
|
| 32 |
"threads": 8
|
| 33 |
},
|
| 34 |
-
"
|
| 35 |
-
"runtime": "
|
| 36 |
-
"
|
| 37 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
},
|
| 39 |
-
"
|
| 40 |
-
|
| 41 |
-
"
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
| 45 |
-
"oov_rate": 0.088
|
| 46 |
}
|
| 47 |
}
|
|
|
|
| 7 |
"python": "3.13.4",
|
| 8 |
"torch": "2.15.0"
|
| 9 |
},
|
| 10 |
+
"compile_dense": {
|
| 11 |
+
"seconds": 6.9,
|
| 12 |
+
"runs": [
|
| 13 |
+
6.9,
|
| 14 |
+
7.0
|
| 15 |
],
|
|
|
|
| 16 |
"byte_identical_rebuild": true,
|
| 17 |
+
"sha256": "ae857db3e321c35782192c198a7778987964e9cc147d2c67428fea9bf5777100",
|
| 18 |
+
"mb": 1521.16,
|
| 19 |
+
"peak_memory_gb": 2.4,
|
| 20 |
+
"tokenize_seconds": 0.48,
|
| 21 |
+
"count_seconds": 2.37,
|
| 22 |
+
"build_tensors_seconds": 1.07,
|
| 23 |
"source_lines": 21106,
|
| 24 |
"params": 380290320,
|
| 25 |
"nonzero_params": 26792230,
|
| 26 |
+
"vocab": 3000,
|
| 27 |
+
"min_context_count": 8.0,
|
| 28 |
"contexts_seen": 434284,
|
| 29 |
+
"contexts_kept": 24773
|
| 30 |
+
},
|
| 31 |
+
"compile_web": {
|
| 32 |
+
"seconds": 6.6,
|
| 33 |
+
"runs": [
|
| 34 |
+
7.2,
|
| 35 |
+
6.6
|
| 36 |
+
],
|
| 37 |
+
"byte_identical_rebuild": true,
|
| 38 |
+
"sha256": "d770bf4ec6644ccab5646e2b1244b58b185378b371c52b1c263b5a0ebf3f8499",
|
| 39 |
+
"mb": 4.49,
|
| 40 |
+
"contexts": 107864,
|
| 41 |
+
"vocab": 6612
|
| 42 |
},
|
| 43 |
"pytorch": {
|
| 44 |
+
"load_seconds": 0.1,
|
| 45 |
+
"next_word_ms": 57.9,
|
| 46 |
+
"generate_tokens_per_second": 18.0,
|
| 47 |
"threads": 8
|
| 48 |
},
|
| 49 |
+
"web_keystroke_latency": {
|
| 50 |
+
"runtime": "web/autocomplete.ts on Bun",
|
| 51 |
+
"median_ms": 0.5,
|
| 52 |
+
"p95_ms": 0.92,
|
| 53 |
+
"max_ms": 3.48
|
| 54 |
+
},
|
| 55 |
+
"max_logprob_error_vs_ngram": 1.6793374655321713e-05,
|
| 56 |
+
"heldout_typing_web": {
|
| 57 |
+
"policy": {
|
| 58 |
+
"tau": 0.72,
|
| 59 |
+
"min_support": 0,
|
| 60 |
+
"max_words": 8
|
| 61 |
+
},
|
| 62 |
+
"heldout_prompts": 711,
|
| 63 |
+
"characters": 42185,
|
| 64 |
+
"tau=0.72": {
|
| 65 |
+
"precision": 0.9029,
|
| 66 |
+
"coverage": 0.3866,
|
| 67 |
+
"keystrokes_saved": 0.2296,
|
| 68 |
+
"shown": 16034
|
| 69 |
+
},
|
| 70 |
+
"tau=0.8": {
|
| 71 |
+
"precision": 0.9351,
|
| 72 |
+
"coverage": 0.3412,
|
| 73 |
+
"keystrokes_saved": 0.2066,
|
| 74 |
+
"shown": 14153
|
| 75 |
+
},
|
| 76 |
+
"tau=0.9": {
|
| 77 |
+
"precision": 0.9644,
|
| 78 |
+
"coverage": 0.2692,
|
| 79 |
+
"keystrokes_saved": 0.1642,
|
| 80 |
+
"shown": 11165
|
| 81 |
+
},
|
| 82 |
+
"tau=0.95": {
|
| 83 |
+
"precision": 0.9773,
|
| 84 |
+
"coverage": 0.2049,
|
| 85 |
+
"keystrokes_saved": 0.1176,
|
| 86 |
+
"shown": 8497
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
"heldout_typing_dense_tables": {
|
| 90 |
+
"policy": {
|
| 91 |
+
"tau": 0.72,
|
| 92 |
+
"min_support": 0,
|
| 93 |
+
"max_words": 8
|
| 94 |
+
},
|
| 95 |
+
"heldout_prompts": 711,
|
| 96 |
+
"characters": 42185,
|
| 97 |
+
"tau=0.72": {
|
| 98 |
+
"precision": 0.9243,
|
| 99 |
+
"coverage": 0.3245,
|
| 100 |
+
"keystrokes_saved": 0.1902,
|
| 101 |
+
"shown": 13458
|
| 102 |
+
},
|
| 103 |
+
"tau=0.8": {
|
| 104 |
+
"precision": 0.9516,
|
| 105 |
+
"coverage": 0.2698,
|
| 106 |
+
"keystrokes_saved": 0.1574,
|
| 107 |
+
"shown": 11191
|
| 108 |
+
},
|
| 109 |
+
"tau=0.9": {
|
| 110 |
+
"precision": 0.9736,
|
| 111 |
+
"coverage": 0.1921,
|
| 112 |
+
"keystrokes_saved": 0.1057,
|
| 113 |
+
"shown": 7966
|
| 114 |
+
},
|
| 115 |
+
"tau=0.95": {
|
| 116 |
+
"precision": 0.9843,
|
| 117 |
+
"coverage": 0.1322,
|
| 118 |
+
"keystrokes_saved": 0.0698,
|
| 119 |
+
"shown": 5481
|
| 120 |
+
}
|
| 121 |
},
|
| 122 |
+
"heldout_next_token_dense": {
|
| 123 |
+
"heldout_lines": 711,
|
| 124 |
+
"heldout_tokens": 9466,
|
| 125 |
+
"next_token_top1": 0.267,
|
| 126 |
+
"next_token_top3": 0.406,
|
| 127 |
+
"oov_rate": 0.07
|
|
|
|
| 128 |
}
|
| 129 |
}
|
docs/experiment_topic.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"topic_words": 48,
|
| 3 |
+
"results": [
|
| 4 |
+
{
|
| 5 |
+
"lambda": 0.0,
|
| 6 |
+
"tau": 0.72,
|
| 7 |
+
"min_support": 0,
|
| 8 |
+
"precision": 0.9029,
|
| 9 |
+
"coverage": 0.3866,
|
| 10 |
+
"keystrokes_saved": 0.2296,
|
| 11 |
+
"shown": 16034
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"lambda": 0.25,
|
| 15 |
+
"tau": 0.72,
|
| 16 |
+
"min_support": 0,
|
| 17 |
+
"precision": 0.9032,
|
| 18 |
+
"coverage": 0.3864,
|
| 19 |
+
"keystrokes_saved": 0.2297,
|
| 20 |
+
"shown": 16026
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"lambda": 0.5,
|
| 24 |
+
"tau": 0.72,
|
| 25 |
+
"min_support": 0,
|
| 26 |
+
"precision": 0.9031,
|
| 27 |
+
"coverage": 0.386,
|
| 28 |
+
"keystrokes_saved": 0.2293,
|
| 29 |
+
"shown": 16007
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"lambda": 1.0,
|
| 33 |
+
"tau": 0.73,
|
| 34 |
+
"min_support": 0,
|
| 35 |
+
"precision": 0.904,
|
| 36 |
+
"coverage": 0.3811,
|
| 37 |
+
"keystrokes_saved": 0.2281,
|
| 38 |
+
"shown": 15804
|
| 39 |
+
}
|
| 40 |
+
]
|
| 41 |
+
}
|
docs/how-it-works.md
CHANGED
|
@@ -1,60 +1,62 @@
|
|
| 1 |
# How tiny-autocomplete was built
|
| 2 |
|
| 3 |
-
This model was **compiled, not trained**. No gradient descent, no optimizer, no
|
| 4 |
-
|
| 5 |
-
text into a standard `LlamaForCausalLM` checkpoint using
|
| 6 |
-
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
The network computes an **interpolated
|
| 9 |
-
|
|
|
|
| 10 |
|
| 11 |
## 1. Source text and tokenizer
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
list was written for this project by an AI coding assistant (Claude). Each phrase counts
|
| 18 |
-
as **5** occurrences, and all of its contexts are always kept (section 5).
|
| 19 |
-
- **`data/coding_prompts.txt`:** 19,967 human-written English sentences about programming.
|
| 20 |
-
`scripts/fetch_coding_prompts.py` downloads them from OpenAssistant oasst2 (prompts and
|
| 21 |
-
replies in coding conversations, with synthetic messages excluded), MBPP and CoNaLa. It
|
| 22 |
-
strips code and markdown and keeps short prose sentences. See `data/SOURCES.md` for
|
| 23 |
-
licenses and attribution. Each line counts once.
|
| 24 |
|
| 25 |
The tokenizer is word-level with GPT-2 style byte-level pre-tokenization:
|
| 26 |
|
| 27 |
- NFKC normalization, then lowercase.
|
| 28 |
- Split on the GPT-2 regex. Words keep their leading space as `Ġ`, and punctuation and
|
| 29 |
contractions (`'s`, `'t`, ...) become their own tokens.
|
| 30 |
-
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
| 35 |
- A post-processor prepends BOS, so every prompt starts at position 0 with `<|endoftext|>`.
|
| 36 |
|
| 37 |
## 2. The reference n-gram
|
| 38 |
|
| 39 |
-
Each line becomes `[BOS
|
| 40 |
-
|
| 41 |
-
(weighted by source).
|
| 42 |
|
| 43 |
- **Order 0:** add-one-smoothed unigram, with `<unk>` pinned at logit -30.
|
| 44 |
-
- **Order k ≥ 1:**
|
| 45 |
-
|
| 46 |
|
| 47 |
```
|
| 48 |
-
|
|
|
|
| 49 |
```
|
| 50 |
|
| 51 |
-
Here `
|
| 52 |
-
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
The model must reproduce `p_K(· | longest kept context)`
|
| 55 |
-
longest context that has a neuron (section 5).
|
| 56 |
|
| 57 |
-
## 3. The residual stream as named slots
|
| 58 |
|
| 59 |
| Dims | Slot | Written by |
|
| 60 |
|---|---|---|
|
|
@@ -66,20 +68,19 @@ longest context that has a neuron (section 5).
|
|
| 66 |
| 1537–4536 | **logits**, one dim per vocab entry, scaled by 1e-5 | MLP |
|
| 67 |
| 4537–4559 | unused padding (transformers wants hidden % heads == 0) | — |
|
| 68 |
|
| 69 |
-
**Token codes** are seeded Gaussian vectors in 384 dimensions, normalized to unit
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
be confused.
|
| 73 |
|
| 74 |
## 4. Attention: previous-token heads from RoPE alone
|
| 75 |
|
| 76 |
-
Llama has no absolute positions. The only position signal is RoPE, which rotates query
|
| 77 |
-
|
| 78 |
|
| 79 |
-
- The **key** for every token is the same constant (read from the constant dim): 1 on
|
| 80 |
-
|
| 81 |
- The **query** for head *a* is also constant, with each pair's phase set to
|
| 82 |
-
`-a * inv_freq[i]`.
|
| 83 |
|
| 84 |
```
|
| 85 |
score(t, s) = A * Σ_i cos((t - s - a) * inv_freq[i]) / sqrt(head_dim)
|
|
@@ -88,91 +89,154 @@ and key pairs by `position * inv_freq[i]`.
|
|
| 88 |
which peaks exactly when `s = t - a`.
|
| 89 |
- When `t < a` there is no position `a` back, and the closest position is 0, which is BOS.
|
| 90 |
The model then "sees" BOS in that slot, matching the BOS padding used when counting.
|
| 91 |
-
- `rope_theta` is picked
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
> 0.9999999 weight on the target.
|
| 95 |
- **Value** = the current-token code. **Output projection** = identity into slot *a*.
|
| 96 |
|
| 97 |
-
All
|
| 98 |
|
| 99 |
## 5. MLP: one neuron per kept context
|
| 100 |
|
| 101 |
-
Llama's MLP is `down(silu(gate(x)) * up(x))`. The compiler creates one neuron for each
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
**Pruning to a budget.** The sources contain 434,284 distinct contexts, and each neuron
|
| 105 |
-
costs 3 × hidden ≈ 13.7k parameters. A context is kept if it was seen at least
|
| 106 |
-
`min_count` times or comes from the hand-written phrases. `min_count` is raised from 1
|
| 107 |
-
until the model fits `PARAM_TARGET` (400M). It came out to **8**, keeping 24,773
|
| 108 |
-
contexts. Pruning never breaks a backoff chain: a context's shorter suffix is always seen
|
| 109 |
-
at least as often, and suffixes of hand-written contexts are hand-written. The reference
|
| 110 |
-
n-gram uses the same kept set, so the model still matches it exactly.
|
| 111 |
-
|
| 112 |
-
For a context `(c_{k-1}, ..., c_1, c_0)` of length k (c_0 = current token):
|
| 113 |
|
| 114 |
- **gate** = `100 * (code(c_0)·cur + code(c_1)·prev1 + ... - (k - 0.5) * const)`.
|
| 115 |
-
If all k tokens match, this is `+50`.
|
| 116 |
-
`100 * (0.26 - 0.5) = -24`, and `silu(-24)` ≈ -1e-9,
|
|
|
|
| 117 |
- **up** = 1 (the constant dim).
|
| 118 |
-
- **down** writes, for each token j seen after
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
| 121 |
-
delta_j = log p_k(j | ctx) - log p_{k-1}(j | ctx[1:])
|
| 122 |
-
```
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
-
|
| 143 |
|
| 144 |
-
|
| 145 |
-
- **Hugging Face loading:** the files load with `AutoModelForCausalLM`, `AutoTokenizer` and
|
| 146 |
-
`pipeline("text-generation")` on transformers 4.52 and 5.22.
|
| 147 |
-
- **Exactness:** next-token log-probs match the reference n-gram within 1e-3 nats (observed
|
| 148 |
-
~3e-5). This is checked on 200 phrase prefixes and 50 random word-salad sequences of up to
|
| 149 |
-
80 tokens.
|
| 150 |
-
- **Completions:** a set of prompts produce the expected greedy completions.
|
| 151 |
-
- **Unknown words:** out-of-vocabulary words back off cleanly.
|
| 152 |
-
- **TypeScript parity:** the zero-dependency TypeScript runtime (`model.ts`) gives the same
|
| 153 |
-
top-3 tokens and log-probs as PyTorch.
|
| 154 |
|
| 155 |
-
|
|
|
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|---|---|
|
| 164 |
-
| `
|
| 165 |
-
| `
|
| 166 |
-
| `
|
| 167 |
-
| `
|
| 168 |
-
| `
|
| 169 |
-
| `
|
| 170 |
-
| `ATTN_MARGIN`, `BETA` | sharpness of attention and of the neuron gates |
|
| 171 |
|
| 172 |
## History
|
| 173 |
|
| 174 |
1. **120 parameters** (hidden size 2): one fixed next word for each of 22 words, with no
|
| 175 |
context.
|
| 176 |
-
2. **91M parameters:** the compiled 5-gram over
|
| 177 |
-
3. **380M parameters
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# How tiny-autocomplete was built
|
| 2 |
|
| 3 |
+
This model was **compiled, not trained**. No gradient descent, no optimizer, no pretrained
|
| 4 |
+
weights, and no other model's outputs were used.
|
| 5 |
+
- `build_weights.py` turns text into a standard `LlamaForCausalLM` checkpoint using
|
| 6 |
+
closed-form rules.
|
| 7 |
+
- `export_web.py` writes the same kind of network in sparse form for the browser.
|
| 8 |
+
- Rebuilds are byte-identical.
|
| 9 |
|
| 10 |
+
The network computes an **interpolated Kneser-Ney 5-gram language model** exactly. It predicts
|
| 11 |
+
the next token from the current token and the 3 before it. Witten-Bell smoothing and other
|
| 12 |
+
context lengths are supported too; the choice was made by measurement (section 9).
|
| 13 |
|
| 14 |
## 1. Source text and tokenizer
|
| 15 |
|
| 16 |
+
| Source | Lines | Weight |
|
| 17 |
+
|---|---|---|
|
| 18 |
+
| `phrases/*.txt`: everyday English, written for this project by an AI coding assistant (Claude) | 1,139 | 1 |
|
| 19 |
+
| `data/prompts.txt`: what people type to an assistant (OASST user turns, MBPP, CoNaLa) | 7,133 | 1 |
|
| 20 |
+
| `data/replies.txt`: human-written assistant replies from OASST coding conversations | 12,834 | 1 |
|
| 21 |
|
| 22 |
+
`scripts/fetch_coding_prompts.py` downloads the data, removes code blocks, code-looking lines and
|
| 23 |
+
markdown, and keeps short prose sentences. Licenses and attribution are in `data/SOURCES.md`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
The tokenizer is word-level with GPT-2 style byte-level pre-tokenization:
|
| 26 |
|
| 27 |
- NFKC normalization, then lowercase.
|
| 28 |
- Split on the GPT-2 regex. Words keep their leading space as `Ġ`, and punctuation and
|
| 29 |
contractions (`'s`, `'t`, ...) become their own tokens.
|
| 30 |
+
- Vocabulary:
|
| 31 |
+
- every hand-written word, then the most frequent data words seen ≥ 3 times;
|
| 32 |
+
- capped at **3,000** for the dense model, where each word costs one hidden dimension;
|
| 33 |
+
- capped at 8,000 for the browser export, where **6,612** words qualify;
|
| 34 |
+
- two specials: `<|endoftext|>` (id 0, used as BOS, EOS and pad) and `<unk>` (id 1);
|
| 35 |
+
- rarer words become `<unk>`, which can appear inside contexts but is never predicted.
|
| 36 |
- A post-processor prepends BOS, so every prompt starts at position 0 with `<|endoftext|>`.
|
| 37 |
|
| 38 |
## 2. The reference n-gram
|
| 39 |
|
| 40 |
+
Each line becomes `[BOS × 4, w1, ..., wn, EOS]`. For every position, the compiler counts which
|
| 41 |
+
token follows each context of length k = 0..4 that ends there, weighted by source.
|
|
|
|
| 42 |
|
| 43 |
- **Order 0:** add-one-smoothed unigram, with `<unk>` pinned at logit -30.
|
| 44 |
+
- **Order k ≥ 1:** interpolate with the next-shorter context `ctx[1:]`. Both smoothing methods
|
| 45 |
+
have the form `p_k(j) = A_j + γ · p_{k-1}(j)`:
|
| 46 |
|
| 47 |
```
|
| 48 |
+
Witten-Bell: A_j = c_j / (c + u) γ = u / (c + u)
|
| 49 |
+
Kneser-Ney: A_j = (c_j - d_j) / c, d_j = min(0.75, c_j) γ = Σ d_j / c
|
| 50 |
```
|
| 51 |
|
| 52 |
+
Here `c_j` is the count of token j after the context, `c` the total, and `u` the number of
|
| 53 |
+
distinct next tokens. Kneser-Ney uses **continuation counts** (how many distinct tokens
|
| 54 |
+
precede `ctx + j`) for every order below the top one, as KenLM does. An unseen or pruned
|
| 55 |
+
context backs off: `p_k = p_{k-1}`.
|
| 56 |
|
| 57 |
+
The model must reproduce `p_K(· | longest kept context)`.
|
|
|
|
| 58 |
|
| 59 |
+
## 3. The residual stream as named slots (dense model)
|
| 60 |
|
| 61 |
| Dims | Slot | Written by |
|
| 62 |
|---|---|---|
|
|
|
|
| 68 |
| 1537–4536 | **logits**, one dim per vocab entry, scaled by 1e-5 | MLP |
|
| 69 |
| 4537–4559 | unused padding (transformers wants hidden % heads == 0) | — |
|
| 70 |
|
| 71 |
+
**Token codes** are seeded Gaussian vectors in 384 dimensions, normalized to unit length. With
|
| 72 |
+
3,000 of them, the largest |cosine| between two different codes is 0.256. That's far enough
|
| 73 |
+
below 1 that a "match" (dot = 1) and a "non-match" (dot ≤ 0.26) can't be confused.
|
|
|
|
| 74 |
|
| 75 |
## 4. Attention: previous-token heads from RoPE alone
|
| 76 |
|
| 77 |
+
Llama has no absolute positions. The only position signal is RoPE, which rotates query and key
|
| 78 |
+
pairs by `position * inv_freq[i]`.
|
| 79 |
|
| 80 |
+
- The **key** for every token is the same constant (read from the constant dim): 1 on the
|
| 81 |
+
first half of each RoPE pair.
|
| 82 |
- The **query** for head *a* is also constant, with each pair's phase set to
|
| 83 |
+
`-a * inv_freq[i]`. Then
|
| 84 |
|
| 85 |
```
|
| 86 |
score(t, s) = A * Σ_i cos((t - s - a) * inv_freq[i]) / sqrt(head_dim)
|
|
|
|
| 89 |
which peaks exactly when `s = t - a`.
|
| 90 |
- When `t < a` there is no position `a` back, and the closest position is 0, which is BOS.
|
| 91 |
The model then "sees" BOS in that slot, matching the BOS padding used when counting.
|
| 92 |
+
- `rope_theta` is picked to maximize the worst-case gap between the target position and every
|
| 93 |
+
other position, for all t < 128 and every head. It came out to 2. `A` scales that gap to
|
| 94 |
+
**30 nats**.
|
|
|
|
| 95 |
- **Value** = the current-token code. **Output projection** = identity into slot *a*.
|
| 96 |
|
| 97 |
+
All heads share one key/value head (`num_key_value_heads = 1`).
|
| 98 |
|
| 99 |
## 5. MLP: one neuron per kept context
|
| 100 |
|
| 101 |
+
Llama's MLP is `down(silu(gate(x)) * up(x))`. The compiler creates one neuron for each **kept**
|
| 102 |
+
context (lengths 1–4) and one always-on unigram neuron. For a context `(c_{k-1}, ..., c_0)`
|
| 103 |
+
(c_0 = current token):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
- **gate** = `100 * (code(c_0)·cur + code(c_1)·prev1 + ... - (k - 0.5) * const)`.
|
| 106 |
+
- If all k tokens match, this is `+50`.
|
| 107 |
+
- If any one fails, it's at most `100 * (0.26 - 0.5) = -24`, and `silu(-24)` ≈ -1e-9,
|
| 108 |
+
effectively off.
|
| 109 |
- **up** = 1 (the constant dim).
|
| 110 |
+
- **down** writes, for each token j seen after the context, `log p_k(j) − log(γ · p_{k-1}(j))`.
|
| 111 |
+
Every unseen token would get the same `log γ`, which softmax ignores, so the column is sparse.
|
| 112 |
+
|
| 113 |
+
**Why this is exact.** If a context of length k is kept, so are its shorter suffixes, and
|
| 114 |
+
their neurons fire too. The unigram neuron writes `log p_0`, and the neurons for lengths 1..K
|
| 115 |
+
add their deltas. The sum telescopes to `log p_K(· | longest kept context)` plus a constant.
|
| 116 |
+
|
| 117 |
+
**Pruning.** A context is kept if it was seen at least `min_count` times (weighted) or comes
|
| 118 |
+
from the hand-written phrases. A suffix is always seen at least as often as the contexts
|
| 119 |
+
ending in it, so pruning never breaks a backoff chain. The reference n-gram uses the same kept
|
| 120 |
+
set, so the model still matches it exactly.
|
| 121 |
+
- **Dense model:** `min_count` is the smallest value that fits 400M parameters. That's **8**,
|
| 122 |
+
keeping 24,773 of 434,284 contexts. Each neuron costs 3 × hidden ≈ 13.7k parameters.
|
| 123 |
+
- **Browser export:** `min_count` = **2**, keeping 107,864 of 520,490 contexts, because a
|
| 124 |
+
sparse neuron costs only a few bytes.
|
| 125 |
+
|
| 126 |
+
## 6. Norms and output head (dense model)
|
| 127 |
+
|
| 128 |
+
- The residual has a known fixed norm at each point: |x|² = 2 before attention and 5 after.
|
| 129 |
+
Each RMSNorm weight is `sqrt(norm² / hidden)`, so the norms leave the stream unchanged.
|
| 130 |
+
- The MLP writes logits scaled by 1e-5, so they barely change the final norm (< 0.001%). At
|
| 131 |
+
1e-4, the 3,000-entry logit slot shifted the norm enough to cause 1e-2-nat errors on extreme
|
| 132 |
+
logits.
|
| 133 |
+
- `lm_head` is `1e5` times the identity on the logit slot.
|
| 134 |
|
| 135 |
+
## 7. Browser export (`web/model.bin`, `web/autocomplete.ts`)
|
|
|
|
|
|
|
| 136 |
|
| 137 |
+
93% of the dense weights are zero. The attention and embedding weights are fixed by
|
| 138 |
+
construction, and each MLP neuron is fully described by:
|
| 139 |
+
- which tokens its gate matches (its context);
|
| 140 |
+
- which logits its down-projection changes, and by how much.
|
| 141 |
|
| 142 |
+
`export_web.py` stores exactly that, plus the unigram neuron and the vocabulary:
|
| 143 |
+
- **contents:** per context its token ids, its count (support) and the logit changes it
|
| 144 |
+
writes (as float32);
|
| 145 |
+
- **size:** 4.5 MB for 107,864 contexts.
|
| 146 |
|
| 147 |
+
`web/autocomplete.ts` looks up the neurons whose context matches the last 1–4 tokens (hash
|
| 148 |
+
maps per context length), adds their deltas to the unigram logits, and applies softmax.
|
| 149 |
+
- **Speed:** 0.5 ms median per keystroke.
|
| 150 |
+
- **Exactness:** a test exports the dense model's own tables and checks that the runtime's
|
| 151 |
+
top-5 next tokens and log-probs equal PyTorch's.
|
| 152 |
|
| 153 |
+
## 8. Confidence policy (`suggest.py`, mirrored by `web/autocomplete.ts`)
|
| 154 |
+
|
| 155 |
+
Nothing here is learned; the policy reads the compiled probabilities.
|
| 156 |
+
|
| 157 |
+
1. **Candidates.** The last pre-token is the word being typed (just `Ġ` after a space). Only
|
| 158 |
+
vocabulary tokens starting with it are candidates (`fun` → `function`).
|
| 159 |
+
2. **Probability.** Candidate probabilities are renormalized over the candidates, then scaled
|
| 160 |
+
by `1 − P(OOV | prefix)`: the weighted share of source words with this prefix that are
|
| 161 |
+
outside the vocabulary. Without that, a prefix whose only in-vocabulary completion is
|
| 162 |
+
`function` would get 100% even when the user is typing `funky`.
|
| 163 |
+
3. **Show** the top candidate only if its probability ≥ τ (and the matched context's support
|
| 164 |
+
≥ `min_support`; the sweep found 0 best).
|
| 165 |
+
4. **Extend** greedily word by word while the product of probabilities stays ≥ τ, up to 8
|
| 166 |
+
words, stopping at end-of-text or after `. ? ! :`.
|
| 167 |
|
| 168 |
+
τ = 0.72 by default. It's a runtime argument (`ac.suggest(text, {...ac.policy, tau})`).
|
| 169 |
|
| 170 |
+
## 9. Choosing the settings (`scripts/sweep.py`)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
+
The compile rules are fixed. A handful of compile settings and the threshold were picked by
|
| 173 |
+
benchmarking:
|
| 174 |
|
| 175 |
+
1. Hold out 10% of `data/prompts.txt` (711 prompts) and compile without them.
|
| 176 |
+
2. Type each held-out prompt one character at a time. After each keystroke, record the
|
| 177 |
+
suggestion chain.
|
| 178 |
+
3. Score every threshold: **precision** (shown suggestions that exactly match the next
|
| 179 |
+
characters), **coverage**, and **keystrokes saved** (accepting each correct suggestion with
|
| 180 |
+
one Tab).
|
| 181 |
+
4. Keep the setting with the most keystrokes saved at precision ≥ 90%, and an export ≤ 50 MB.
|
| 182 |
|
| 183 |
+
Highlights from `docs/sweep.json` (keystrokes saved at ≥ 90% precision, 0.01 threshold grid):
|
| 184 |
+
|
| 185 |
+
| Setting | Keystrokes saved |
|
| 186 |
+
|---|---|
|
| 187 |
+
| Witten-Bell, 4-token context (phrases ×5) | 21.7% |
|
| 188 |
+
| **Kneser-Ney, 4-token context (phrases ×5)** | **22.5%** |
|
| 189 |
+
| Kneser-Ney, 5 / 6 / 8 / 10-token context (phrases ×5) | 22.2% / 22.0% / 21.6% / 21.6% |
|
| 190 |
+
| no assistant replies (prompts + phrases only) | 20.1–20.9% |
|
| 191 |
+
| assistant replies ×0.3 | 21.8–22.4% |
|
| 192 |
+
| **final: KN, 4 tokens, all sources ×1, vocab 8,000, min_count 2** | **23.0%** |
|
| 193 |
+
| final settings with 5 / 6 / 8 / 10-token context | 22.8% / 22.7% / 22.4% / 22.4% |
|
| 194 |
+
|
| 195 |
+
- **Longer contexts:** they lose slightly. With ~21k lines, 5+-token contexts rarely repeat,
|
| 196 |
+
and when they don't match the model is back to the same 4-token prediction.
|
| 197 |
+
- **Topic head** (`scripts/experiment_topic.py`): a content-addressed head that would carry
|
| 198 |
+
the most recent language or tool name (python, sql, react, …) from anywhere earlier in the
|
| 199 |
+
prompt.
|
| 200 |
+
- Tested as a closed-form log-ratio bias `λ · log p(w | topic) / p(w)`.
|
| 201 |
+
- At best it left keystrokes saved at 23.0% (22.97% at λ = 0.25), and λ = 1 was worse.
|
| 202 |
+
- It wasn't compiled.
|
| 203 |
+
|
| 204 |
+
## 10. Verification (`test_model.py`)
|
| 205 |
+
|
| 206 |
+
- **Hugging Face loading** with `AutoModelForCausalLM`, `AutoTokenizer` and `pipeline` on
|
| 207 |
+
transformers 4.52 and 5.22.
|
| 208 |
+
- **Exactness:** dense log-probs match the reference n-gram within 1e-3 nats (observed ~2e-5)
|
| 209 |
+
on 200 phrase prefixes and 50 random sequences of up to 80 tokens.
|
| 210 |
+
- **Completions:** a set of greedy completions; unknown words back off cleanly.
|
| 211 |
+
- **Dense TypeScript runtime** (`model.ts`): same top-3 tokens and log-probs as PyTorch.
|
| 212 |
+
- **Browser runtime:**
|
| 213 |
+
- the same suggestion chains (text, probability, support) as `suggest.py` on 300 random
|
| 214 |
+
prefixes;
|
| 215 |
+
- the same distribution as PyTorch when given the dense model's tables;
|
| 216 |
+
- `model.bin` ≤ 50 MB.
|
| 217 |
+
|
| 218 |
+
## 11. Changing it
|
| 219 |
+
|
| 220 |
+
Edit `phrases/*.txt` or `data/*.txt` (one sentence per line), or the settings in
|
| 221 |
+
`compile_settings.json`. Then run `python build_weights.py && python export_web.py &&
|
| 222 |
+
python test_model.py`. Run `python scripts/sweep.py` to re-check the settings.
|
| 223 |
+
|
| 224 |
+
| Setting | Meaning |
|
| 225 |
|---|---|
|
| 226 |
+
| `n_prev` | previous tokens attended to (context = n_prev + 1) |
|
| 227 |
+
| `smoothing`, `discount` | `kn` or `wb`; Kneser-Ney discount |
|
| 228 |
+
| `phrase_weight`, `prompt_weight`, `reply_weight` | how much each source counts |
|
| 229 |
+
| `max_vocab`, `min_word_count` | vocabulary cap and minimum word frequency |
|
| 230 |
+
| `min_count` / `param_target` | pruning threshold, or the dense budget to fit it to |
|
| 231 |
+
| `policy.tau`, `policy.min_support`, `policy.max_words` | confidence policy shipped in `model.bin` |
|
|
|
|
| 232 |
|
| 233 |
## History
|
| 234 |
|
| 235 |
1. **120 parameters** (hidden size 2): one fixed next word for each of 22 words, with no
|
| 236 |
context.
|
| 237 |
+
2. **91M parameters:** the compiled 5-gram over 1,139 hand-written phrases.
|
| 238 |
+
3. **380M parameters:** adds ~20k human-written coding sentences, with a vocabulary cap and
|
| 239 |
+
count pruning to fit the budget.
|
| 240 |
+
4. **Current:** Kneser-Ney smoothing; a confidence-gated ghost-text policy; settings chosen by
|
| 241 |
+
held-out precision and keystrokes saved; a 4.5 MB browser export with a zero-dependency
|
| 242 |
+
runtime.
|
docs/sweep.json
ADDED
|
@@ -0,0 +1,2181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"target_precision": 0.9,
|
| 3 |
+
"max_export_mb": 50,
|
| 4 |
+
"heldout_prompts": 711,
|
| 5 |
+
"best": {
|
| 6 |
+
"settings": {
|
| 7 |
+
"n_prev": 3,
|
| 8 |
+
"smoothing": "kn",
|
| 9 |
+
"discount": 0.75,
|
| 10 |
+
"phrase_weight": 1.0,
|
| 11 |
+
"prompt_weight": 1,
|
| 12 |
+
"reply_weight": 1.0,
|
| 13 |
+
"max_vocab": 8000,
|
| 14 |
+
"min_word_count": 3,
|
| 15 |
+
"min_count": 2,
|
| 16 |
+
"param_target": 400000000
|
| 17 |
+
},
|
| 18 |
+
"vocab": 6550,
|
| 19 |
+
"min_context_count": 2,
|
| 20 |
+
"contexts_seen": 509522,
|
| 21 |
+
"contexts_kept": 105467,
|
| 22 |
+
"export_mb": 4.2,
|
| 23 |
+
"best": {
|
| 24 |
+
"tau": 0.72,
|
| 25 |
+
"min_support": 0,
|
| 26 |
+
"precision": 0.9029,
|
| 27 |
+
"coverage": 0.3866,
|
| 28 |
+
"keystrokes_saved": 0.2296,
|
| 29 |
+
"shown": 16034
|
| 30 |
+
},
|
| 31 |
+
"seconds": 14.7
|
| 32 |
+
},
|
| 33 |
+
"runs": [
|
| 34 |
+
{
|
| 35 |
+
"settings": {
|
| 36 |
+
"n_prev": 3,
|
| 37 |
+
"smoothing": "wb",
|
| 38 |
+
"discount": 0.75,
|
| 39 |
+
"phrase_weight": 5,
|
| 40 |
+
"prompt_weight": 1,
|
| 41 |
+
"reply_weight": 1,
|
| 42 |
+
"max_vocab": 8000,
|
| 43 |
+
"min_word_count": 3,
|
| 44 |
+
"min_count": 2,
|
| 45 |
+
"param_target": 400000000
|
| 46 |
+
},
|
| 47 |
+
"vocab": 6550,
|
| 48 |
+
"min_context_count": 2,
|
| 49 |
+
"contexts_seen": 509522,
|
| 50 |
+
"contexts_kept": 105467,
|
| 51 |
+
"export_mb": 4.2,
|
| 52 |
+
"best": {
|
| 53 |
+
"tau": 0.81,
|
| 54 |
+
"min_support": 0,
|
| 55 |
+
"precision": 0.9023,
|
| 56 |
+
"coverage": 0.3647,
|
| 57 |
+
"keystrokes_saved": 0.2173,
|
| 58 |
+
"shown": 15125
|
| 59 |
+
},
|
| 60 |
+
"seconds": 13.3
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"settings": {
|
| 64 |
+
"n_prev": 4,
|
| 65 |
+
"smoothing": "wb",
|
| 66 |
+
"discount": 0.75,
|
| 67 |
+
"phrase_weight": 5,
|
| 68 |
+
"prompt_weight": 1,
|
| 69 |
+
"reply_weight": 1,
|
| 70 |
+
"max_vocab": 8000,
|
| 71 |
+
"min_word_count": 3,
|
| 72 |
+
"min_count": 2,
|
| 73 |
+
"param_target": 400000000
|
| 74 |
+
},
|
| 75 |
+
"vocab": 6550,
|
| 76 |
+
"min_context_count": 2,
|
| 77 |
+
"contexts_seen": 754606,
|
| 78 |
+
"contexts_kept": 125943,
|
| 79 |
+
"export_mb": 4.9,
|
| 80 |
+
"best": {
|
| 81 |
+
"tau": 0.83,
|
| 82 |
+
"min_support": 0,
|
| 83 |
+
"precision": 0.9021,
|
| 84 |
+
"coverage": 0.3555,
|
| 85 |
+
"keystrokes_saved": 0.2116,
|
| 86 |
+
"shown": 14744
|
| 87 |
+
},
|
| 88 |
+
"seconds": 15.0
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"settings": {
|
| 92 |
+
"n_prev": 5,
|
| 93 |
+
"smoothing": "wb",
|
| 94 |
+
"discount": 0.75,
|
| 95 |
+
"phrase_weight": 5,
|
| 96 |
+
"prompt_weight": 1,
|
| 97 |
+
"reply_weight": 1,
|
| 98 |
+
"max_vocab": 8000,
|
| 99 |
+
"min_word_count": 3,
|
| 100 |
+
"min_count": 2,
|
| 101 |
+
"param_target": 400000000
|
| 102 |
+
},
|
| 103 |
+
"vocab": 6550,
|
| 104 |
+
"min_context_count": 2,
|
| 105 |
+
"contexts_seen": 1005811,
|
| 106 |
+
"contexts_kept": 143202,
|
| 107 |
+
"export_mb": 5.4,
|
| 108 |
+
"best": {
|
| 109 |
+
"tau": 0.84,
|
| 110 |
+
"min_support": 0,
|
| 111 |
+
"precision": 0.9019,
|
| 112 |
+
"coverage": 0.3499,
|
| 113 |
+
"keystrokes_saved": 0.2084,
|
| 114 |
+
"shown": 14512
|
| 115 |
+
},
|
| 116 |
+
"seconds": 16.2
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"settings": {
|
| 120 |
+
"n_prev": 7,
|
| 121 |
+
"smoothing": "wb",
|
| 122 |
+
"discount": 0.75,
|
| 123 |
+
"phrase_weight": 5,
|
| 124 |
+
"prompt_weight": 1,
|
| 125 |
+
"reply_weight": 1,
|
| 126 |
+
"max_vocab": 8000,
|
| 127 |
+
"min_word_count": 3,
|
| 128 |
+
"min_count": 2,
|
| 129 |
+
"param_target": 400000000
|
| 130 |
+
},
|
| 131 |
+
"vocab": 6550,
|
| 132 |
+
"min_context_count": 2,
|
| 133 |
+
"contexts_seen": 1514926,
|
| 134 |
+
"contexts_kept": 173445,
|
| 135 |
+
"export_mb": 6.6,
|
| 136 |
+
"best": {
|
| 137 |
+
"tau": 0.84,
|
| 138 |
+
"min_support": 0,
|
| 139 |
+
"precision": 0.9012,
|
| 140 |
+
"coverage": 0.3502,
|
| 141 |
+
"keystrokes_saved": 0.2079,
|
| 142 |
+
"shown": 14524
|
| 143 |
+
},
|
| 144 |
+
"seconds": 18.4
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"settings": {
|
| 148 |
+
"n_prev": 9,
|
| 149 |
+
"smoothing": "wb",
|
| 150 |
+
"discount": 0.75,
|
| 151 |
+
"phrase_weight": 5,
|
| 152 |
+
"prompt_weight": 1,
|
| 153 |
+
"reply_weight": 1,
|
| 154 |
+
"max_vocab": 8000,
|
| 155 |
+
"min_word_count": 3,
|
| 156 |
+
"min_count": 2,
|
| 157 |
+
"param_target": 400000000
|
| 158 |
+
},
|
| 159 |
+
"vocab": 6550,
|
| 160 |
+
"min_context_count": 2,
|
| 161 |
+
"contexts_seen": 2027338,
|
| 162 |
+
"contexts_kept": 201278,
|
| 163 |
+
"export_mb": 7.7,
|
| 164 |
+
"best": {
|
| 165 |
+
"tau": 0.84,
|
| 166 |
+
"min_support": 0,
|
| 167 |
+
"precision": 0.9012,
|
| 168 |
+
"coverage": 0.3502,
|
| 169 |
+
"keystrokes_saved": 0.2079,
|
| 170 |
+
"shown": 14524
|
| 171 |
+
},
|
| 172 |
+
"seconds": 20.2
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"settings": {
|
| 176 |
+
"n_prev": 3,
|
| 177 |
+
"smoothing": "kn",
|
| 178 |
+
"discount": 0.75,
|
| 179 |
+
"phrase_weight": 5,
|
| 180 |
+
"prompt_weight": 1,
|
| 181 |
+
"reply_weight": 1,
|
| 182 |
+
"max_vocab": 8000,
|
| 183 |
+
"min_word_count": 3,
|
| 184 |
+
"min_count": 2,
|
| 185 |
+
"param_target": 400000000
|
| 186 |
+
},
|
| 187 |
+
"vocab": 6550,
|
| 188 |
+
"min_context_count": 2,
|
| 189 |
+
"contexts_seen": 509522,
|
| 190 |
+
"contexts_kept": 105467,
|
| 191 |
+
"export_mb": 4.2,
|
| 192 |
+
"best": {
|
| 193 |
+
"tau": 0.73,
|
| 194 |
+
"min_support": 0,
|
| 195 |
+
"precision": 0.9033,
|
| 196 |
+
"coverage": 0.3819,
|
| 197 |
+
"keystrokes_saved": 0.2251,
|
| 198 |
+
"shown": 15837
|
| 199 |
+
},
|
| 200 |
+
"seconds": 14.1
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"settings": {
|
| 204 |
+
"n_prev": 4,
|
| 205 |
+
"smoothing": "kn",
|
| 206 |
+
"discount": 0.75,
|
| 207 |
+
"phrase_weight": 5,
|
| 208 |
+
"prompt_weight": 1,
|
| 209 |
+
"reply_weight": 1,
|
| 210 |
+
"max_vocab": 8000,
|
| 211 |
+
"min_word_count": 3,
|
| 212 |
+
"min_count": 2,
|
| 213 |
+
"param_target": 400000000
|
| 214 |
+
},
|
| 215 |
+
"vocab": 6550,
|
| 216 |
+
"min_context_count": 2,
|
| 217 |
+
"contexts_seen": 754606,
|
| 218 |
+
"contexts_kept": 125943,
|
| 219 |
+
"export_mb": 4.9,
|
| 220 |
+
"best": {
|
| 221 |
+
"tau": 0.74,
|
| 222 |
+
"min_support": 0,
|
| 223 |
+
"precision": 0.9023,
|
| 224 |
+
"coverage": 0.3779,
|
| 225 |
+
"keystrokes_saved": 0.2224,
|
| 226 |
+
"shown": 15673
|
| 227 |
+
},
|
| 228 |
+
"seconds": 15.7
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"settings": {
|
| 232 |
+
"n_prev": 5,
|
| 233 |
+
"smoothing": "kn",
|
| 234 |
+
"discount": 0.75,
|
| 235 |
+
"phrase_weight": 5,
|
| 236 |
+
"prompt_weight": 1,
|
| 237 |
+
"reply_weight": 1,
|
| 238 |
+
"max_vocab": 8000,
|
| 239 |
+
"min_word_count": 3,
|
| 240 |
+
"min_count": 2,
|
| 241 |
+
"param_target": 400000000
|
| 242 |
+
},
|
| 243 |
+
"vocab": 6550,
|
| 244 |
+
"min_context_count": 2,
|
| 245 |
+
"contexts_seen": 1005811,
|
| 246 |
+
"contexts_kept": 143202,
|
| 247 |
+
"export_mb": 5.4,
|
| 248 |
+
"best": {
|
| 249 |
+
"tau": 0.75,
|
| 250 |
+
"min_support": 0,
|
| 251 |
+
"precision": 0.9029,
|
| 252 |
+
"coverage": 0.3739,
|
| 253 |
+
"keystrokes_saved": 0.2196,
|
| 254 |
+
"shown": 15506
|
| 255 |
+
},
|
| 256 |
+
"seconds": 17.7
|
| 257 |
+
},
|
| 258 |
+
{
|
| 259 |
+
"settings": {
|
| 260 |
+
"n_prev": 7,
|
| 261 |
+
"smoothing": "kn",
|
| 262 |
+
"discount": 0.75,
|
| 263 |
+
"phrase_weight": 5,
|
| 264 |
+
"prompt_weight": 1,
|
| 265 |
+
"reply_weight": 1,
|
| 266 |
+
"max_vocab": 8000,
|
| 267 |
+
"min_word_count": 3,
|
| 268 |
+
"min_count": 2,
|
| 269 |
+
"param_target": 400000000
|
| 270 |
+
},
|
| 271 |
+
"vocab": 6550,
|
| 272 |
+
"min_context_count": 2,
|
| 273 |
+
"contexts_seen": 1514926,
|
| 274 |
+
"contexts_kept": 173445,
|
| 275 |
+
"export_mb": 6.6,
|
| 276 |
+
"best": {
|
| 277 |
+
"tau": 0.76,
|
| 278 |
+
"min_support": 0,
|
| 279 |
+
"precision": 0.9026,
|
| 280 |
+
"coverage": 0.3693,
|
| 281 |
+
"keystrokes_saved": 0.2162,
|
| 282 |
+
"shown": 15315
|
| 283 |
+
},
|
| 284 |
+
"seconds": 21.5
|
| 285 |
+
},
|
| 286 |
+
{
|
| 287 |
+
"settings": {
|
| 288 |
+
"n_prev": 9,
|
| 289 |
+
"smoothing": "kn",
|
| 290 |
+
"discount": 0.75,
|
| 291 |
+
"phrase_weight": 5,
|
| 292 |
+
"prompt_weight": 1,
|
| 293 |
+
"reply_weight": 1,
|
| 294 |
+
"max_vocab": 8000,
|
| 295 |
+
"min_word_count": 3,
|
| 296 |
+
"min_count": 2,
|
| 297 |
+
"param_target": 400000000
|
| 298 |
+
},
|
| 299 |
+
"vocab": 6550,
|
| 300 |
+
"min_context_count": 2,
|
| 301 |
+
"contexts_seen": 2027338,
|
| 302 |
+
"contexts_kept": 201278,
|
| 303 |
+
"export_mb": 7.7,
|
| 304 |
+
"best": {
|
| 305 |
+
"tau": 0.76,
|
| 306 |
+
"min_support": 0,
|
| 307 |
+
"precision": 0.9017,
|
| 308 |
+
"coverage": 0.3693,
|
| 309 |
+
"keystrokes_saved": 0.216,
|
| 310 |
+
"shown": 15316
|
| 311 |
+
},
|
| 312 |
+
"seconds": 24.4
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"settings": {
|
| 316 |
+
"n_prev": 3,
|
| 317 |
+
"smoothing": "kn",
|
| 318 |
+
"discount": 0.75,
|
| 319 |
+
"phrase_weight": 1.0,
|
| 320 |
+
"prompt_weight": 1,
|
| 321 |
+
"reply_weight": 0.0,
|
| 322 |
+
"max_vocab": 8000,
|
| 323 |
+
"min_word_count": 3,
|
| 324 |
+
"min_count": 2,
|
| 325 |
+
"param_target": 400000000
|
| 326 |
+
},
|
| 327 |
+
"vocab": 2830,
|
| 328 |
+
"min_context_count": 2,
|
| 329 |
+
"contexts_seen": 133364,
|
| 330 |
+
"contexts_kept": 33000,
|
| 331 |
+
"export_mb": 1.3,
|
| 332 |
+
"best": {
|
| 333 |
+
"tau": 0.7,
|
| 334 |
+
"min_support": 0,
|
| 335 |
+
"precision": 0.901,
|
| 336 |
+
"coverage": 0.3569,
|
| 337 |
+
"keystrokes_saved": 0.2089,
|
| 338 |
+
"shown": 14803
|
| 339 |
+
},
|
| 340 |
+
"seconds": 8.8
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"settings": {
|
| 344 |
+
"n_prev": 3,
|
| 345 |
+
"smoothing": "kn",
|
| 346 |
+
"discount": 0.75,
|
| 347 |
+
"phrase_weight": 1.0,
|
| 348 |
+
"prompt_weight": 1,
|
| 349 |
+
"reply_weight": 0.3,
|
| 350 |
+
"max_vocab": 8000,
|
| 351 |
+
"min_word_count": 3,
|
| 352 |
+
"min_count": 2,
|
| 353 |
+
"param_target": 400000000
|
| 354 |
+
},
|
| 355 |
+
"vocab": 3841,
|
| 356 |
+
"min_context_count": 2,
|
| 357 |
+
"contexts_seen": 454608,
|
| 358 |
+
"contexts_kept": 40744,
|
| 359 |
+
"export_mb": 2.6,
|
| 360 |
+
"best": {
|
| 361 |
+
"tau": 0.69,
|
| 362 |
+
"min_support": 0,
|
| 363 |
+
"precision": 0.9026,
|
| 364 |
+
"coverage": 0.3784,
|
| 365 |
+
"keystrokes_saved": 0.2238,
|
| 366 |
+
"shown": 15694
|
| 367 |
+
},
|
| 368 |
+
"seconds": 12.2
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
"settings": {
|
| 372 |
+
"n_prev": 3,
|
| 373 |
+
"smoothing": "kn",
|
| 374 |
+
"discount": 0.75,
|
| 375 |
+
"phrase_weight": 1.0,
|
| 376 |
+
"prompt_weight": 1,
|
| 377 |
+
"reply_weight": 1.0,
|
| 378 |
+
"max_vocab": 8000,
|
| 379 |
+
"min_word_count": 3,
|
| 380 |
+
"min_count": 2,
|
| 381 |
+
"param_target": 400000000
|
| 382 |
+
},
|
| 383 |
+
"vocab": 6550,
|
| 384 |
+
"min_context_count": 2,
|
| 385 |
+
"contexts_seen": 509522,
|
| 386 |
+
"contexts_kept": 105467,
|
| 387 |
+
"export_mb": 4.2,
|
| 388 |
+
"best": {
|
| 389 |
+
"tau": 0.72,
|
| 390 |
+
"min_support": 0,
|
| 391 |
+
"precision": 0.9029,
|
| 392 |
+
"coverage": 0.3866,
|
| 393 |
+
"keystrokes_saved": 0.2296,
|
| 394 |
+
"shown": 16034
|
| 395 |
+
},
|
| 396 |
+
"seconds": 14.7
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"settings": {
|
| 400 |
+
"n_prev": 3,
|
| 401 |
+
"smoothing": "kn",
|
| 402 |
+
"discount": 0.75,
|
| 403 |
+
"phrase_weight": 5.0,
|
| 404 |
+
"prompt_weight": 1,
|
| 405 |
+
"reply_weight": 0.0,
|
| 406 |
+
"max_vocab": 8000,
|
| 407 |
+
"min_word_count": 3,
|
| 408 |
+
"min_count": 2,
|
| 409 |
+
"param_target": 400000000
|
| 410 |
+
},
|
| 411 |
+
"vocab": 2830,
|
| 412 |
+
"min_context_count": 2,
|
| 413 |
+
"contexts_seen": 133364,
|
| 414 |
+
"contexts_kept": 33000,
|
| 415 |
+
"export_mb": 1.3,
|
| 416 |
+
"best": {
|
| 417 |
+
"tau": 0.72,
|
| 418 |
+
"min_support": 0,
|
| 419 |
+
"precision": 0.9034,
|
| 420 |
+
"coverage": 0.3489,
|
| 421 |
+
"keystrokes_saved": 0.2007,
|
| 422 |
+
"shown": 14472
|
| 423 |
+
},
|
| 424 |
+
"seconds": 8.4
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"settings": {
|
| 428 |
+
"n_prev": 3,
|
| 429 |
+
"smoothing": "kn",
|
| 430 |
+
"discount": 0.75,
|
| 431 |
+
"phrase_weight": 5.0,
|
| 432 |
+
"prompt_weight": 1,
|
| 433 |
+
"reply_weight": 0.3,
|
| 434 |
+
"max_vocab": 8000,
|
| 435 |
+
"min_word_count": 3,
|
| 436 |
+
"min_count": 2,
|
| 437 |
+
"param_target": 400000000
|
| 438 |
+
},
|
| 439 |
+
"vocab": 3841,
|
| 440 |
+
"min_context_count": 2,
|
| 441 |
+
"contexts_seen": 454608,
|
| 442 |
+
"contexts_kept": 40744,
|
| 443 |
+
"export_mb": 2.6,
|
| 444 |
+
"best": {
|
| 445 |
+
"tau": 0.7,
|
| 446 |
+
"min_support": 0,
|
| 447 |
+
"precision": 0.9006,
|
| 448 |
+
"coverage": 0.3758,
|
| 449 |
+
"keystrokes_saved": 0.2178,
|
| 450 |
+
"shown": 15588
|
| 451 |
+
},
|
| 452 |
+
"seconds": 11.9
|
| 453 |
+
},
|
| 454 |
+
{
|
| 455 |
+
"settings": {
|
| 456 |
+
"n_prev": 3,
|
| 457 |
+
"smoothing": "kn",
|
| 458 |
+
"discount": 0.75,
|
| 459 |
+
"phrase_weight": 1.0,
|
| 460 |
+
"prompt_weight": 1,
|
| 461 |
+
"reply_weight": 1.0,
|
| 462 |
+
"max_vocab": 3000,
|
| 463 |
+
"min_word_count": 3,
|
| 464 |
+
"min_count": 2,
|
| 465 |
+
"param_target": 400000000
|
| 466 |
+
},
|
| 467 |
+
"vocab": 3000,
|
| 468 |
+
"min_context_count": 2,
|
| 469 |
+
"contexts_seen": 425514,
|
| 470 |
+
"contexts_kept": 96265,
|
| 471 |
+
"export_mb": 4.0,
|
| 472 |
+
"best": {
|
| 473 |
+
"tau": 0.7,
|
| 474 |
+
"min_support": 2,
|
| 475 |
+
"precision": 0.9,
|
| 476 |
+
"coverage": 0.3601,
|
| 477 |
+
"keystrokes_saved": 0.2123,
|
| 478 |
+
"shown": 14933
|
| 479 |
+
},
|
| 480 |
+
"seconds": 11.2
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
"settings": {
|
| 484 |
+
"n_prev": 3,
|
| 485 |
+
"smoothing": "kn",
|
| 486 |
+
"discount": 0.75,
|
| 487 |
+
"phrase_weight": 1.0,
|
| 488 |
+
"prompt_weight": 1,
|
| 489 |
+
"reply_weight": 1.0,
|
| 490 |
+
"max_vocab": 3000,
|
| 491 |
+
"min_word_count": 3,
|
| 492 |
+
"min_count": 3,
|
| 493 |
+
"param_target": 400000000
|
| 494 |
+
},
|
| 495 |
+
"vocab": 3000,
|
| 496 |
+
"min_context_count": 3,
|
| 497 |
+
"contexts_seen": 425514,
|
| 498 |
+
"contexts_kept": 54867,
|
| 499 |
+
"export_mb": 3.0,
|
| 500 |
+
"best": {
|
| 501 |
+
"tau": 0.69,
|
| 502 |
+
"min_support": 0,
|
| 503 |
+
"precision": 0.9016,
|
| 504 |
+
"coverage": 0.358,
|
| 505 |
+
"keystrokes_saved": 0.2091,
|
| 506 |
+
"shown": 14849
|
| 507 |
+
},
|
| 508 |
+
"seconds": 11.1
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"settings": {
|
| 512 |
+
"n_prev": 3,
|
| 513 |
+
"smoothing": "kn",
|
| 514 |
+
"discount": 0.75,
|
| 515 |
+
"phrase_weight": 1.0,
|
| 516 |
+
"prompt_weight": 1,
|
| 517 |
+
"reply_weight": 1.0,
|
| 518 |
+
"max_vocab": 8000,
|
| 519 |
+
"min_word_count": 3,
|
| 520 |
+
"min_count": 3,
|
| 521 |
+
"param_target": 400000000
|
| 522 |
+
},
|
| 523 |
+
"vocab": 6550,
|
| 524 |
+
"min_context_count": 3,
|
| 525 |
+
"contexts_seen": 509522,
|
| 526 |
+
"contexts_kept": 58669,
|
| 527 |
+
"export_mb": 3.1,
|
| 528 |
+
"best": {
|
| 529 |
+
"tau": 0.7,
|
| 530 |
+
"min_support": 0,
|
| 531 |
+
"precision": 0.9007,
|
| 532 |
+
"coverage": 0.3888,
|
| 533 |
+
"keystrokes_saved": 0.2294,
|
| 534 |
+
"shown": 16126
|
| 535 |
+
},
|
| 536 |
+
"seconds": 13.5
|
| 537 |
+
},
|
| 538 |
+
{
|
| 539 |
+
"settings": {
|
| 540 |
+
"n_prev": 3,
|
| 541 |
+
"smoothing": "kn",
|
| 542 |
+
"discount": 0.75,
|
| 543 |
+
"phrase_weight": 1.0,
|
| 544 |
+
"prompt_weight": 1,
|
| 545 |
+
"reply_weight": 1.0,
|
| 546 |
+
"max_vocab": 16000,
|
| 547 |
+
"min_word_count": 3,
|
| 548 |
+
"min_count": 2,
|
| 549 |
+
"param_target": 400000000
|
| 550 |
+
},
|
| 551 |
+
"vocab": 6550,
|
| 552 |
+
"min_context_count": 2,
|
| 553 |
+
"contexts_seen": 509522,
|
| 554 |
+
"contexts_kept": 105467,
|
| 555 |
+
"export_mb": 4.2,
|
| 556 |
+
"best": {
|
| 557 |
+
"tau": 0.72,
|
| 558 |
+
"min_support": 0,
|
| 559 |
+
"precision": 0.9029,
|
| 560 |
+
"coverage": 0.3866,
|
| 561 |
+
"keystrokes_saved": 0.2296,
|
| 562 |
+
"shown": 16034
|
| 563 |
+
},
|
| 564 |
+
"seconds": 13.2
|
| 565 |
+
},
|
| 566 |
+
{
|
| 567 |
+
"settings": {
|
| 568 |
+
"n_prev": 3,
|
| 569 |
+
"smoothing": "kn",
|
| 570 |
+
"discount": 0.75,
|
| 571 |
+
"phrase_weight": 1.0,
|
| 572 |
+
"prompt_weight": 1,
|
| 573 |
+
"reply_weight": 1.0,
|
| 574 |
+
"max_vocab": 16000,
|
| 575 |
+
"min_word_count": 3,
|
| 576 |
+
"min_count": 3,
|
| 577 |
+
"param_target": 400000000
|
| 578 |
+
},
|
| 579 |
+
"vocab": 6550,
|
| 580 |
+
"min_context_count": 3,
|
| 581 |
+
"contexts_seen": 509522,
|
| 582 |
+
"contexts_kept": 58669,
|
| 583 |
+
"export_mb": 3.1,
|
| 584 |
+
"best": {
|
| 585 |
+
"tau": 0.7,
|
| 586 |
+
"min_support": 0,
|
| 587 |
+
"precision": 0.9007,
|
| 588 |
+
"coverage": 0.3888,
|
| 589 |
+
"keystrokes_saved": 0.2294,
|
| 590 |
+
"shown": 16126
|
| 591 |
+
},
|
| 592 |
+
"seconds": 12.9
|
| 593 |
+
},
|
| 594 |
+
{
|
| 595 |
+
"settings": {
|
| 596 |
+
"n_prev": 4,
|
| 597 |
+
"smoothing": "kn",
|
| 598 |
+
"discount": 0.75,
|
| 599 |
+
"phrase_weight": 1.0,
|
| 600 |
+
"prompt_weight": 1,
|
| 601 |
+
"reply_weight": 1.0,
|
| 602 |
+
"max_vocab": 8000,
|
| 603 |
+
"min_word_count": 3,
|
| 604 |
+
"min_count": 2,
|
| 605 |
+
"param_target": 400000000
|
| 606 |
+
},
|
| 607 |
+
"vocab": 6550,
|
| 608 |
+
"min_context_count": 2,
|
| 609 |
+
"contexts_seen": 754606,
|
| 610 |
+
"contexts_kept": 125943,
|
| 611 |
+
"export_mb": 4.9,
|
| 612 |
+
"best": {
|
| 613 |
+
"tau": 0.73,
|
| 614 |
+
"min_support": 0,
|
| 615 |
+
"precision": 0.9025,
|
| 616 |
+
"coverage": 0.382,
|
| 617 |
+
"keystrokes_saved": 0.2279,
|
| 618 |
+
"shown": 15841
|
| 619 |
+
},
|
| 620 |
+
"seconds": 14.6
|
| 621 |
+
},
|
| 622 |
+
{
|
| 623 |
+
"settings": {
|
| 624 |
+
"n_prev": 5,
|
| 625 |
+
"smoothing": "kn",
|
| 626 |
+
"discount": 0.75,
|
| 627 |
+
"phrase_weight": 1.0,
|
| 628 |
+
"prompt_weight": 1,
|
| 629 |
+
"reply_weight": 1.0,
|
| 630 |
+
"max_vocab": 8000,
|
| 631 |
+
"min_word_count": 3,
|
| 632 |
+
"min_count": 2,
|
| 633 |
+
"param_target": 400000000
|
| 634 |
+
},
|
| 635 |
+
"vocab": 6550,
|
| 636 |
+
"min_context_count": 2,
|
| 637 |
+
"contexts_seen": 1005811,
|
| 638 |
+
"contexts_kept": 143202,
|
| 639 |
+
"export_mb": 5.4,
|
| 640 |
+
"best": {
|
| 641 |
+
"tau": 0.73,
|
| 642 |
+
"min_support": 2,
|
| 643 |
+
"precision": 0.9003,
|
| 644 |
+
"coverage": 0.3816,
|
| 645 |
+
"keystrokes_saved": 0.2273,
|
| 646 |
+
"shown": 15826
|
| 647 |
+
},
|
| 648 |
+
"seconds": 16.2
|
| 649 |
+
},
|
| 650 |
+
{
|
| 651 |
+
"settings": {
|
| 652 |
+
"n_prev": 7,
|
| 653 |
+
"smoothing": "kn",
|
| 654 |
+
"discount": 0.75,
|
| 655 |
+
"phrase_weight": 1.0,
|
| 656 |
+
"prompt_weight": 1,
|
| 657 |
+
"reply_weight": 1.0,
|
| 658 |
+
"max_vocab": 8000,
|
| 659 |
+
"min_word_count": 3,
|
| 660 |
+
"min_count": 2,
|
| 661 |
+
"param_target": 400000000
|
| 662 |
+
},
|
| 663 |
+
"vocab": 6550,
|
| 664 |
+
"min_context_count": 2,
|
| 665 |
+
"contexts_seen": 1514926,
|
| 666 |
+
"contexts_kept": 173445,
|
| 667 |
+
"export_mb": 6.6,
|
| 668 |
+
"best": {
|
| 669 |
+
"tau": 0.74,
|
| 670 |
+
"min_support": 0,
|
| 671 |
+
"precision": 0.9001,
|
| 672 |
+
"coverage": 0.3788,
|
| 673 |
+
"keystrokes_saved": 0.2244,
|
| 674 |
+
"shown": 15710
|
| 675 |
+
},
|
| 676 |
+
"seconds": 19.1
|
| 677 |
+
},
|
| 678 |
+
{
|
| 679 |
+
"settings": {
|
| 680 |
+
"n_prev": 9,
|
| 681 |
+
"smoothing": "kn",
|
| 682 |
+
"discount": 0.75,
|
| 683 |
+
"phrase_weight": 1.0,
|
| 684 |
+
"prompt_weight": 1,
|
| 685 |
+
"reply_weight": 1.0,
|
| 686 |
+
"max_vocab": 8000,
|
| 687 |
+
"min_word_count": 3,
|
| 688 |
+
"min_count": 2,
|
| 689 |
+
"param_target": 400000000
|
| 690 |
+
},
|
| 691 |
+
"vocab": 6550,
|
| 692 |
+
"min_context_count": 2,
|
| 693 |
+
"contexts_seen": 2027338,
|
| 694 |
+
"contexts_kept": 201278,
|
| 695 |
+
"export_mb": 7.7,
|
| 696 |
+
"best": {
|
| 697 |
+
"tau": 0.74,
|
| 698 |
+
"min_support": 2,
|
| 699 |
+
"precision": 0.9008,
|
| 700 |
+
"coverage": 0.378,
|
| 701 |
+
"keystrokes_saved": 0.2244,
|
| 702 |
+
"shown": 15678
|
| 703 |
+
},
|
| 704 |
+
"seconds": 22.9
|
| 705 |
+
}
|
| 706 |
+
],
|
| 707 |
+
"best_grid": [
|
| 708 |
+
{
|
| 709 |
+
"tau": 0.5,
|
| 710 |
+
"min_support": 0,
|
| 711 |
+
"precision": 0.7865,
|
| 712 |
+
"coverage": 0.5115,
|
| 713 |
+
"keystrokes_saved": 0.2789,
|
| 714 |
+
"shown": 21215
|
| 715 |
+
},
|
| 716 |
+
{
|
| 717 |
+
"tau": 0.5,
|
| 718 |
+
"min_support": 2,
|
| 719 |
+
"precision": 0.7871,
|
| 720 |
+
"coverage": 0.5107,
|
| 721 |
+
"keystrokes_saved": 0.2786,
|
| 722 |
+
"shown": 21180
|
| 723 |
+
},
|
| 724 |
+
{
|
| 725 |
+
"tau": 0.5,
|
| 726 |
+
"min_support": 5,
|
| 727 |
+
"precision": 0.8083,
|
| 728 |
+
"coverage": 0.3863,
|
| 729 |
+
"keystrokes_saved": 0.2119,
|
| 730 |
+
"shown": 16021
|
| 731 |
+
},
|
| 732 |
+
{
|
| 733 |
+
"tau": 0.5,
|
| 734 |
+
"min_support": 10,
|
| 735 |
+
"precision": 0.8177,
|
| 736 |
+
"coverage": 0.3212,
|
| 737 |
+
"keystrokes_saved": 0.1777,
|
| 738 |
+
"shown": 13322
|
| 739 |
+
},
|
| 740 |
+
{
|
| 741 |
+
"tau": 0.51,
|
| 742 |
+
"min_support": 0,
|
| 743 |
+
"precision": 0.7919,
|
| 744 |
+
"coverage": 0.5048,
|
| 745 |
+
"keystrokes_saved": 0.2761,
|
| 746 |
+
"shown": 20937
|
| 747 |
+
},
|
| 748 |
+
{
|
| 749 |
+
"tau": 0.51,
|
| 750 |
+
"min_support": 2,
|
| 751 |
+
"precision": 0.7925,
|
| 752 |
+
"coverage": 0.504,
|
| 753 |
+
"keystrokes_saved": 0.2759,
|
| 754 |
+
"shown": 20902
|
| 755 |
+
},
|
| 756 |
+
{
|
| 757 |
+
"tau": 0.51,
|
| 758 |
+
"min_support": 5,
|
| 759 |
+
"precision": 0.8132,
|
| 760 |
+
"coverage": 0.3811,
|
| 761 |
+
"keystrokes_saved": 0.2099,
|
| 762 |
+
"shown": 15805
|
| 763 |
+
},
|
| 764 |
+
{
|
| 765 |
+
"tau": 0.51,
|
| 766 |
+
"min_support": 10,
|
| 767 |
+
"precision": 0.8217,
|
| 768 |
+
"coverage": 0.3173,
|
| 769 |
+
"keystrokes_saved": 0.1761,
|
| 770 |
+
"shown": 13161
|
| 771 |
+
},
|
| 772 |
+
{
|
| 773 |
+
"tau": 0.52,
|
| 774 |
+
"min_support": 0,
|
| 775 |
+
"precision": 0.797,
|
| 776 |
+
"coverage": 0.4991,
|
| 777 |
+
"keystrokes_saved": 0.2743,
|
| 778 |
+
"shown": 20699
|
| 779 |
+
},
|
| 780 |
+
{
|
| 781 |
+
"tau": 0.52,
|
| 782 |
+
"min_support": 2,
|
| 783 |
+
"precision": 0.7976,
|
| 784 |
+
"coverage": 0.4983,
|
| 785 |
+
"keystrokes_saved": 0.2741,
|
| 786 |
+
"shown": 20666
|
| 787 |
+
},
|
| 788 |
+
{
|
| 789 |
+
"tau": 0.52,
|
| 790 |
+
"min_support": 5,
|
| 791 |
+
"precision": 0.818,
|
| 792 |
+
"coverage": 0.3768,
|
| 793 |
+
"keystrokes_saved": 0.2085,
|
| 794 |
+
"shown": 15628
|
| 795 |
+
},
|
| 796 |
+
{
|
| 797 |
+
"tau": 0.52,
|
| 798 |
+
"min_support": 10,
|
| 799 |
+
"precision": 0.8261,
|
| 800 |
+
"coverage": 0.3142,
|
| 801 |
+
"keystrokes_saved": 0.1752,
|
| 802 |
+
"shown": 13030
|
| 803 |
+
},
|
| 804 |
+
{
|
| 805 |
+
"tau": 0.53,
|
| 806 |
+
"min_support": 0,
|
| 807 |
+
"precision": 0.803,
|
| 808 |
+
"coverage": 0.4929,
|
| 809 |
+
"keystrokes_saved": 0.2724,
|
| 810 |
+
"shown": 20441
|
| 811 |
+
},
|
| 812 |
+
{
|
| 813 |
+
"tau": 0.53,
|
| 814 |
+
"min_support": 2,
|
| 815 |
+
"precision": 0.8037,
|
| 816 |
+
"coverage": 0.4921,
|
| 817 |
+
"keystrokes_saved": 0.2722,
|
| 818 |
+
"shown": 20408
|
| 819 |
+
},
|
| 820 |
+
{
|
| 821 |
+
"tau": 0.53,
|
| 822 |
+
"min_support": 5,
|
| 823 |
+
"precision": 0.8236,
|
| 824 |
+
"coverage": 0.372,
|
| 825 |
+
"keystrokes_saved": 0.2073,
|
| 826 |
+
"shown": 15430
|
| 827 |
+
},
|
| 828 |
+
{
|
| 829 |
+
"tau": 0.53,
|
| 830 |
+
"min_support": 10,
|
| 831 |
+
"precision": 0.8309,
|
| 832 |
+
"coverage": 0.3104,
|
| 833 |
+
"keystrokes_saved": 0.1742,
|
| 834 |
+
"shown": 12873
|
| 835 |
+
},
|
| 836 |
+
{
|
| 837 |
+
"tau": 0.54,
|
| 838 |
+
"min_support": 0,
|
| 839 |
+
"precision": 0.8066,
|
| 840 |
+
"coverage": 0.4881,
|
| 841 |
+
"keystrokes_saved": 0.2709,
|
| 842 |
+
"shown": 20242
|
| 843 |
+
},
|
| 844 |
+
{
|
| 845 |
+
"tau": 0.54,
|
| 846 |
+
"min_support": 2,
|
| 847 |
+
"precision": 0.8072,
|
| 848 |
+
"coverage": 0.4873,
|
| 849 |
+
"keystrokes_saved": 0.2707,
|
| 850 |
+
"shown": 20209
|
| 851 |
+
},
|
| 852 |
+
{
|
| 853 |
+
"tau": 0.54,
|
| 854 |
+
"min_support": 5,
|
| 855 |
+
"precision": 0.8263,
|
| 856 |
+
"coverage": 0.3687,
|
| 857 |
+
"keystrokes_saved": 0.2062,
|
| 858 |
+
"shown": 15290
|
| 859 |
+
},
|
| 860 |
+
{
|
| 861 |
+
"tau": 0.54,
|
| 862 |
+
"min_support": 10,
|
| 863 |
+
"precision": 0.8336,
|
| 864 |
+
"coverage": 0.3077,
|
| 865 |
+
"keystrokes_saved": 0.1733,
|
| 866 |
+
"shown": 12763
|
| 867 |
+
},
|
| 868 |
+
{
|
| 869 |
+
"tau": 0.55,
|
| 870 |
+
"min_support": 0,
|
| 871 |
+
"precision": 0.813,
|
| 872 |
+
"coverage": 0.4822,
|
| 873 |
+
"keystrokes_saved": 0.2698,
|
| 874 |
+
"shown": 20000
|
| 875 |
+
},
|
| 876 |
+
{
|
| 877 |
+
"tau": 0.55,
|
| 878 |
+
"min_support": 2,
|
| 879 |
+
"precision": 0.8137,
|
| 880 |
+
"coverage": 0.4815,
|
| 881 |
+
"keystrokes_saved": 0.2696,
|
| 882 |
+
"shown": 19968
|
| 883 |
+
},
|
| 884 |
+
{
|
| 885 |
+
"tau": 0.55,
|
| 886 |
+
"min_support": 5,
|
| 887 |
+
"precision": 0.8324,
|
| 888 |
+
"coverage": 0.3641,
|
| 889 |
+
"keystrokes_saved": 0.2051,
|
| 890 |
+
"shown": 15101
|
| 891 |
+
},
|
| 892 |
+
{
|
| 893 |
+
"tau": 0.55,
|
| 894 |
+
"min_support": 10,
|
| 895 |
+
"precision": 0.8393,
|
| 896 |
+
"coverage": 0.3039,
|
| 897 |
+
"keystrokes_saved": 0.1725,
|
| 898 |
+
"shown": 12605
|
| 899 |
+
},
|
| 900 |
+
{
|
| 901 |
+
"tau": 0.56,
|
| 902 |
+
"min_support": 0,
|
| 903 |
+
"precision": 0.8183,
|
| 904 |
+
"coverage": 0.4766,
|
| 905 |
+
"keystrokes_saved": 0.2652,
|
| 906 |
+
"shown": 19765
|
| 907 |
+
},
|
| 908 |
+
{
|
| 909 |
+
"tau": 0.56,
|
| 910 |
+
"min_support": 2,
|
| 911 |
+
"precision": 0.8189,
|
| 912 |
+
"coverage": 0.4758,
|
| 913 |
+
"keystrokes_saved": 0.265,
|
| 914 |
+
"shown": 19734
|
| 915 |
+
},
|
| 916 |
+
{
|
| 917 |
+
"tau": 0.56,
|
| 918 |
+
"min_support": 5,
|
| 919 |
+
"precision": 0.8372,
|
| 920 |
+
"coverage": 0.3597,
|
| 921 |
+
"keystrokes_saved": 0.2007,
|
| 922 |
+
"shown": 14918
|
| 923 |
+
},
|
| 924 |
+
{
|
| 925 |
+
"tau": 0.56,
|
| 926 |
+
"min_support": 10,
|
| 927 |
+
"precision": 0.8438,
|
| 928 |
+
"coverage": 0.3003,
|
| 929 |
+
"keystrokes_saved": 0.1683,
|
| 930 |
+
"shown": 12455
|
| 931 |
+
},
|
| 932 |
+
{
|
| 933 |
+
"tau": 0.57,
|
| 934 |
+
"min_support": 0,
|
| 935 |
+
"precision": 0.8259,
|
| 936 |
+
"coverage": 0.4716,
|
| 937 |
+
"keystrokes_saved": 0.2665,
|
| 938 |
+
"shown": 19559
|
| 939 |
+
},
|
| 940 |
+
{
|
| 941 |
+
"tau": 0.57,
|
| 942 |
+
"min_support": 2,
|
| 943 |
+
"precision": 0.8265,
|
| 944 |
+
"coverage": 0.4709,
|
| 945 |
+
"keystrokes_saved": 0.2663,
|
| 946 |
+
"shown": 19530
|
| 947 |
+
},
|
| 948 |
+
{
|
| 949 |
+
"tau": 0.57,
|
| 950 |
+
"min_support": 5,
|
| 951 |
+
"precision": 0.8444,
|
| 952 |
+
"coverage": 0.356,
|
| 953 |
+
"keystrokes_saved": 0.2018,
|
| 954 |
+
"shown": 14766
|
| 955 |
+
},
|
| 956 |
+
{
|
| 957 |
+
"tau": 0.57,
|
| 958 |
+
"min_support": 10,
|
| 959 |
+
"precision": 0.8518,
|
| 960 |
+
"coverage": 0.2973,
|
| 961 |
+
"keystrokes_saved": 0.1697,
|
| 962 |
+
"shown": 12331
|
| 963 |
+
},
|
| 964 |
+
{
|
| 965 |
+
"tau": 0.58,
|
| 966 |
+
"min_support": 0,
|
| 967 |
+
"precision": 0.8361,
|
| 968 |
+
"coverage": 0.4668,
|
| 969 |
+
"keystrokes_saved": 0.265,
|
| 970 |
+
"shown": 19359
|
| 971 |
+
},
|
| 972 |
+
{
|
| 973 |
+
"tau": 0.58,
|
| 974 |
+
"min_support": 2,
|
| 975 |
+
"precision": 0.8366,
|
| 976 |
+
"coverage": 0.4661,
|
| 977 |
+
"keystrokes_saved": 0.2648,
|
| 978 |
+
"shown": 19332
|
| 979 |
+
},
|
| 980 |
+
{
|
| 981 |
+
"tau": 0.58,
|
| 982 |
+
"min_support": 5,
|
| 983 |
+
"precision": 0.8549,
|
| 984 |
+
"coverage": 0.3527,
|
| 985 |
+
"keystrokes_saved": 0.2004,
|
| 986 |
+
"shown": 14629
|
| 987 |
+
},
|
| 988 |
+
{
|
| 989 |
+
"tau": 0.58,
|
| 990 |
+
"min_support": 10,
|
| 991 |
+
"precision": 0.863,
|
| 992 |
+
"coverage": 0.2947,
|
| 993 |
+
"keystrokes_saved": 0.1685,
|
| 994 |
+
"shown": 12222
|
| 995 |
+
},
|
| 996 |
+
{
|
| 997 |
+
"tau": 0.59,
|
| 998 |
+
"min_support": 0,
|
| 999 |
+
"precision": 0.8403,
|
| 1000 |
+
"coverage": 0.4613,
|
| 1001 |
+
"keystrokes_saved": 0.2622,
|
| 1002 |
+
"shown": 19132
|
| 1003 |
+
},
|
| 1004 |
+
{
|
| 1005 |
+
"tau": 0.59,
|
| 1006 |
+
"min_support": 2,
|
| 1007 |
+
"precision": 0.8407,
|
| 1008 |
+
"coverage": 0.4607,
|
| 1009 |
+
"keystrokes_saved": 0.262,
|
| 1010 |
+
"shown": 19106
|
| 1011 |
+
},
|
| 1012 |
+
{
|
| 1013 |
+
"tau": 0.59,
|
| 1014 |
+
"min_support": 5,
|
| 1015 |
+
"precision": 0.8582,
|
| 1016 |
+
"coverage": 0.3483,
|
| 1017 |
+
"keystrokes_saved": 0.198,
|
| 1018 |
+
"shown": 14445
|
| 1019 |
+
},
|
| 1020 |
+
{
|
| 1021 |
+
"tau": 0.59,
|
| 1022 |
+
"min_support": 10,
|
| 1023 |
+
"precision": 0.8661,
|
| 1024 |
+
"coverage": 0.2909,
|
| 1025 |
+
"keystrokes_saved": 0.1663,
|
| 1026 |
+
"shown": 12065
|
| 1027 |
+
},
|
| 1028 |
+
{
|
| 1029 |
+
"tau": 0.6,
|
| 1030 |
+
"min_support": 0,
|
| 1031 |
+
"precision": 0.844,
|
| 1032 |
+
"coverage": 0.4557,
|
| 1033 |
+
"keystrokes_saved": 0.2575,
|
| 1034 |
+
"shown": 18899
|
| 1035 |
+
},
|
| 1036 |
+
{
|
| 1037 |
+
"tau": 0.6,
|
| 1038 |
+
"min_support": 2,
|
| 1039 |
+
"precision": 0.8444,
|
| 1040 |
+
"coverage": 0.4551,
|
| 1041 |
+
"keystrokes_saved": 0.2572,
|
| 1042 |
+
"shown": 18873
|
| 1043 |
+
},
|
| 1044 |
+
{
|
| 1045 |
+
"tau": 0.6,
|
| 1046 |
+
"min_support": 5,
|
| 1047 |
+
"precision": 0.862,
|
| 1048 |
+
"coverage": 0.3438,
|
| 1049 |
+
"keystrokes_saved": 0.1937,
|
| 1050 |
+
"shown": 14259
|
| 1051 |
+
},
|
| 1052 |
+
{
|
| 1053 |
+
"tau": 0.6,
|
| 1054 |
+
"min_support": 10,
|
| 1055 |
+
"precision": 0.8701,
|
| 1056 |
+
"coverage": 0.2872,
|
| 1057 |
+
"keystrokes_saved": 0.1626,
|
| 1058 |
+
"shown": 11910
|
| 1059 |
+
},
|
| 1060 |
+
{
|
| 1061 |
+
"tau": 0.61,
|
| 1062 |
+
"min_support": 0,
|
| 1063 |
+
"precision": 0.8482,
|
| 1064 |
+
"coverage": 0.4501,
|
| 1065 |
+
"keystrokes_saved": 0.2555,
|
| 1066 |
+
"shown": 18667
|
| 1067 |
+
},
|
| 1068 |
+
{
|
| 1069 |
+
"tau": 0.61,
|
| 1070 |
+
"min_support": 2,
|
| 1071 |
+
"precision": 0.8485,
|
| 1072 |
+
"coverage": 0.4495,
|
| 1073 |
+
"keystrokes_saved": 0.2553,
|
| 1074 |
+
"shown": 18643
|
| 1075 |
+
},
|
| 1076 |
+
{
|
| 1077 |
+
"tau": 0.61,
|
| 1078 |
+
"min_support": 5,
|
| 1079 |
+
"precision": 0.8655,
|
| 1080 |
+
"coverage": 0.3396,
|
| 1081 |
+
"keystrokes_saved": 0.1921,
|
| 1082 |
+
"shown": 14085
|
| 1083 |
+
},
|
| 1084 |
+
{
|
| 1085 |
+
"tau": 0.61,
|
| 1086 |
+
"min_support": 10,
|
| 1087 |
+
"precision": 0.8729,
|
| 1088 |
+
"coverage": 0.2842,
|
| 1089 |
+
"keystrokes_saved": 0.1613,
|
| 1090 |
+
"shown": 11787
|
| 1091 |
+
},
|
| 1092 |
+
{
|
| 1093 |
+
"tau": 0.62,
|
| 1094 |
+
"min_support": 0,
|
| 1095 |
+
"precision": 0.8551,
|
| 1096 |
+
"coverage": 0.4405,
|
| 1097 |
+
"keystrokes_saved": 0.2542,
|
| 1098 |
+
"shown": 18271
|
| 1099 |
+
},
|
| 1100 |
+
{
|
| 1101 |
+
"tau": 0.62,
|
| 1102 |
+
"min_support": 2,
|
| 1103 |
+
"precision": 0.8554,
|
| 1104 |
+
"coverage": 0.44,
|
| 1105 |
+
"keystrokes_saved": 0.254,
|
| 1106 |
+
"shown": 18247
|
| 1107 |
+
},
|
| 1108 |
+
{
|
| 1109 |
+
"tau": 0.62,
|
| 1110 |
+
"min_support": 5,
|
| 1111 |
+
"precision": 0.8729,
|
| 1112 |
+
"coverage": 0.3316,
|
| 1113 |
+
"keystrokes_saved": 0.1915,
|
| 1114 |
+
"shown": 13753
|
| 1115 |
+
},
|
| 1116 |
+
{
|
| 1117 |
+
"tau": 0.62,
|
| 1118 |
+
"min_support": 10,
|
| 1119 |
+
"precision": 0.8805,
|
| 1120 |
+
"coverage": 0.2772,
|
| 1121 |
+
"keystrokes_saved": 0.1614,
|
| 1122 |
+
"shown": 11497
|
| 1123 |
+
},
|
| 1124 |
+
{
|
| 1125 |
+
"tau": 0.63,
|
| 1126 |
+
"min_support": 0,
|
| 1127 |
+
"precision": 0.8595,
|
| 1128 |
+
"coverage": 0.4349,
|
| 1129 |
+
"keystrokes_saved": 0.2518,
|
| 1130 |
+
"shown": 18035
|
| 1131 |
+
},
|
| 1132 |
+
{
|
| 1133 |
+
"tau": 0.63,
|
| 1134 |
+
"min_support": 2,
|
| 1135 |
+
"precision": 0.8598,
|
| 1136 |
+
"coverage": 0.4343,
|
| 1137 |
+
"keystrokes_saved": 0.2516,
|
| 1138 |
+
"shown": 18012
|
| 1139 |
+
},
|
| 1140 |
+
{
|
| 1141 |
+
"tau": 0.63,
|
| 1142 |
+
"min_support": 5,
|
| 1143 |
+
"precision": 0.8767,
|
| 1144 |
+
"coverage": 0.3276,
|
| 1145 |
+
"keystrokes_saved": 0.1898,
|
| 1146 |
+
"shown": 13587
|
| 1147 |
+
},
|
| 1148 |
+
{
|
| 1149 |
+
"tau": 0.63,
|
| 1150 |
+
"min_support": 10,
|
| 1151 |
+
"precision": 0.884,
|
| 1152 |
+
"coverage": 0.274,
|
| 1153 |
+
"keystrokes_saved": 0.1601,
|
| 1154 |
+
"shown": 11364
|
| 1155 |
+
},
|
| 1156 |
+
{
|
| 1157 |
+
"tau": 0.64,
|
| 1158 |
+
"min_support": 0,
|
| 1159 |
+
"precision": 0.8637,
|
| 1160 |
+
"coverage": 0.4308,
|
| 1161 |
+
"keystrokes_saved": 0.2502,
|
| 1162 |
+
"shown": 17865
|
| 1163 |
+
},
|
| 1164 |
+
{
|
| 1165 |
+
"tau": 0.64,
|
| 1166 |
+
"min_support": 2,
|
| 1167 |
+
"precision": 0.864,
|
| 1168 |
+
"coverage": 0.4302,
|
| 1169 |
+
"keystrokes_saved": 0.2501,
|
| 1170 |
+
"shown": 17843
|
| 1171 |
+
},
|
| 1172 |
+
{
|
| 1173 |
+
"tau": 0.64,
|
| 1174 |
+
"min_support": 5,
|
| 1175 |
+
"precision": 0.8798,
|
| 1176 |
+
"coverage": 0.3249,
|
| 1177 |
+
"keystrokes_saved": 0.1886,
|
| 1178 |
+
"shown": 13474
|
| 1179 |
+
},
|
| 1180 |
+
{
|
| 1181 |
+
"tau": 0.64,
|
| 1182 |
+
"min_support": 10,
|
| 1183 |
+
"precision": 0.8871,
|
| 1184 |
+
"coverage": 0.2715,
|
| 1185 |
+
"keystrokes_saved": 0.1592,
|
| 1186 |
+
"shown": 11260
|
| 1187 |
+
},
|
| 1188 |
+
{
|
| 1189 |
+
"tau": 0.65,
|
| 1190 |
+
"min_support": 0,
|
| 1191 |
+
"precision": 0.8697,
|
| 1192 |
+
"coverage": 0.4247,
|
| 1193 |
+
"keystrokes_saved": 0.2479,
|
| 1194 |
+
"shown": 17616
|
| 1195 |
+
},
|
| 1196 |
+
{
|
| 1197 |
+
"tau": 0.65,
|
| 1198 |
+
"min_support": 2,
|
| 1199 |
+
"precision": 0.87,
|
| 1200 |
+
"coverage": 0.4242,
|
| 1201 |
+
"keystrokes_saved": 0.2477,
|
| 1202 |
+
"shown": 17594
|
| 1203 |
+
},
|
| 1204 |
+
{
|
| 1205 |
+
"tau": 0.65,
|
| 1206 |
+
"min_support": 5,
|
| 1207 |
+
"precision": 0.8849,
|
| 1208 |
+
"coverage": 0.3206,
|
| 1209 |
+
"keystrokes_saved": 0.1867,
|
| 1210 |
+
"shown": 13296
|
| 1211 |
+
},
|
| 1212 |
+
{
|
| 1213 |
+
"tau": 0.65,
|
| 1214 |
+
"min_support": 10,
|
| 1215 |
+
"precision": 0.8917,
|
| 1216 |
+
"coverage": 0.268,
|
| 1217 |
+
"keystrokes_saved": 0.1576,
|
| 1218 |
+
"shown": 11113
|
| 1219 |
+
},
|
| 1220 |
+
{
|
| 1221 |
+
"tau": 0.66,
|
| 1222 |
+
"min_support": 0,
|
| 1223 |
+
"precision": 0.8748,
|
| 1224 |
+
"coverage": 0.4193,
|
| 1225 |
+
"keystrokes_saved": 0.2456,
|
| 1226 |
+
"shown": 17391
|
| 1227 |
+
},
|
| 1228 |
+
{
|
| 1229 |
+
"tau": 0.66,
|
| 1230 |
+
"min_support": 2,
|
| 1231 |
+
"precision": 0.875,
|
| 1232 |
+
"coverage": 0.4188,
|
| 1233 |
+
"keystrokes_saved": 0.2454,
|
| 1234 |
+
"shown": 17369
|
| 1235 |
+
},
|
| 1236 |
+
{
|
| 1237 |
+
"tau": 0.66,
|
| 1238 |
+
"min_support": 5,
|
| 1239 |
+
"precision": 0.8883,
|
| 1240 |
+
"coverage": 0.317,
|
| 1241 |
+
"keystrokes_saved": 0.1852,
|
| 1242 |
+
"shown": 13149
|
| 1243 |
+
},
|
| 1244 |
+
{
|
| 1245 |
+
"tau": 0.66,
|
| 1246 |
+
"min_support": 10,
|
| 1247 |
+
"precision": 0.8943,
|
| 1248 |
+
"coverage": 0.2651,
|
| 1249 |
+
"keystrokes_saved": 0.1563,
|
| 1250 |
+
"shown": 10994
|
| 1251 |
+
},
|
| 1252 |
+
{
|
| 1253 |
+
"tau": 0.67,
|
| 1254 |
+
"min_support": 0,
|
| 1255 |
+
"precision": 0.8797,
|
| 1256 |
+
"coverage": 0.4133,
|
| 1257 |
+
"keystrokes_saved": 0.2425,
|
| 1258 |
+
"shown": 17141
|
| 1259 |
+
},
|
| 1260 |
+
{
|
| 1261 |
+
"tau": 0.67,
|
| 1262 |
+
"min_support": 2,
|
| 1263 |
+
"precision": 0.8799,
|
| 1264 |
+
"coverage": 0.4128,
|
| 1265 |
+
"keystrokes_saved": 0.2424,
|
| 1266 |
+
"shown": 17122
|
| 1267 |
+
},
|
| 1268 |
+
{
|
| 1269 |
+
"tau": 0.67,
|
| 1270 |
+
"min_support": 5,
|
| 1271 |
+
"precision": 0.8929,
|
| 1272 |
+
"coverage": 0.3124,
|
| 1273 |
+
"keystrokes_saved": 0.183,
|
| 1274 |
+
"shown": 12958
|
| 1275 |
+
},
|
| 1276 |
+
{
|
| 1277 |
+
"tau": 0.67,
|
| 1278 |
+
"min_support": 10,
|
| 1279 |
+
"precision": 0.8989,
|
| 1280 |
+
"coverage": 0.2614,
|
| 1281 |
+
"keystrokes_saved": 0.1542,
|
| 1282 |
+
"shown": 10840
|
| 1283 |
+
},
|
| 1284 |
+
{
|
| 1285 |
+
"tau": 0.68,
|
| 1286 |
+
"min_support": 0,
|
| 1287 |
+
"precision": 0.8852,
|
| 1288 |
+
"coverage": 0.4085,
|
| 1289 |
+
"keystrokes_saved": 0.2409,
|
| 1290 |
+
"shown": 16943
|
| 1291 |
+
},
|
| 1292 |
+
{
|
| 1293 |
+
"tau": 0.68,
|
| 1294 |
+
"min_support": 2,
|
| 1295 |
+
"precision": 0.8854,
|
| 1296 |
+
"coverage": 0.4081,
|
| 1297 |
+
"keystrokes_saved": 0.2407,
|
| 1298 |
+
"shown": 16925
|
| 1299 |
+
},
|
| 1300 |
+
{
|
| 1301 |
+
"tau": 0.68,
|
| 1302 |
+
"min_support": 5,
|
| 1303 |
+
"precision": 0.8966,
|
| 1304 |
+
"coverage": 0.3091,
|
| 1305 |
+
"keystrokes_saved": 0.1819,
|
| 1306 |
+
"shown": 12820
|
| 1307 |
+
},
|
| 1308 |
+
{
|
| 1309 |
+
"tau": 0.68,
|
| 1310 |
+
"min_support": 10,
|
| 1311 |
+
"precision": 0.9024,
|
| 1312 |
+
"coverage": 0.2587,
|
| 1313 |
+
"keystrokes_saved": 0.1531,
|
| 1314 |
+
"shown": 10728
|
| 1315 |
+
},
|
| 1316 |
+
{
|
| 1317 |
+
"tau": 0.69,
|
| 1318 |
+
"min_support": 0,
|
| 1319 |
+
"precision": 0.8894,
|
| 1320 |
+
"coverage": 0.4034,
|
| 1321 |
+
"keystrokes_saved": 0.2382,
|
| 1322 |
+
"shown": 16731
|
| 1323 |
+
},
|
| 1324 |
+
{
|
| 1325 |
+
"tau": 0.69,
|
| 1326 |
+
"min_support": 2,
|
| 1327 |
+
"precision": 0.8895,
|
| 1328 |
+
"coverage": 0.403,
|
| 1329 |
+
"keystrokes_saved": 0.2381,
|
| 1330 |
+
"shown": 16713
|
| 1331 |
+
},
|
| 1332 |
+
{
|
| 1333 |
+
"tau": 0.69,
|
| 1334 |
+
"min_support": 5,
|
| 1335 |
+
"precision": 0.899,
|
| 1336 |
+
"coverage": 0.3057,
|
| 1337 |
+
"keystrokes_saved": 0.1798,
|
| 1338 |
+
"shown": 12680
|
| 1339 |
+
},
|
| 1340 |
+
{
|
| 1341 |
+
"tau": 0.69,
|
| 1342 |
+
"min_support": 10,
|
| 1343 |
+
"precision": 0.9044,
|
| 1344 |
+
"coverage": 0.2561,
|
| 1345 |
+
"keystrokes_saved": 0.1512,
|
| 1346 |
+
"shown": 10620
|
| 1347 |
+
},
|
| 1348 |
+
{
|
| 1349 |
+
"tau": 0.7,
|
| 1350 |
+
"min_support": 0,
|
| 1351 |
+
"precision": 0.8942,
|
| 1352 |
+
"coverage": 0.3982,
|
| 1353 |
+
"keystrokes_saved": 0.2359,
|
| 1354 |
+
"shown": 16516
|
| 1355 |
+
},
|
| 1356 |
+
{
|
| 1357 |
+
"tau": 0.7,
|
| 1358 |
+
"min_support": 2,
|
| 1359 |
+
"precision": 0.8944,
|
| 1360 |
+
"coverage": 0.3978,
|
| 1361 |
+
"keystrokes_saved": 0.2358,
|
| 1362 |
+
"shown": 16498
|
| 1363 |
+
},
|
| 1364 |
+
{
|
| 1365 |
+
"tau": 0.7,
|
| 1366 |
+
"min_support": 5,
|
| 1367 |
+
"precision": 0.904,
|
| 1368 |
+
"coverage": 0.3017,
|
| 1369 |
+
"keystrokes_saved": 0.1779,
|
| 1370 |
+
"shown": 12512
|
| 1371 |
+
},
|
| 1372 |
+
{
|
| 1373 |
+
"tau": 0.7,
|
| 1374 |
+
"min_support": 10,
|
| 1375 |
+
"precision": 0.9094,
|
| 1376 |
+
"coverage": 0.2529,
|
| 1377 |
+
"keystrokes_saved": 0.1496,
|
| 1378 |
+
"shown": 10490
|
| 1379 |
+
},
|
| 1380 |
+
{
|
| 1381 |
+
"tau": 0.71,
|
| 1382 |
+
"min_support": 0,
|
| 1383 |
+
"precision": 0.8976,
|
| 1384 |
+
"coverage": 0.3929,
|
| 1385 |
+
"keystrokes_saved": 0.233,
|
| 1386 |
+
"shown": 16295
|
| 1387 |
+
},
|
| 1388 |
+
{
|
| 1389 |
+
"tau": 0.71,
|
| 1390 |
+
"min_support": 2,
|
| 1391 |
+
"precision": 0.8978,
|
| 1392 |
+
"coverage": 0.3925,
|
| 1393 |
+
"keystrokes_saved": 0.2329,
|
| 1394 |
+
"shown": 16277
|
| 1395 |
+
},
|
| 1396 |
+
{
|
| 1397 |
+
"tau": 0.71,
|
| 1398 |
+
"min_support": 5,
|
| 1399 |
+
"precision": 0.9062,
|
| 1400 |
+
"coverage": 0.2981,
|
| 1401 |
+
"keystrokes_saved": 0.1758,
|
| 1402 |
+
"shown": 12365
|
| 1403 |
+
},
|
| 1404 |
+
{
|
| 1405 |
+
"tau": 0.71,
|
| 1406 |
+
"min_support": 10,
|
| 1407 |
+
"precision": 0.9116,
|
| 1408 |
+
"coverage": 0.2499,
|
| 1409 |
+
"keystrokes_saved": 0.1477,
|
| 1410 |
+
"shown": 10363
|
| 1411 |
+
},
|
| 1412 |
+
{
|
| 1413 |
+
"tau": 0.72,
|
| 1414 |
+
"min_support": 0,
|
| 1415 |
+
"precision": 0.9029,
|
| 1416 |
+
"coverage": 0.3866,
|
| 1417 |
+
"keystrokes_saved": 0.2296,
|
| 1418 |
+
"shown": 16034
|
| 1419 |
+
},
|
| 1420 |
+
{
|
| 1421 |
+
"tau": 0.72,
|
| 1422 |
+
"min_support": 2,
|
| 1423 |
+
"precision": 0.9031,
|
| 1424 |
+
"coverage": 0.3862,
|
| 1425 |
+
"keystrokes_saved": 0.2295,
|
| 1426 |
+
"shown": 16016
|
| 1427 |
+
},
|
| 1428 |
+
{
|
| 1429 |
+
"tau": 0.72,
|
| 1430 |
+
"min_support": 5,
|
| 1431 |
+
"precision": 0.9113,
|
| 1432 |
+
"coverage": 0.2934,
|
| 1433 |
+
"keystrokes_saved": 0.173,
|
| 1434 |
+
"shown": 12167
|
| 1435 |
+
},
|
| 1436 |
+
{
|
| 1437 |
+
"tau": 0.72,
|
| 1438 |
+
"min_support": 10,
|
| 1439 |
+
"precision": 0.9172,
|
| 1440 |
+
"coverage": 0.2457,
|
| 1441 |
+
"keystrokes_saved": 0.1451,
|
| 1442 |
+
"shown": 10191
|
| 1443 |
+
},
|
| 1444 |
+
{
|
| 1445 |
+
"tau": 0.73,
|
| 1446 |
+
"min_support": 0,
|
| 1447 |
+
"precision": 0.9074,
|
| 1448 |
+
"coverage": 0.381,
|
| 1449 |
+
"keystrokes_saved": 0.2279,
|
| 1450 |
+
"shown": 15800
|
| 1451 |
+
},
|
| 1452 |
+
{
|
| 1453 |
+
"tau": 0.73,
|
| 1454 |
+
"min_support": 2,
|
| 1455 |
+
"precision": 0.9076,
|
| 1456 |
+
"coverage": 0.3805,
|
| 1457 |
+
"keystrokes_saved": 0.2278,
|
| 1458 |
+
"shown": 15782
|
| 1459 |
+
},
|
| 1460 |
+
{
|
| 1461 |
+
"tau": 0.73,
|
| 1462 |
+
"min_support": 5,
|
| 1463 |
+
"precision": 0.9149,
|
| 1464 |
+
"coverage": 0.2894,
|
| 1465 |
+
"keystrokes_saved": 0.1718,
|
| 1466 |
+
"shown": 12004
|
| 1467 |
+
},
|
| 1468 |
+
{
|
| 1469 |
+
"tau": 0.73,
|
| 1470 |
+
"min_support": 10,
|
| 1471 |
+
"precision": 0.9209,
|
| 1472 |
+
"coverage": 0.2427,
|
| 1473 |
+
"keystrokes_saved": 0.1443,
|
| 1474 |
+
"shown": 10065
|
| 1475 |
+
},
|
| 1476 |
+
{
|
| 1477 |
+
"tau": 0.74,
|
| 1478 |
+
"min_support": 0,
|
| 1479 |
+
"precision": 0.9114,
|
| 1480 |
+
"coverage": 0.3765,
|
| 1481 |
+
"keystrokes_saved": 0.2254,
|
| 1482 |
+
"shown": 15615
|
| 1483 |
+
},
|
| 1484 |
+
{
|
| 1485 |
+
"tau": 0.74,
|
| 1486 |
+
"min_support": 2,
|
| 1487 |
+
"precision": 0.9116,
|
| 1488 |
+
"coverage": 0.3761,
|
| 1489 |
+
"keystrokes_saved": 0.2252,
|
| 1490 |
+
"shown": 15597
|
| 1491 |
+
},
|
| 1492 |
+
{
|
| 1493 |
+
"tau": 0.74,
|
| 1494 |
+
"min_support": 5,
|
| 1495 |
+
"precision": 0.9183,
|
| 1496 |
+
"coverage": 0.2863,
|
| 1497 |
+
"keystrokes_saved": 0.17,
|
| 1498 |
+
"shown": 11876
|
| 1499 |
+
},
|
| 1500 |
+
{
|
| 1501 |
+
"tau": 0.74,
|
| 1502 |
+
"min_support": 10,
|
| 1503 |
+
"precision": 0.9242,
|
| 1504 |
+
"coverage": 0.2402,
|
| 1505 |
+
"keystrokes_saved": 0.1428,
|
| 1506 |
+
"shown": 9964
|
| 1507 |
+
},
|
| 1508 |
+
{
|
| 1509 |
+
"tau": 0.75,
|
| 1510 |
+
"min_support": 0,
|
| 1511 |
+
"precision": 0.9141,
|
| 1512 |
+
"coverage": 0.3718,
|
| 1513 |
+
"keystrokes_saved": 0.2234,
|
| 1514 |
+
"shown": 15420
|
| 1515 |
+
},
|
| 1516 |
+
{
|
| 1517 |
+
"tau": 0.75,
|
| 1518 |
+
"min_support": 2,
|
| 1519 |
+
"precision": 0.9143,
|
| 1520 |
+
"coverage": 0.3714,
|
| 1521 |
+
"keystrokes_saved": 0.2233,
|
| 1522 |
+
"shown": 15403
|
| 1523 |
+
},
|
| 1524 |
+
{
|
| 1525 |
+
"tau": 0.75,
|
| 1526 |
+
"min_support": 5,
|
| 1527 |
+
"precision": 0.9204,
|
| 1528 |
+
"coverage": 0.2826,
|
| 1529 |
+
"keystrokes_saved": 0.1686,
|
| 1530 |
+
"shown": 11720
|
| 1531 |
+
},
|
| 1532 |
+
{
|
| 1533 |
+
"tau": 0.75,
|
| 1534 |
+
"min_support": 10,
|
| 1535 |
+
"precision": 0.9261,
|
| 1536 |
+
"coverage": 0.2373,
|
| 1537 |
+
"keystrokes_saved": 0.1418,
|
| 1538 |
+
"shown": 9843
|
| 1539 |
+
},
|
| 1540 |
+
{
|
| 1541 |
+
"tau": 0.76,
|
| 1542 |
+
"min_support": 0,
|
| 1543 |
+
"precision": 0.918,
|
| 1544 |
+
"coverage": 0.3657,
|
| 1545 |
+
"keystrokes_saved": 0.2199,
|
| 1546 |
+
"shown": 15166
|
| 1547 |
+
},
|
| 1548 |
+
{
|
| 1549 |
+
"tau": 0.76,
|
| 1550 |
+
"min_support": 2,
|
| 1551 |
+
"precision": 0.9182,
|
| 1552 |
+
"coverage": 0.3653,
|
| 1553 |
+
"keystrokes_saved": 0.2197,
|
| 1554 |
+
"shown": 15150
|
| 1555 |
+
},
|
| 1556 |
+
{
|
| 1557 |
+
"tau": 0.76,
|
| 1558 |
+
"min_support": 5,
|
| 1559 |
+
"precision": 0.9246,
|
| 1560 |
+
"coverage": 0.2782,
|
| 1561 |
+
"keystrokes_saved": 0.1665,
|
| 1562 |
+
"shown": 11538
|
| 1563 |
+
},
|
| 1564 |
+
{
|
| 1565 |
+
"tau": 0.76,
|
| 1566 |
+
"min_support": 10,
|
| 1567 |
+
"precision": 0.9299,
|
| 1568 |
+
"coverage": 0.2337,
|
| 1569 |
+
"keystrokes_saved": 0.1403,
|
| 1570 |
+
"shown": 9694
|
| 1571 |
+
},
|
| 1572 |
+
{
|
| 1573 |
+
"tau": 0.77,
|
| 1574 |
+
"min_support": 0,
|
| 1575 |
+
"precision": 0.9224,
|
| 1576 |
+
"coverage": 0.3601,
|
| 1577 |
+
"keystrokes_saved": 0.2169,
|
| 1578 |
+
"shown": 14934
|
| 1579 |
+
},
|
| 1580 |
+
{
|
| 1581 |
+
"tau": 0.77,
|
| 1582 |
+
"min_support": 2,
|
| 1583 |
+
"precision": 0.9226,
|
| 1584 |
+
"coverage": 0.3597,
|
| 1585 |
+
"keystrokes_saved": 0.2168,
|
| 1586 |
+
"shown": 14918
|
| 1587 |
+
},
|
| 1588 |
+
{
|
| 1589 |
+
"tau": 0.77,
|
| 1590 |
+
"min_support": 5,
|
| 1591 |
+
"precision": 0.928,
|
| 1592 |
+
"coverage": 0.2741,
|
| 1593 |
+
"keystrokes_saved": 0.1643,
|
| 1594 |
+
"shown": 11369
|
| 1595 |
+
},
|
| 1596 |
+
{
|
| 1597 |
+
"tau": 0.77,
|
| 1598 |
+
"min_support": 10,
|
| 1599 |
+
"precision": 0.9325,
|
| 1600 |
+
"coverage": 0.2304,
|
| 1601 |
+
"keystrokes_saved": 0.1385,
|
| 1602 |
+
"shown": 9557
|
| 1603 |
+
},
|
| 1604 |
+
{
|
| 1605 |
+
"tau": 0.78,
|
| 1606 |
+
"min_support": 0,
|
| 1607 |
+
"precision": 0.9258,
|
| 1608 |
+
"coverage": 0.3547,
|
| 1609 |
+
"keystrokes_saved": 0.2143,
|
| 1610 |
+
"shown": 14710
|
| 1611 |
+
},
|
| 1612 |
+
{
|
| 1613 |
+
"tau": 0.78,
|
| 1614 |
+
"min_support": 2,
|
| 1615 |
+
"precision": 0.926,
|
| 1616 |
+
"coverage": 0.3543,
|
| 1617 |
+
"keystrokes_saved": 0.2142,
|
| 1618 |
+
"shown": 14694
|
| 1619 |
+
},
|
| 1620 |
+
{
|
| 1621 |
+
"tau": 0.78,
|
| 1622 |
+
"min_support": 5,
|
| 1623 |
+
"precision": 0.9311,
|
| 1624 |
+
"coverage": 0.2708,
|
| 1625 |
+
"keystrokes_saved": 0.1626,
|
| 1626 |
+
"shown": 11230
|
| 1627 |
+
},
|
| 1628 |
+
{
|
| 1629 |
+
"tau": 0.78,
|
| 1630 |
+
"min_support": 10,
|
| 1631 |
+
"precision": 0.9351,
|
| 1632 |
+
"coverage": 0.2281,
|
| 1633 |
+
"keystrokes_saved": 0.1373,
|
| 1634 |
+
"shown": 9461
|
| 1635 |
+
},
|
| 1636 |
+
{
|
| 1637 |
+
"tau": 0.79,
|
| 1638 |
+
"min_support": 0,
|
| 1639 |
+
"precision": 0.9307,
|
| 1640 |
+
"coverage": 0.3489,
|
| 1641 |
+
"keystrokes_saved": 0.2115,
|
| 1642 |
+
"shown": 14469
|
| 1643 |
+
},
|
| 1644 |
+
{
|
| 1645 |
+
"tau": 0.79,
|
| 1646 |
+
"min_support": 2,
|
| 1647 |
+
"precision": 0.931,
|
| 1648 |
+
"coverage": 0.3485,
|
| 1649 |
+
"keystrokes_saved": 0.2114,
|
| 1650 |
+
"shown": 14454
|
| 1651 |
+
},
|
| 1652 |
+
{
|
| 1653 |
+
"tau": 0.79,
|
| 1654 |
+
"min_support": 5,
|
| 1655 |
+
"precision": 0.9356,
|
| 1656 |
+
"coverage": 0.2667,
|
| 1657 |
+
"keystrokes_saved": 0.1606,
|
| 1658 |
+
"shown": 11062
|
| 1659 |
+
},
|
| 1660 |
+
{
|
| 1661 |
+
"tau": 0.79,
|
| 1662 |
+
"min_support": 10,
|
| 1663 |
+
"precision": 0.9391,
|
| 1664 |
+
"coverage": 0.2247,
|
| 1665 |
+
"keystrokes_saved": 0.1355,
|
| 1666 |
+
"shown": 9320
|
| 1667 |
+
},
|
| 1668 |
+
{
|
| 1669 |
+
"tau": 0.8,
|
| 1670 |
+
"min_support": 0,
|
| 1671 |
+
"precision": 0.9351,
|
| 1672 |
+
"coverage": 0.3412,
|
| 1673 |
+
"keystrokes_saved": 0.2066,
|
| 1674 |
+
"shown": 14153
|
| 1675 |
+
},
|
| 1676 |
+
{
|
| 1677 |
+
"tau": 0.8,
|
| 1678 |
+
"min_support": 2,
|
| 1679 |
+
"precision": 0.9352,
|
| 1680 |
+
"coverage": 0.3409,
|
| 1681 |
+
"keystrokes_saved": 0.2065,
|
| 1682 |
+
"shown": 14139
|
| 1683 |
+
},
|
| 1684 |
+
{
|
| 1685 |
+
"tau": 0.8,
|
| 1686 |
+
"min_support": 5,
|
| 1687 |
+
"precision": 0.9396,
|
| 1688 |
+
"coverage": 0.2606,
|
| 1689 |
+
"keystrokes_saved": 0.1567,
|
| 1690 |
+
"shown": 10810
|
| 1691 |
+
},
|
| 1692 |
+
{
|
| 1693 |
+
"tau": 0.8,
|
| 1694 |
+
"min_support": 10,
|
| 1695 |
+
"precision": 0.9424,
|
| 1696 |
+
"coverage": 0.2192,
|
| 1697 |
+
"keystrokes_saved": 0.1318,
|
| 1698 |
+
"shown": 9093
|
| 1699 |
+
},
|
| 1700 |
+
{
|
| 1701 |
+
"tau": 0.81,
|
| 1702 |
+
"min_support": 0,
|
| 1703 |
+
"precision": 0.9379,
|
| 1704 |
+
"coverage": 0.335,
|
| 1705 |
+
"keystrokes_saved": 0.2032,
|
| 1706 |
+
"shown": 13894
|
| 1707 |
+
},
|
| 1708 |
+
{
|
| 1709 |
+
"tau": 0.81,
|
| 1710 |
+
"min_support": 2,
|
| 1711 |
+
"precision": 0.938,
|
| 1712 |
+
"coverage": 0.3347,
|
| 1713 |
+
"keystrokes_saved": 0.2031,
|
| 1714 |
+
"shown": 13881
|
| 1715 |
+
},
|
| 1716 |
+
{
|
| 1717 |
+
"tau": 0.81,
|
| 1718 |
+
"min_support": 5,
|
| 1719 |
+
"precision": 0.9423,
|
| 1720 |
+
"coverage": 0.2559,
|
| 1721 |
+
"keystrokes_saved": 0.154,
|
| 1722 |
+
"shown": 10615
|
| 1723 |
+
},
|
| 1724 |
+
{
|
| 1725 |
+
"tau": 0.81,
|
| 1726 |
+
"min_support": 10,
|
| 1727 |
+
"precision": 0.945,
|
| 1728 |
+
"coverage": 0.2153,
|
| 1729 |
+
"keystrokes_saved": 0.1299,
|
| 1730 |
+
"shown": 8931
|
| 1731 |
+
},
|
| 1732 |
+
{
|
| 1733 |
+
"tau": 0.82,
|
| 1734 |
+
"min_support": 0,
|
| 1735 |
+
"precision": 0.9402,
|
| 1736 |
+
"coverage": 0.3296,
|
| 1737 |
+
"keystrokes_saved": 0.1999,
|
| 1738 |
+
"shown": 13671
|
| 1739 |
+
},
|
| 1740 |
+
{
|
| 1741 |
+
"tau": 0.82,
|
| 1742 |
+
"min_support": 2,
|
| 1743 |
+
"precision": 0.9403,
|
| 1744 |
+
"coverage": 0.3293,
|
| 1745 |
+
"keystrokes_saved": 0.1998,
|
| 1746 |
+
"shown": 13658
|
| 1747 |
+
},
|
| 1748 |
+
{
|
| 1749 |
+
"tau": 0.82,
|
| 1750 |
+
"min_support": 5,
|
| 1751 |
+
"precision": 0.9439,
|
| 1752 |
+
"coverage": 0.2523,
|
| 1753 |
+
"keystrokes_saved": 0.1517,
|
| 1754 |
+
"shown": 10465
|
| 1755 |
+
},
|
| 1756 |
+
{
|
| 1757 |
+
"tau": 0.82,
|
| 1758 |
+
"min_support": 10,
|
| 1759 |
+
"precision": 0.9465,
|
| 1760 |
+
"coverage": 0.2124,
|
| 1761 |
+
"keystrokes_saved": 0.1281,
|
| 1762 |
+
"shown": 8808
|
| 1763 |
+
},
|
| 1764 |
+
{
|
| 1765 |
+
"tau": 0.83,
|
| 1766 |
+
"min_support": 0,
|
| 1767 |
+
"precision": 0.9433,
|
| 1768 |
+
"coverage": 0.323,
|
| 1769 |
+
"keystrokes_saved": 0.1966,
|
| 1770 |
+
"shown": 13396
|
| 1771 |
+
},
|
| 1772 |
+
{
|
| 1773 |
+
"tau": 0.83,
|
| 1774 |
+
"min_support": 2,
|
| 1775 |
+
"precision": 0.9434,
|
| 1776 |
+
"coverage": 0.3227,
|
| 1777 |
+
"keystrokes_saved": 0.1965,
|
| 1778 |
+
"shown": 13384
|
| 1779 |
+
},
|
| 1780 |
+
{
|
| 1781 |
+
"tau": 0.83,
|
| 1782 |
+
"min_support": 5,
|
| 1783 |
+
"precision": 0.9473,
|
| 1784 |
+
"coverage": 0.2474,
|
| 1785 |
+
"keystrokes_saved": 0.1494,
|
| 1786 |
+
"shown": 10260
|
| 1787 |
+
},
|
| 1788 |
+
{
|
| 1789 |
+
"tau": 0.83,
|
| 1790 |
+
"min_support": 10,
|
| 1791 |
+
"precision": 0.9495,
|
| 1792 |
+
"coverage": 0.2085,
|
| 1793 |
+
"keystrokes_saved": 0.1263,
|
| 1794 |
+
"shown": 8646
|
| 1795 |
+
},
|
| 1796 |
+
{
|
| 1797 |
+
"tau": 0.84,
|
| 1798 |
+
"min_support": 0,
|
| 1799 |
+
"precision": 0.9472,
|
| 1800 |
+
"coverage": 0.3166,
|
| 1801 |
+
"keystrokes_saved": 0.1932,
|
| 1802 |
+
"shown": 13130
|
| 1803 |
+
},
|
| 1804 |
+
{
|
| 1805 |
+
"tau": 0.84,
|
| 1806 |
+
"min_support": 2,
|
| 1807 |
+
"precision": 0.9472,
|
| 1808 |
+
"coverage": 0.3163,
|
| 1809 |
+
"keystrokes_saved": 0.1931,
|
| 1810 |
+
"shown": 13120
|
| 1811 |
+
},
|
| 1812 |
+
{
|
| 1813 |
+
"tau": 0.84,
|
| 1814 |
+
"min_support": 5,
|
| 1815 |
+
"precision": 0.951,
|
| 1816 |
+
"coverage": 0.2423,
|
| 1817 |
+
"keystrokes_saved": 0.1469,
|
| 1818 |
+
"shown": 10051
|
| 1819 |
+
},
|
| 1820 |
+
{
|
| 1821 |
+
"tau": 0.84,
|
| 1822 |
+
"min_support": 10,
|
| 1823 |
+
"precision": 0.9519,
|
| 1824 |
+
"coverage": 0.2047,
|
| 1825 |
+
"keystrokes_saved": 0.1247,
|
| 1826 |
+
"shown": 8490
|
| 1827 |
+
},
|
| 1828 |
+
{
|
| 1829 |
+
"tau": 0.85,
|
| 1830 |
+
"min_support": 0,
|
| 1831 |
+
"precision": 0.9507,
|
| 1832 |
+
"coverage": 0.3089,
|
| 1833 |
+
"keystrokes_saved": 0.1886,
|
| 1834 |
+
"shown": 12810
|
| 1835 |
+
},
|
| 1836 |
+
{
|
| 1837 |
+
"tau": 0.85,
|
| 1838 |
+
"min_support": 2,
|
| 1839 |
+
"precision": 0.9507,
|
| 1840 |
+
"coverage": 0.3086,
|
| 1841 |
+
"keystrokes_saved": 0.1886,
|
| 1842 |
+
"shown": 12800
|
| 1843 |
+
},
|
| 1844 |
+
{
|
| 1845 |
+
"tau": 0.85,
|
| 1846 |
+
"min_support": 5,
|
| 1847 |
+
"precision": 0.9542,
|
| 1848 |
+
"coverage": 0.2364,
|
| 1849 |
+
"keystrokes_saved": 0.1436,
|
| 1850 |
+
"shown": 9806
|
| 1851 |
+
},
|
| 1852 |
+
{
|
| 1853 |
+
"tau": 0.85,
|
| 1854 |
+
"min_support": 10,
|
| 1855 |
+
"precision": 0.9551,
|
| 1856 |
+
"coverage": 0.1999,
|
| 1857 |
+
"keystrokes_saved": 0.1218,
|
| 1858 |
+
"shown": 8292
|
| 1859 |
+
},
|
| 1860 |
+
{
|
| 1861 |
+
"tau": 0.86,
|
| 1862 |
+
"min_support": 0,
|
| 1863 |
+
"precision": 0.9535,
|
| 1864 |
+
"coverage": 0.3026,
|
| 1865 |
+
"keystrokes_saved": 0.183,
|
| 1866 |
+
"shown": 12550
|
| 1867 |
+
},
|
| 1868 |
+
{
|
| 1869 |
+
"tau": 0.86,
|
| 1870 |
+
"min_support": 2,
|
| 1871 |
+
"precision": 0.9534,
|
| 1872 |
+
"coverage": 0.3024,
|
| 1873 |
+
"keystrokes_saved": 0.1829,
|
| 1874 |
+
"shown": 12540
|
| 1875 |
+
},
|
| 1876 |
+
{
|
| 1877 |
+
"tau": 0.86,
|
| 1878 |
+
"min_support": 5,
|
| 1879 |
+
"precision": 0.9569,
|
| 1880 |
+
"coverage": 0.232,
|
| 1881 |
+
"keystrokes_saved": 0.1391,
|
| 1882 |
+
"shown": 9623
|
| 1883 |
+
},
|
| 1884 |
+
{
|
| 1885 |
+
"tau": 0.86,
|
| 1886 |
+
"min_support": 10,
|
| 1887 |
+
"precision": 0.9574,
|
| 1888 |
+
"coverage": 0.1965,
|
| 1889 |
+
"keystrokes_saved": 0.1179,
|
| 1890 |
+
"shown": 8149
|
| 1891 |
+
},
|
| 1892 |
+
{
|
| 1893 |
+
"tau": 0.87,
|
| 1894 |
+
"min_support": 0,
|
| 1895 |
+
"precision": 0.9565,
|
| 1896 |
+
"coverage": 0.2951,
|
| 1897 |
+
"keystrokes_saved": 0.1793,
|
| 1898 |
+
"shown": 12241
|
| 1899 |
+
},
|
| 1900 |
+
{
|
| 1901 |
+
"tau": 0.87,
|
| 1902 |
+
"min_support": 2,
|
| 1903 |
+
"precision": 0.9565,
|
| 1904 |
+
"coverage": 0.2949,
|
| 1905 |
+
"keystrokes_saved": 0.1792,
|
| 1906 |
+
"shown": 12232
|
| 1907 |
+
},
|
| 1908 |
+
{
|
| 1909 |
+
"tau": 0.87,
|
| 1910 |
+
"min_support": 5,
|
| 1911 |
+
"precision": 0.9593,
|
| 1912 |
+
"coverage": 0.2264,
|
| 1913 |
+
"keystrokes_saved": 0.1366,
|
| 1914 |
+
"shown": 9390
|
| 1915 |
+
},
|
| 1916 |
+
{
|
| 1917 |
+
"tau": 0.87,
|
| 1918 |
+
"min_support": 10,
|
| 1919 |
+
"precision": 0.9594,
|
| 1920 |
+
"coverage": 0.1919,
|
| 1921 |
+
"keystrokes_saved": 0.1159,
|
| 1922 |
+
"shown": 7958
|
| 1923 |
+
},
|
| 1924 |
+
{
|
| 1925 |
+
"tau": 0.88,
|
| 1926 |
+
"min_support": 0,
|
| 1927 |
+
"precision": 0.9588,
|
| 1928 |
+
"coverage": 0.2883,
|
| 1929 |
+
"keystrokes_saved": 0.1753,
|
| 1930 |
+
"shown": 11958
|
| 1931 |
+
},
|
| 1932 |
+
{
|
| 1933 |
+
"tau": 0.88,
|
| 1934 |
+
"min_support": 2,
|
| 1935 |
+
"precision": 0.9587,
|
| 1936 |
+
"coverage": 0.2881,
|
| 1937 |
+
"keystrokes_saved": 0.1752,
|
| 1938 |
+
"shown": 11949
|
| 1939 |
+
},
|
| 1940 |
+
{
|
| 1941 |
+
"tau": 0.88,
|
| 1942 |
+
"min_support": 5,
|
| 1943 |
+
"precision": 0.9607,
|
| 1944 |
+
"coverage": 0.2215,
|
| 1945 |
+
"keystrokes_saved": 0.1336,
|
| 1946 |
+
"shown": 9187
|
| 1947 |
+
},
|
| 1948 |
+
{
|
| 1949 |
+
"tau": 0.88,
|
| 1950 |
+
"min_support": 10,
|
| 1951 |
+
"precision": 0.9609,
|
| 1952 |
+
"coverage": 0.188,
|
| 1953 |
+
"keystrokes_saved": 0.1135,
|
| 1954 |
+
"shown": 7797
|
| 1955 |
+
},
|
| 1956 |
+
{
|
| 1957 |
+
"tau": 0.89,
|
| 1958 |
+
"min_support": 0,
|
| 1959 |
+
"precision": 0.9614,
|
| 1960 |
+
"coverage": 0.2802,
|
| 1961 |
+
"keystrokes_saved": 0.1691,
|
| 1962 |
+
"shown": 11621
|
| 1963 |
+
},
|
| 1964 |
+
{
|
| 1965 |
+
"tau": 0.89,
|
| 1966 |
+
"min_support": 2,
|
| 1967 |
+
"precision": 0.9613,
|
| 1968 |
+
"coverage": 0.28,
|
| 1969 |
+
"keystrokes_saved": 0.1691,
|
| 1970 |
+
"shown": 11612
|
| 1971 |
+
},
|
| 1972 |
+
{
|
| 1973 |
+
"tau": 0.89,
|
| 1974 |
+
"min_support": 5,
|
| 1975 |
+
"precision": 0.9631,
|
| 1976 |
+
"coverage": 0.2154,
|
| 1977 |
+
"keystrokes_saved": 0.1285,
|
| 1978 |
+
"shown": 8932
|
| 1979 |
+
},
|
| 1980 |
+
{
|
| 1981 |
+
"tau": 0.89,
|
| 1982 |
+
"min_support": 10,
|
| 1983 |
+
"precision": 0.9631,
|
| 1984 |
+
"coverage": 0.1829,
|
| 1985 |
+
"keystrokes_saved": 0.1092,
|
| 1986 |
+
"shown": 7587
|
| 1987 |
+
},
|
| 1988 |
+
{
|
| 1989 |
+
"tau": 0.9,
|
| 1990 |
+
"min_support": 0,
|
| 1991 |
+
"precision": 0.9644,
|
| 1992 |
+
"coverage": 0.2692,
|
| 1993 |
+
"keystrokes_saved": 0.1642,
|
| 1994 |
+
"shown": 11165
|
| 1995 |
+
},
|
| 1996 |
+
{
|
| 1997 |
+
"tau": 0.9,
|
| 1998 |
+
"min_support": 2,
|
| 1999 |
+
"precision": 0.9643,
|
| 2000 |
+
"coverage": 0.269,
|
| 2001 |
+
"keystrokes_saved": 0.1642,
|
| 2002 |
+
"shown": 11156
|
| 2003 |
+
},
|
| 2004 |
+
{
|
| 2005 |
+
"tau": 0.9,
|
| 2006 |
+
"min_support": 5,
|
| 2007 |
+
"precision": 0.9666,
|
| 2008 |
+
"coverage": 0.2069,
|
| 2009 |
+
"keystrokes_saved": 0.1256,
|
| 2010 |
+
"shown": 8580
|
| 2011 |
+
},
|
| 2012 |
+
{
|
| 2013 |
+
"tau": 0.9,
|
| 2014 |
+
"min_support": 10,
|
| 2015 |
+
"precision": 0.9666,
|
| 2016 |
+
"coverage": 0.1757,
|
| 2017 |
+
"keystrokes_saved": 0.1072,
|
| 2018 |
+
"shown": 7286
|
| 2019 |
+
},
|
| 2020 |
+
{
|
| 2021 |
+
"tau": 0.91,
|
| 2022 |
+
"min_support": 0,
|
| 2023 |
+
"precision": 0.9673,
|
| 2024 |
+
"coverage": 0.2609,
|
| 2025 |
+
"keystrokes_saved": 0.1552,
|
| 2026 |
+
"shown": 10820
|
| 2027 |
+
},
|
| 2028 |
+
{
|
| 2029 |
+
"tau": 0.91,
|
| 2030 |
+
"min_support": 2,
|
| 2031 |
+
"precision": 0.9673,
|
| 2032 |
+
"coverage": 0.2607,
|
| 2033 |
+
"keystrokes_saved": 0.1551,
|
| 2034 |
+
"shown": 10811
|
| 2035 |
+
},
|
| 2036 |
+
{
|
| 2037 |
+
"tau": 0.91,
|
| 2038 |
+
"min_support": 5,
|
| 2039 |
+
"precision": 0.9696,
|
| 2040 |
+
"coverage": 0.2008,
|
| 2041 |
+
"keystrokes_saved": 0.1177,
|
| 2042 |
+
"shown": 8328
|
| 2043 |
+
},
|
| 2044 |
+
{
|
| 2045 |
+
"tau": 0.91,
|
| 2046 |
+
"min_support": 10,
|
| 2047 |
+
"precision": 0.9698,
|
| 2048 |
+
"coverage": 0.171,
|
| 2049 |
+
"keystrokes_saved": 0.1002,
|
| 2050 |
+
"shown": 7093
|
| 2051 |
+
},
|
| 2052 |
+
{
|
| 2053 |
+
"tau": 0.92,
|
| 2054 |
+
"min_support": 0,
|
| 2055 |
+
"precision": 0.9693,
|
| 2056 |
+
"coverage": 0.2474,
|
| 2057 |
+
"keystrokes_saved": 0.1485,
|
| 2058 |
+
"shown": 10260
|
| 2059 |
+
},
|
| 2060 |
+
{
|
| 2061 |
+
"tau": 0.92,
|
| 2062 |
+
"min_support": 2,
|
| 2063 |
+
"precision": 0.9693,
|
| 2064 |
+
"coverage": 0.2472,
|
| 2065 |
+
"keystrokes_saved": 0.1484,
|
| 2066 |
+
"shown": 10253
|
| 2067 |
+
},
|
| 2068 |
+
{
|
| 2069 |
+
"tau": 0.92,
|
| 2070 |
+
"min_support": 5,
|
| 2071 |
+
"precision": 0.9717,
|
| 2072 |
+
"coverage": 0.19,
|
| 2073 |
+
"keystrokes_saved": 0.1126,
|
| 2074 |
+
"shown": 7878
|
| 2075 |
+
},
|
| 2076 |
+
{
|
| 2077 |
+
"tau": 0.92,
|
| 2078 |
+
"min_support": 10,
|
| 2079 |
+
"precision": 0.9714,
|
| 2080 |
+
"coverage": 0.1611,
|
| 2081 |
+
"keystrokes_saved": 0.0954,
|
| 2082 |
+
"shown": 6680
|
| 2083 |
+
},
|
| 2084 |
+
{
|
| 2085 |
+
"tau": 0.93,
|
| 2086 |
+
"min_support": 0,
|
| 2087 |
+
"precision": 0.9722,
|
| 2088 |
+
"coverage": 0.2361,
|
| 2089 |
+
"keystrokes_saved": 0.1417,
|
| 2090 |
+
"shown": 9791
|
| 2091 |
+
},
|
| 2092 |
+
{
|
| 2093 |
+
"tau": 0.93,
|
| 2094 |
+
"min_support": 2,
|
| 2095 |
+
"precision": 0.9722,
|
| 2096 |
+
"coverage": 0.2359,
|
| 2097 |
+
"keystrokes_saved": 0.1417,
|
| 2098 |
+
"shown": 9784
|
| 2099 |
+
},
|
| 2100 |
+
{
|
| 2101 |
+
"tau": 0.93,
|
| 2102 |
+
"min_support": 5,
|
| 2103 |
+
"precision": 0.9746,
|
| 2104 |
+
"coverage": 0.1815,
|
| 2105 |
+
"keystrokes_saved": 0.1077,
|
| 2106 |
+
"shown": 7526
|
| 2107 |
+
},
|
| 2108 |
+
{
|
| 2109 |
+
"tau": 0.93,
|
| 2110 |
+
"min_support": 10,
|
| 2111 |
+
"precision": 0.9746,
|
| 2112 |
+
"coverage": 0.154,
|
| 2113 |
+
"keystrokes_saved": 0.0913,
|
| 2114 |
+
"shown": 6388
|
| 2115 |
+
},
|
| 2116 |
+
{
|
| 2117 |
+
"tau": 0.94,
|
| 2118 |
+
"min_support": 0,
|
| 2119 |
+
"precision": 0.9748,
|
| 2120 |
+
"coverage": 0.2226,
|
| 2121 |
+
"keystrokes_saved": 0.1317,
|
| 2122 |
+
"shown": 9234
|
| 2123 |
+
},
|
| 2124 |
+
{
|
| 2125 |
+
"tau": 0.94,
|
| 2126 |
+
"min_support": 2,
|
| 2127 |
+
"precision": 0.9748,
|
| 2128 |
+
"coverage": 0.2225,
|
| 2129 |
+
"keystrokes_saved": 0.1316,
|
| 2130 |
+
"shown": 9228
|
| 2131 |
+
},
|
| 2132 |
+
{
|
| 2133 |
+
"tau": 0.94,
|
| 2134 |
+
"min_support": 5,
|
| 2135 |
+
"precision": 0.9774,
|
| 2136 |
+
"coverage": 0.1719,
|
| 2137 |
+
"keystrokes_saved": 0.1007,
|
| 2138 |
+
"shown": 7130
|
| 2139 |
+
},
|
| 2140 |
+
{
|
| 2141 |
+
"tau": 0.94,
|
| 2142 |
+
"min_support": 10,
|
| 2143 |
+
"precision": 0.9775,
|
| 2144 |
+
"coverage": 0.1459,
|
| 2145 |
+
"keystrokes_saved": 0.0852,
|
| 2146 |
+
"shown": 6052
|
| 2147 |
+
},
|
| 2148 |
+
{
|
| 2149 |
+
"tau": 0.95,
|
| 2150 |
+
"min_support": 0,
|
| 2151 |
+
"precision": 0.9773,
|
| 2152 |
+
"coverage": 0.2049,
|
| 2153 |
+
"keystrokes_saved": 0.1176,
|
| 2154 |
+
"shown": 8497
|
| 2155 |
+
},
|
| 2156 |
+
{
|
| 2157 |
+
"tau": 0.95,
|
| 2158 |
+
"min_support": 2,
|
| 2159 |
+
"precision": 0.9773,
|
| 2160 |
+
"coverage": 0.2047,
|
| 2161 |
+
"keystrokes_saved": 0.1176,
|
| 2162 |
+
"shown": 8491
|
| 2163 |
+
},
|
| 2164 |
+
{
|
| 2165 |
+
"tau": 0.95,
|
| 2166 |
+
"min_support": 5,
|
| 2167 |
+
"precision": 0.9806,
|
| 2168 |
+
"coverage": 0.1582,
|
| 2169 |
+
"keystrokes_saved": 0.0897,
|
| 2170 |
+
"shown": 6560
|
| 2171 |
+
},
|
| 2172 |
+
{
|
| 2173 |
+
"tau": 0.95,
|
| 2174 |
+
"min_support": 10,
|
| 2175 |
+
"precision": 0.9806,
|
| 2176 |
+
"coverage": 0.134,
|
| 2177 |
+
"keystrokes_saved": 0.0756,
|
| 2178 |
+
"shown": 5559
|
| 2179 |
+
}
|
| 2180 |
+
]
|
| 2181 |
+
}
|
export_web.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Write web/model.bin: the compiled model in sparse form for the browser.
|
| 2 |
+
|
| 3 |
+
The dense model.safetensors is ~93% zeros. Each MLP neuron of the compiled
|
| 4 |
+
transformer is fully described by its context tokens (which slots its gate
|
| 5 |
+
matches) and the logit changes its down-projection writes; the attention and
|
| 6 |
+
embedding weights are fixed by construction. This file stores exactly that, so
|
| 7 |
+
web/autocomplete.ts can evaluate only the neurons whose context matches: the
|
| 8 |
+
same computation as the transformer, without the zeros.
|
| 9 |
+
|
| 10 |
+
Layout (little-endian):
|
| 11 |
+
"TACB" | u32 version | u32 header bytes | header JSON | pad to 8
|
| 12 |
+
sections (each padded to 8), offsets relative to the first section:
|
| 13 |
+
base f32[V] unigram neuron (always on)
|
| 14 |
+
ctx{k} k=1..order u16/u32[n_k * k] context token ids, oldest first, sorted
|
| 15 |
+
support{k} f32[n_k] weighted count of the context
|
| 16 |
+
offsets{k} u32[n_k + 1] slice of this neuron's deltas
|
| 17 |
+
dids{k}, dvals{k} u16/u32, f32 next-token ids and logit changes
|
| 18 |
+
|
| 19 |
+
python export_web.py # uses the "web" settings and "policy" in compile_settings.json
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import json
|
| 23 |
+
from dataclasses import asdict
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
import numpy as np
|
| 27 |
+
|
| 28 |
+
import build_weights as bw
|
| 29 |
+
import suggest as sg
|
| 30 |
+
|
| 31 |
+
ROOT = Path(__file__).parent
|
| 32 |
+
OUT = ROOT / "web" / "model.bin"
|
| 33 |
+
MAGIC, VERSION = b"TACB", 1
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def load_policy() -> sg.Policy:
|
| 37 |
+
return sg.Policy(**json.loads(bw.SETTINGS_PATH.read_text())["policy"])
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def export(vocab: list[str], ngram: bw.NGram, oov: dict, policy: sg.Policy, path: Path) -> dict:
|
| 41 |
+
id_dtype = np.uint16 if len(vocab) < 65536 else np.uint32
|
| 42 |
+
sections, orders = [], []
|
| 43 |
+
|
| 44 |
+
def add(name: str, arr: np.ndarray) -> None:
|
| 45 |
+
sections.append((name, np.ascontiguousarray(arr).tobytes()))
|
| 46 |
+
|
| 47 |
+
add("base", ngram.base_logits.astype(np.float32))
|
| 48 |
+
for k in range(1, ngram.order + 1):
|
| 49 |
+
ctxs = sorted(ngram.tables[k])
|
| 50 |
+
offsets, dids, dvals = [0], [], []
|
| 51 |
+
for ctx in ctxs:
|
| 52 |
+
d = ngram.delta(ctx)
|
| 53 |
+
dids.extend(d.keys())
|
| 54 |
+
dvals.extend(d.values())
|
| 55 |
+
offsets.append(len(dids))
|
| 56 |
+
add(f"ctx{k}", np.array(ctxs, dtype=id_dtype).reshape(-1))
|
| 57 |
+
add(f"support{k}", np.array([ngram.support[c] for c in ctxs], dtype=np.float32))
|
| 58 |
+
add(f"offsets{k}", np.array(offsets, dtype=np.uint32))
|
| 59 |
+
add(f"dids{k}", np.array(dids, dtype=id_dtype))
|
| 60 |
+
add(f"dvals{k}", np.array(dvals, dtype=np.float32))
|
| 61 |
+
orders.append({"k": k, "contexts": len(ctxs), "pairs": len(dids)})
|
| 62 |
+
|
| 63 |
+
table, offset = [], 0
|
| 64 |
+
for name, data in sections:
|
| 65 |
+
table.append({"name": name, "offset": offset, "bytes": len(data)})
|
| 66 |
+
offset += -(-len(data) // 8) * 8
|
| 67 |
+
header = {
|
| 68 |
+
"n_prev": ngram.s.n_prev,
|
| 69 |
+
"vocab": vocab,
|
| 70 |
+
"eos_id": bw.EOS_ID,
|
| 71 |
+
"unk_id": bw.UNK_ID,
|
| 72 |
+
"id_bytes": np.dtype(id_dtype).itemsize,
|
| 73 |
+
"orders": orders,
|
| 74 |
+
"oov_prefix_rates": {p: round(r, 4) for p, r in oov.items()},
|
| 75 |
+
"policy": asdict(policy),
|
| 76 |
+
"stop_after": sorted(sg.STOP_AFTER),
|
| 77 |
+
"settings": asdict(ngram.s),
|
| 78 |
+
"sections": table,
|
| 79 |
+
}
|
| 80 |
+
head = json.dumps(header, separators=(",", ":")).encode()
|
| 81 |
+
path.parent.mkdir(exist_ok=True)
|
| 82 |
+
with open(path, "wb") as f:
|
| 83 |
+
f.write(MAGIC + np.array([VERSION, len(head)], dtype="<u4").tobytes() + head)
|
| 84 |
+
f.write(b"\0" * (-(12 + len(head)) % 8))
|
| 85 |
+
for _, data in sections:
|
| 86 |
+
f.write(data + b"\0" * (-len(data) % 8))
|
| 87 |
+
return {"bytes": path.stat().st_size, "contexts": sum(o["contexts"] for o in orders), "pairs": sum(o["pairs"] for o in orders)}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def build(settings: bw.Settings, sources: dict, policy: sg.Policy, path: Path = OUT) -> dict:
|
| 91 |
+
vocab, ngram, stats = bw.compile_tables(settings, sources)
|
| 92 |
+
oov = sg.oov_prefix_rates(settings, sources, vocab)
|
| 93 |
+
return {**stats, **export(vocab, ngram, oov, policy, path)}
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def main() -> None:
|
| 97 |
+
info = build(bw.load_settings("web"), bw.load_sources(), load_policy())
|
| 98 |
+
print(json.dumps({**info, "mb": round(info["bytes"] / 1e6, 1), "out": str(OUT)}, indent=2))
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
main()
|
model.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 1521162624
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ae857db3e321c35782192c198a7778987964e9cc147d2c67428fea9bf5777100
|
| 3 |
size 1521162624
|
model.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
| 4 |
|
| 5 |
import { readFileSync } from "node:fs";
|
| 6 |
import { join } from "node:path";
|
|
|
|
| 7 |
|
| 8 |
export type Config = {
|
| 9 |
vocab_size: number;
|
|
@@ -44,63 +45,6 @@ function loadSafetensors(path: string): Map<string, Float32Array> {
|
|
| 44 |
return tensors;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
// ---------- tokenizer (WordLevel + ByteLevel, as in tokenizer.json) ----------
|
| 48 |
-
|
| 49 |
-
function bytesToUnicode(): Map<number, string> {
|
| 50 |
-
const bs: number[] = [];
|
| 51 |
-
for (let b = 33; b <= 126; b++) bs.push(b);
|
| 52 |
-
for (let b = 161; b <= 172; b++) bs.push(b);
|
| 53 |
-
for (let b = 174; b <= 255; b++) bs.push(b);
|
| 54 |
-
const cs = [...bs];
|
| 55 |
-
let n = 0;
|
| 56 |
-
for (let b = 0; b < 256; b++) {
|
| 57 |
-
if (!bs.includes(b)) {
|
| 58 |
-
bs.push(b);
|
| 59 |
-
cs.push(256 + n++);
|
| 60 |
-
}
|
| 61 |
-
}
|
| 62 |
-
return new Map(bs.map((b, i) => [b, String.fromCodePoint(cs[i])]));
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
const GPT2_SPLIT = /'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu;
|
| 66 |
-
|
| 67 |
-
export class Tokenizer {
|
| 68 |
-
vocab: Map<string, number>;
|
| 69 |
-
inverse: string[];
|
| 70 |
-
special = new Set<number>();
|
| 71 |
-
unkId: number;
|
| 72 |
-
bosId: number;
|
| 73 |
-
private byteEnc = bytesToUnicode();
|
| 74 |
-
private byteDec = new Map([...this.byteEnc].map(([b, c]) => [c, b]));
|
| 75 |
-
|
| 76 |
-
constructor(path: string) {
|
| 77 |
-
const json = JSON.parse(readFileSync(path, "utf8"));
|
| 78 |
-
this.vocab = new Map(Object.entries<number>(json.model.vocab));
|
| 79 |
-
this.inverse = [];
|
| 80 |
-
for (const [tok, id] of this.vocab) this.inverse[id] = tok;
|
| 81 |
-
for (const t of json.added_tokens ?? []) if (t.special) this.special.add(t.id);
|
| 82 |
-
this.unkId = this.vocab.get(json.model.unk_token)!;
|
| 83 |
-
const bos = json.post_processor?.single?.find((p: any) => p.SpecialToken)?.SpecialToken?.id;
|
| 84 |
-
this.bosId = this.vocab.get(bos)!;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
encode(text: string): number[] {
|
| 88 |
-
let norm = text.normalize("NFKC").toLowerCase();
|
| 89 |
-
if (!/^\s/.test(norm)) norm = " " + norm; // ByteLevel add_prefix_space
|
| 90 |
-
const ids = [this.bosId];
|
| 91 |
-
for (const piece of norm.match(GPT2_SPLIT) ?? []) {
|
| 92 |
-
const mapped = [...new TextEncoder().encode(piece)].map((b) => this.byteEnc.get(b)!).join("");
|
| 93 |
-
ids.push(this.vocab.get(mapped) ?? this.unkId);
|
| 94 |
-
}
|
| 95 |
-
return ids;
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
decode(ids: number[]): string {
|
| 99 |
-
const chars = ids.filter((id) => !this.special.has(id)).map((id) => this.inverse[id]).join("");
|
| 100 |
-
return new TextDecoder().decode(new Uint8Array([...chars].map((c) => this.byteDec.get(c) ?? 63)));
|
| 101 |
-
}
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
// ---------- Llama forward pass ----------
|
| 105 |
|
| 106 |
function matvec(w: Float32Array, x: Float64Array, rows: number, cols: number): Float64Array {
|
|
@@ -228,12 +172,12 @@ export type Completion = {
|
|
| 228 |
};
|
| 229 |
|
| 230 |
export class Autocompleter {
|
| 231 |
-
tok:
|
| 232 |
model: Llama;
|
| 233 |
maxNew: number;
|
| 234 |
|
| 235 |
constructor(dir: string) {
|
| 236 |
-
this.tok =
|
| 237 |
this.model = new Llama(dir);
|
| 238 |
const gen = JSON.parse(readFileSync(join(dir, "generation_config.json"), "utf8"));
|
| 239 |
this.maxNew = gen.max_new_tokens ?? 16;
|
|
|
|
| 4 |
|
| 5 |
import { readFileSync } from "node:fs";
|
| 6 |
import { join } from "node:path";
|
| 7 |
+
import { WordTokenizer } from "./tokenizer.ts";
|
| 8 |
|
| 9 |
export type Config = {
|
| 10 |
vocab_size: number;
|
|
|
|
| 45 |
return tensors;
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
// ---------- Llama forward pass ----------
|
| 49 |
|
| 50 |
function matvec(w: Float32Array, x: Float64Array, rows: number, cols: number): Float64Array {
|
|
|
|
| 172 |
};
|
| 173 |
|
| 174 |
export class Autocompleter {
|
| 175 |
+
tok: WordTokenizer;
|
| 176 |
model: Llama;
|
| 177 |
maxNew: number;
|
| 178 |
|
| 179 |
constructor(dir: string) {
|
| 180 |
+
this.tok = WordTokenizer.fromJson(JSON.parse(readFileSync(join(dir, "tokenizer.json"), "utf8")));
|
| 181 |
this.model = new Llama(dir);
|
| 182 |
const gen = JSON.parse(readFileSync(join(dir, "generation_config.json"), "utf8"));
|
| 183 |
this.maxNew = gen.max_new_tokens ?? 16;
|
scripts/benchmark.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
"""Measure compile cost, inference speed, exactness and held-out
|
| 2 |
|
| 3 |
-
Writes docs/benchmarks.json. Run from the repo root after build_weights.py:
|
| 4 |
python scripts/benchmark.py
|
| 5 |
"""
|
| 6 |
|
|
@@ -11,17 +11,21 @@ import platform
|
|
| 11 |
import random
|
| 12 |
import resource
|
| 13 |
import shutil
|
|
|
|
| 14 |
import subprocess
|
| 15 |
import sys
|
| 16 |
import time
|
| 17 |
-
import zlib
|
| 18 |
from pathlib import Path
|
| 19 |
|
| 20 |
import torch
|
| 21 |
|
| 22 |
ROOT = Path(__file__).resolve().parent.parent
|
| 23 |
sys.path.insert(0, str(ROOT))
|
|
|
|
| 24 |
import build_weights as bw # noqa: E402
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
PROMPTS = [
|
| 27 |
"Once upon a", "I hope this email", "The quick brown", "Thank you for", "Looking forward to",
|
|
@@ -50,44 +54,53 @@ def hardware() -> dict:
|
|
| 50 |
"python": platform.python_version(), "torch": torch.__version__}
|
| 51 |
|
| 52 |
|
| 53 |
-
def
|
| 54 |
-
"""Full `python build_weights.py` runs: wall time, peak memory, determinism."""
|
| 55 |
times, hashes = [], []
|
| 56 |
for _ in range(n):
|
| 57 |
t0 = time.perf_counter()
|
| 58 |
-
subprocess.run(
|
| 59 |
times.append(time.perf_counter() - t0)
|
| 60 |
-
hashes.append(sha256(
|
| 61 |
-
peak = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
|
| 62 |
-
peak_gb = peak / 2**30 if sys.platform == "darwin" else peak / 2**20
|
| 63 |
return {
|
| 64 |
-
"
|
| 65 |
-
"
|
| 66 |
-
"peak_memory_gb": round(peak_gb, 1),
|
| 67 |
"byte_identical_rebuild": len(set(hashes)) == 1,
|
| 68 |
-
"
|
| 69 |
-
"
|
| 70 |
}
|
| 71 |
|
| 72 |
|
| 73 |
-
def
|
|
|
|
|
|
|
| 74 |
t0 = time.perf_counter()
|
| 75 |
-
|
| 76 |
t1 = time.perf_counter()
|
| 77 |
-
|
|
|
|
| 78 |
t2 = time.perf_counter()
|
| 79 |
-
|
|
|
|
| 80 |
return {
|
|
|
|
|
|
|
| 81 |
"tokenize_seconds": round(t1 - t0, 2),
|
| 82 |
-
"
|
| 83 |
-
"
|
|
|
|
| 84 |
"params": info["params"],
|
| 85 |
-
"nonzero_params":
|
| 86 |
-
|
| 87 |
-
"neurons": info["neurons"],
|
| 88 |
}
|
| 89 |
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
def torch_inference() -> dict:
|
| 92 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 93 |
|
|
@@ -95,7 +108,6 @@ def torch_inference() -> dict:
|
|
| 95 |
tok = AutoTokenizer.from_pretrained(ROOT)
|
| 96 |
model = AutoModelForCausalLM.from_pretrained(ROOT).eval()
|
| 97 |
load = time.perf_counter() - t0
|
| 98 |
-
|
| 99 |
with torch.no_grad():
|
| 100 |
ids = tok("Write a python function to", return_tensors="pt")
|
| 101 |
model(**ids) # warm-up
|
|
@@ -103,33 +115,27 @@ def torch_inference() -> dict:
|
|
| 103 |
for _ in range(20):
|
| 104 |
model(**ids)
|
| 105 |
forward_ms = (time.perf_counter() - t0) / 20 * 1000
|
| 106 |
-
|
| 107 |
new_tokens, t0 = 0, time.perf_counter()
|
| 108 |
for p in PROMPTS:
|
| 109 |
ids = tok(p, return_tensors="pt")
|
| 110 |
out = model.generate(**ids, max_new_tokens=16)
|
| 111 |
new_tokens += out.shape[1] - ids["input_ids"].shape[1]
|
| 112 |
gen = time.perf_counter() - t0
|
| 113 |
-
return {
|
| 114 |
-
|
| 115 |
-
"next_word_ms": round(forward_ms, 1),
|
| 116 |
-
"generate_tokens_per_second": round(new_tokens / gen, 1),
|
| 117 |
-
"threads": torch.get_num_threads(),
|
| 118 |
-
}
|
| 119 |
|
| 120 |
|
| 121 |
-
def
|
| 122 |
-
|
| 123 |
-
if not bun:
|
| 124 |
return None
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
return {"runtime": "bun, plain TypeScript, no ML library", "load_seconds": round(load, 1), "seconds_per_token": round(total / words, 2)}
|
| 133 |
|
| 134 |
|
| 135 |
def exactness() -> float:
|
|
@@ -146,12 +152,23 @@ def exactness() -> float:
|
|
| 146 |
return worst
|
| 147 |
|
| 148 |
|
| 149 |
-
def
|
| 150 |
-
"""
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
index = {p: i for i, p in enumerate(vocab)}
|
| 156 |
top1 = top3 = total = oov = 0
|
| 157 |
for toks in test:
|
|
@@ -165,27 +182,26 @@ def heldout(frac: float = 0.1) -> dict:
|
|
| 165 |
ranked = ngram.reference_logits(ids[: t + 1]).topk(3).indices.tolist()
|
| 166 |
top1 += ranked[0] == target
|
| 167 |
top3 += target in ranked
|
| 168 |
-
return {
|
| 169 |
-
|
| 170 |
-
"heldout_tokens": total,
|
| 171 |
-
"next_token_top1": round(top1 / total, 3),
|
| 172 |
-
"next_token_top3": round(top3 / total, 3),
|
| 173 |
-
"oov_rate": round(oov / total, 3),
|
| 174 |
-
}
|
| 175 |
|
| 176 |
|
| 177 |
def main() -> None:
|
| 178 |
results = {"hardware": hardware()}
|
| 179 |
-
print("compiling (2
|
| 180 |
-
results["
|
|
|
|
|
|
|
| 181 |
print("inference...", flush=True)
|
| 182 |
results["pytorch"] = torch_inference()
|
| 183 |
-
|
|
|
|
| 184 |
results["max_logprob_error_vs_ngram"] = exactness()
|
| 185 |
-
print("held-out
|
| 186 |
-
results["
|
| 187 |
-
|
| 188 |
-
|
|
|
|
| 189 |
print(json.dumps(results, indent=2))
|
| 190 |
|
| 191 |
|
|
|
|
| 1 |
+
"""Measure compile cost, inference speed, exactness and held-out quality.
|
| 2 |
|
| 3 |
+
Writes docs/benchmarks.json. Run from the repo root after build_weights.py and export_web.py:
|
| 4 |
python scripts/benchmark.py
|
| 5 |
"""
|
| 6 |
|
|
|
|
| 11 |
import random
|
| 12 |
import resource
|
| 13 |
import shutil
|
| 14 |
+
import statistics
|
| 15 |
import subprocess
|
| 16 |
import sys
|
| 17 |
import time
|
|
|
|
| 18 |
from pathlib import Path
|
| 19 |
|
| 20 |
import torch
|
| 21 |
|
| 22 |
ROOT = Path(__file__).resolve().parent.parent
|
| 23 |
sys.path.insert(0, str(ROOT))
|
| 24 |
+
sys.path.insert(0, str(ROOT / "scripts"))
|
| 25 |
import build_weights as bw # noqa: E402
|
| 26 |
+
import export_web # noqa: E402
|
| 27 |
+
import suggest as sg # noqa: E402
|
| 28 |
+
import sweep # noqa: E402
|
| 29 |
|
| 30 |
PROMPTS = [
|
| 31 |
"Once upon a", "I hope this email", "The quick brown", "Thank you for", "Looking forward to",
|
|
|
|
| 54 |
"python": platform.python_version(), "torch": torch.__version__}
|
| 55 |
|
| 56 |
|
| 57 |
+
def timed_runs(cmd: list[str], out: Path, n: int = 2) -> dict:
|
|
|
|
| 58 |
times, hashes = [], []
|
| 59 |
for _ in range(n):
|
| 60 |
t0 = time.perf_counter()
|
| 61 |
+
subprocess.run(cmd, cwd=ROOT, check=True, capture_output=True)
|
| 62 |
times.append(time.perf_counter() - t0)
|
| 63 |
+
hashes.append(sha256(out))
|
|
|
|
|
|
|
| 64 |
return {
|
| 65 |
+
"seconds": round(min(times), 1),
|
| 66 |
+
"runs": [round(t, 1) for t in times],
|
|
|
|
| 67 |
"byte_identical_rebuild": len(set(hashes)) == 1,
|
| 68 |
+
"sha256": hashes[0],
|
| 69 |
+
"mb": round(out.stat().st_size / 1e6, 2),
|
| 70 |
}
|
| 71 |
|
| 72 |
|
| 73 |
+
def compile_dense() -> dict:
|
| 74 |
+
runs = timed_runs([sys.executable, "build_weights.py"], ROOT / "model.safetensors")
|
| 75 |
+
peak = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
|
| 76 |
t0 = time.perf_counter()
|
| 77 |
+
sources = bw.load_sources()
|
| 78 |
t1 = time.perf_counter()
|
| 79 |
+
s = bw.load_settings("hf")
|
| 80 |
+
vocab, ngram, stats = bw.compile_tables(s, sources)
|
| 81 |
t2 = time.perf_counter()
|
| 82 |
+
state, _, info = bw.build_dense(vocab, ngram, s)
|
| 83 |
+
t3 = time.perf_counter()
|
| 84 |
return {
|
| 85 |
+
**runs,
|
| 86 |
+
"peak_memory_gb": round(peak / 2**30 if sys.platform == "darwin" else peak / 2**20, 1),
|
| 87 |
"tokenize_seconds": round(t1 - t0, 2),
|
| 88 |
+
"count_seconds": round(t2 - t1, 2),
|
| 89 |
+
"build_tensors_seconds": round(t3 - t2, 2),
|
| 90 |
+
"source_lines": sum(len(v) for v in sources.values()),
|
| 91 |
"params": info["params"],
|
| 92 |
+
"nonzero_params": sum(int((t != 0).sum()) for t in state.values()),
|
| 93 |
+
**stats,
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
|
| 97 |
+
def compile_web() -> dict:
|
| 98 |
+
runs = timed_runs([sys.executable, "export_web.py"], ROOT / "web" / "model.bin")
|
| 99 |
+
header_len = int.from_bytes((ROOT / "web" / "model.bin").read_bytes()[8:12], "little")
|
| 100 |
+
header = json.loads((ROOT / "web" / "model.bin").read_bytes()[12 : 12 + header_len])
|
| 101 |
+
return {**runs, "contexts": sum(o["contexts"] for o in header["orders"]), "vocab": len(header["vocab"])}
|
| 102 |
+
|
| 103 |
+
|
| 104 |
def torch_inference() -> dict:
|
| 105 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 106 |
|
|
|
|
| 108 |
tok = AutoTokenizer.from_pretrained(ROOT)
|
| 109 |
model = AutoModelForCausalLM.from_pretrained(ROOT).eval()
|
| 110 |
load = time.perf_counter() - t0
|
|
|
|
| 111 |
with torch.no_grad():
|
| 112 |
ids = tok("Write a python function to", return_tensors="pt")
|
| 113 |
model(**ids) # warm-up
|
|
|
|
| 115 |
for _ in range(20):
|
| 116 |
model(**ids)
|
| 117 |
forward_ms = (time.perf_counter() - t0) / 20 * 1000
|
|
|
|
| 118 |
new_tokens, t0 = 0, time.perf_counter()
|
| 119 |
for p in PROMPTS:
|
| 120 |
ids = tok(p, return_tensors="pt")
|
| 121 |
out = model.generate(**ids, max_new_tokens=16)
|
| 122 |
new_tokens += out.shape[1] - ids["input_ids"].shape[1]
|
| 123 |
gen = time.perf_counter() - t0
|
| 124 |
+
return {"load_seconds": round(load, 1), "next_word_ms": round(forward_ms, 1),
|
| 125 |
+
"generate_tokens_per_second": round(new_tokens / gen, 1), "threads": torch.get_num_threads()}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
|
| 128 |
+
def web_latency(texts: list[str]) -> dict | None:
|
| 129 |
+
"""Per-keystroke suggestion time of web/autocomplete.ts on held-out prefixes (Bun)."""
|
| 130 |
+
if not shutil.which("bun"):
|
| 131 |
return None
|
| 132 |
+
rng = random.Random(0)
|
| 133 |
+
prefixes = [t[: rng.randint(1, len(t) - 1)] for t in rng.sample(texts, 500)]
|
| 134 |
+
out = json.loads(subprocess.run(["bun", "web/cli.ts", "--json", *prefixes], cwd=ROOT, check=True,
|
| 135 |
+
capture_output=True, text=True).stdout)
|
| 136 |
+
ms = sorted(r["ms"] for r in out[50:]) # skip JIT warm-up
|
| 137 |
+
return {"runtime": "web/autocomplete.ts on Bun", "median_ms": round(statistics.median(ms), 2),
|
| 138 |
+
"p95_ms": round(ms[int(0.95 * len(ms))], 2), "max_ms": round(ms[-1], 2)}
|
|
|
|
| 139 |
|
| 140 |
|
| 141 |
def exactness() -> float:
|
|
|
|
| 152 |
return worst
|
| 153 |
|
| 154 |
|
| 155 |
+
def heldout_typing(name: str, texts: list[str], src: dict) -> dict:
|
| 156 |
+
"""Simulated typing on held-out prompts with the shipped policy, compiled without them."""
|
| 157 |
+
s = bw.load_settings(name)
|
| 158 |
+
vocab, ngram, _ = bw.compile_tables(s, src)
|
| 159 |
+
sugg = sg.Suggester(vocab, ngram, sg.oov_prefix_rates(s, src, vocab))
|
| 160 |
+
recs = sweep.record(sugg, texts)
|
| 161 |
+
policy = export_web.load_policy()
|
| 162 |
+
curve = {f"tau={t}": sweep.evaluate(recs, sg.Policy(tau=t, min_support=policy.min_support))
|
| 163 |
+
for t in [policy.tau, 0.8, 0.9, 0.95]}
|
| 164 |
+
return {"policy": policy.__dict__, "heldout_prompts": len(texts), "characters": sum(map(len, texts)), **curve}
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def heldout_next_token(texts_src: dict) -> dict:
|
| 168 |
+
"""Top-1/top-3 next-token accuracy of the dense model's tables on held-out prompts."""
|
| 169 |
+
s = bw.load_settings("hf")
|
| 170 |
+
_, test = bw.heldout_split(bw.load_sources()["prompts"])
|
| 171 |
+
vocab, ngram, _ = bw.compile_tables(s, texts_src)
|
| 172 |
index = {p: i for i, p in enumerate(vocab)}
|
| 173 |
top1 = top3 = total = oov = 0
|
| 174 |
for toks in test:
|
|
|
|
| 182 |
ranked = ngram.reference_logits(ids[: t + 1]).topk(3).indices.tolist()
|
| 183 |
top1 += ranked[0] == target
|
| 184 |
top3 += target in ranked
|
| 185 |
+
return {"heldout_lines": len(test), "heldout_tokens": total, "next_token_top1": round(top1 / total, 3),
|
| 186 |
+
"next_token_top3": round(top3 / total, 3), "oov_rate": round(oov / total, 3)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
|
| 189 |
def main() -> None:
|
| 190 |
results = {"hardware": hardware()}
|
| 191 |
+
print("compiling dense model (2 runs)...", flush=True)
|
| 192 |
+
results["compile_dense"] = compile_dense()
|
| 193 |
+
print("compiling web export (2 runs)...", flush=True)
|
| 194 |
+
results["compile_web"] = compile_web()
|
| 195 |
print("inference...", flush=True)
|
| 196 |
results["pytorch"] = torch_inference()
|
| 197 |
+
texts, src = sweep.heldout_texts(), sweep.train_sources()
|
| 198 |
+
results["web_keystroke_latency"] = web_latency(texts)
|
| 199 |
results["max_logprob_error_vs_ngram"] = exactness()
|
| 200 |
+
print("held-out quality...", flush=True)
|
| 201 |
+
results["heldout_typing_web"] = heldout_typing("web", texts, src)
|
| 202 |
+
results["heldout_typing_dense_tables"] = heldout_typing("hf", texts, src)
|
| 203 |
+
results["heldout_next_token_dense"] = heldout_next_token(src)
|
| 204 |
+
(ROOT / "docs" / "benchmarks.json").write_text(json.dumps(results, indent=2) + "\n")
|
| 205 |
print(json.dumps(results, indent=2))
|
| 206 |
|
| 207 |
|
scripts/experiment_topic.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Experiment: does long-range "topic" context help, before compiling it into attention?
|
| 2 |
+
|
| 3 |
+
Hypothesis: a head that attends to the most recent topic word anywhere earlier in the
|
| 4 |
+
prompt (python, sql, react, ...) gives context the 4-token n-gram window can't see.
|
| 5 |
+
|
| 6 |
+
Closed-form trigger model (no fitting): for each topic word t, count next-word
|
| 7 |
+
frequencies at positions where t is the most recent topic word earlier in the line.
|
| 8 |
+
The bias added to the n-gram logits is
|
| 9 |
+
lambda * log( p(w | t) / p(w) ), p(w | t) = (c(t, w) + 1) / (c(t) + V)
|
| 10 |
+
kept only where c(t, w) >= MIN_COUNT. In a compiled transformer this would be one
|
| 11 |
+
content-addressed attention head (keys = "is topic word" + code, recency via RoPE) and
|
| 12 |
+
one MLP neuron per topic word writing these biases.
|
| 13 |
+
|
| 14 |
+
Scored with the same held-out typing simulation as scripts/sweep.py.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
import sys
|
| 19 |
+
from collections import Counter, defaultdict
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
|
| 24 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 25 |
+
sys.path.insert(0, str(ROOT))
|
| 26 |
+
sys.path.insert(0, str(ROOT / "scripts"))
|
| 27 |
+
import build_weights as bw # noqa: E402
|
| 28 |
+
import suggest as sg # noqa: E402
|
| 29 |
+
import sweep # noqa: E402
|
| 30 |
+
|
| 31 |
+
TOPIC_WORDS = [
|
| 32 |
+
"python", "javascript", "js", "typescript", "java", "c", "c++", "c#", "rust", "golang", "go", "ruby", "php",
|
| 33 |
+
"kotlin", "swift", "sql", "mysql", "postgresql", "sqlite", "bash", "shell", "powershell", "html", "css", "react",
|
| 34 |
+
"vue", "angular", "node", "django", "flask", "pandas", "numpy", "pytorch", "tensorflow", "excel", "linux", "windows",
|
| 35 |
+
"git", "github", "docker", "kubernetes", "regex", "json", "api", "unity", "blender", "matlab", "r", "lua", "haskell",
|
| 36 |
+
]
|
| 37 |
+
MIN_COUNT = 3
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class TopicSuggester(sg.Suggester):
|
| 41 |
+
def __init__(self, vocab, ngram, oov, topics: set[int], bias: dict[int, tuple[np.ndarray, np.ndarray]], lam: float):
|
| 42 |
+
super().__init__(vocab, ngram, oov)
|
| 43 |
+
self.topics, self.bias, self.lam = topics, bias, lam
|
| 44 |
+
|
| 45 |
+
def topic_of(self, ids: list[int]) -> int | None:
|
| 46 |
+
for i in reversed(ids):
|
| 47 |
+
if i in self.topics:
|
| 48 |
+
return i
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
def dist(self, ids):
|
| 52 |
+
t = self.topic_of(ids)
|
| 53 |
+
key = (self.ngram.window(ids), t)
|
| 54 |
+
if key not in self._dist:
|
| 55 |
+
logits, support, _ = self.ngram.predict(ids)
|
| 56 |
+
if t is not None and t in self.bias:
|
| 57 |
+
idx, vals = self.bias[t]
|
| 58 |
+
logits[idx] += self.lam * vals
|
| 59 |
+
p = np.exp(logits - logits.max())
|
| 60 |
+
self._dist[key] = (p / p.sum(), support)
|
| 61 |
+
return self._dist[key]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def topic_bias(vocab: list[str], src: dict, s: bw.Settings, topics: set[int]):
|
| 65 |
+
index = {p: i for i, p in enumerate(vocab)}
|
| 66 |
+
V = len(vocab)
|
| 67 |
+
uni, by_topic = Counter(), defaultdict(Counter)
|
| 68 |
+
for lines, w in bw.weighted_sources(s, src):
|
| 69 |
+
for toks in lines:
|
| 70 |
+
ids = [index.get(p, bw.UNK_ID) for p in toks] + [bw.EOS_ID]
|
| 71 |
+
topic = None
|
| 72 |
+
for i, nxt in enumerate(ids):
|
| 73 |
+
if nxt != bw.UNK_ID:
|
| 74 |
+
uni[nxt] += w
|
| 75 |
+
if topic is not None:
|
| 76 |
+
by_topic[topic][nxt] += w
|
| 77 |
+
if i < len(ids) - 1 and ids[i] in topics:
|
| 78 |
+
topic = ids[i]
|
| 79 |
+
total = sum(uni.values())
|
| 80 |
+
out = {}
|
| 81 |
+
for t, cnt in by_topic.items():
|
| 82 |
+
ct = sum(cnt.values())
|
| 83 |
+
keep = [(w, c) for w, c in cnt.items() if c >= MIN_COUNT]
|
| 84 |
+
if not keep:
|
| 85 |
+
continue
|
| 86 |
+
idx = np.array([w for w, _ in keep])
|
| 87 |
+
vals = np.array([np.log((c + 1) / (ct + V)) - np.log((uni[w] + 1) / (total + V)) for w, c in keep])
|
| 88 |
+
out[t] = (idx, vals)
|
| 89 |
+
return out
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def main() -> None:
|
| 93 |
+
s = bw.load_settings("web")
|
| 94 |
+
src, texts = sweep.train_sources(), sweep.heldout_texts()
|
| 95 |
+
vocab, ngram, _ = bw.compile_tables(s, src)
|
| 96 |
+
oov = sg.oov_prefix_rates(s, src, vocab)
|
| 97 |
+
index = {p: i for i, p in enumerate(vocab)}
|
| 98 |
+
topics = {index[p] for w in TOPIC_WORDS for p in ["Ġ" + w] if p in index}
|
| 99 |
+
bias = topic_bias(vocab, src, s, topics)
|
| 100 |
+
with_topic = sum(1 for t in texts if any(("Ġ" + w) in bw.pieces(t) for w in TOPIC_WORDS))
|
| 101 |
+
print(f"topic words in vocab: {len(topics)}, with biases: {len(bias)}; held-out prompts mentioning one: {with_topic}/{len(texts)}")
|
| 102 |
+
results = []
|
| 103 |
+
for lam in [0.0, 0.25, 0.5, 1.0]:
|
| 104 |
+
sugg = TopicSuggester(vocab, ngram, oov, topics, bias, lam)
|
| 105 |
+
best = sweep.best_policy(sweep.record(sugg, texts))["best"]
|
| 106 |
+
results.append({"lambda": lam, **best})
|
| 107 |
+
print(f"lambda={lam}: saved={best['keystrokes_saved']} prec={best['precision']} cov={best['coverage']} tau={best['tau']}", flush=True)
|
| 108 |
+
(ROOT / "docs" / "experiment_topic.json").write_text(json.dumps({"topic_words": len(topics), "results": results}, indent=2) + "\n")
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
if __name__ == "__main__":
|
| 112 |
+
main()
|
scripts/fetch_coding_prompts.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
| 1 |
-
"""Download human-written coding prompts and
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
Sources (all human-written, permissively licensed; see data/SOURCES.md):
|
| 4 |
- OpenAssistant/oasst2 (Apache-2.0) English prompter and assistant messages from
|
|
@@ -18,7 +21,7 @@ from pathlib import Path
|
|
| 18 |
import pyarrow.parquet as pq
|
| 19 |
from huggingface_hub import hf_hub_download
|
| 20 |
|
| 21 |
-
|
| 22 |
MAX_WORDS = 24
|
| 23 |
MAX_PROMPT_SENTENCES = 2
|
| 24 |
MAX_REPLY_SENTENCES = 5
|
|
@@ -61,7 +64,7 @@ def clean(text: str, max_sentences: int = MAX_PROMPT_SENTENCES) -> list[str]:
|
|
| 61 |
return out
|
| 62 |
|
| 63 |
|
| 64 |
-
def oasst() -> list[str]:
|
| 65 |
path = hf_hub_download("OpenAssistant/oasst2", "2023-11-05_oasst2_ready.messages.jsonl.gz", repo_type="dataset")
|
| 66 |
rows = [json.loads(line) for line in gzip.open(path, "rt")]
|
| 67 |
rows = [
|
|
@@ -69,12 +72,14 @@ def oasst() -> list[str]:
|
|
| 69 |
if r["lang"] == "en" and not r["deleted"] and r["review_result"] and not r["synthetic"]
|
| 70 |
]
|
| 71 |
coding_trees = {r["message_tree_id"] for r in rows if r["parent_id"] is None and CODE_RE.search(r["text"])}
|
| 72 |
-
|
| 73 |
for r in rows:
|
| 74 |
if r["message_tree_id"] in coding_trees:
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
def mbpp() -> list[str]:
|
|
@@ -97,19 +102,24 @@ def conala() -> list[str]:
|
|
| 97 |
|
| 98 |
|
| 99 |
def main() -> None:
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
|
| 115 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
"""Download human-written coding text into data/prompts.txt and data/replies.txt.
|
| 2 |
+
|
| 3 |
+
prompts.txt: what people type to an assistant (OASST user turns, MBPP, CoNaLa).
|
| 4 |
+
replies.txt: OASST assistant turns (human-written replies).
|
| 5 |
|
| 6 |
Sources (all human-written, permissively licensed; see data/SOURCES.md):
|
| 7 |
- OpenAssistant/oasst2 (Apache-2.0) English prompter and assistant messages from
|
|
|
|
| 21 |
import pyarrow.parquet as pq
|
| 22 |
from huggingface_hub import hf_hub_download
|
| 23 |
|
| 24 |
+
DATA = Path(__file__).resolve().parent.parent / "data"
|
| 25 |
MAX_WORDS = 24
|
| 26 |
MAX_PROMPT_SENTENCES = 2
|
| 27 |
MAX_REPLY_SENTENCES = 5
|
|
|
|
| 64 |
return out
|
| 65 |
|
| 66 |
|
| 67 |
+
def oasst() -> tuple[list[str], list[str]]:
|
| 68 |
path = hf_hub_download("OpenAssistant/oasst2", "2023-11-05_oasst2_ready.messages.jsonl.gz", repo_type="dataset")
|
| 69 |
rows = [json.loads(line) for line in gzip.open(path, "rt")]
|
| 70 |
rows = [
|
|
|
|
| 72 |
if r["lang"] == "en" and not r["deleted"] and r["review_result"] and not r["synthetic"]
|
| 73 |
]
|
| 74 |
coding_trees = {r["message_tree_id"] for r in rows if r["parent_id"] is None and CODE_RE.search(r["text"])}
|
| 75 |
+
prompts, replies = [], []
|
| 76 |
for r in rows:
|
| 77 |
if r["message_tree_id"] in coding_trees:
|
| 78 |
+
if r["role"] == "prompter":
|
| 79 |
+
prompts += clean(r["text"], MAX_PROMPT_SENTENCES)
|
| 80 |
+
else:
|
| 81 |
+
replies += clean(r["text"], MAX_REPLY_SENTENCES)
|
| 82 |
+
return prompts, replies
|
| 83 |
|
| 84 |
|
| 85 |
def mbpp() -> list[str]:
|
|
|
|
| 102 |
|
| 103 |
|
| 104 |
def main() -> None:
|
| 105 |
+
oasst_prompts, oasst_replies = oasst()
|
| 106 |
+
sources = {
|
| 107 |
+
"prompts": [("oasst2_user", oasst_prompts), ("mbpp", mbpp()), ("conala", conala())],
|
| 108 |
+
"replies": [("oasst2_assistant", oasst_replies)],
|
| 109 |
+
}
|
| 110 |
+
seen, report = set(), {}
|
| 111 |
+
DATA.mkdir(exist_ok=True)
|
| 112 |
+
for kind, parts in sources.items():
|
| 113 |
+
lines = []
|
| 114 |
+
for name, got in parts:
|
| 115 |
+
report[name] = len(got)
|
| 116 |
+
for s in got:
|
| 117 |
+
if s.lower() not in seen:
|
| 118 |
+
seen.add(s.lower())
|
| 119 |
+
lines.append(s)
|
| 120 |
+
(DATA / f"{kind}.txt").write_text("\n".join(lines) + "\n")
|
| 121 |
+
report[f"{kind}_unique"] = len(lines)
|
| 122 |
+
print(json.dumps(report, indent=2))
|
| 123 |
|
| 124 |
|
| 125 |
if __name__ == "__main__":
|
scripts/sweep.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Choose compile settings and the confidence policy by simulated typing on held-out prompts.
|
| 2 |
+
|
| 3 |
+
The weights are always produced by the same closed-form compile; this script only
|
| 4 |
+
compares a handful of compile settings (context length, smoothing, source weights,
|
| 5 |
+
vocabulary, pruning) and policy thresholds, and reports which one saves the most
|
| 6 |
+
keystrokes while keeping precision >= TARGET_PRECISION.
|
| 7 |
+
|
| 8 |
+
Simulation: each held-out prompt (10% of data/prompts.txt, never compiled in) is
|
| 9 |
+
"typed" one character at a time. After every keystroke the policy shows ghost text
|
| 10 |
+
or nothing. A shown suggestion is correct only if the user's next characters start
|
| 11 |
+
with the whole ghost text. Keystrokes saved replays the prompt accepting every
|
| 12 |
+
correct suggestion with one Tab press.
|
| 13 |
+
|
| 14 |
+
python scripts/sweep.py # writes docs/sweep.json
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
import sys
|
| 19 |
+
import time
|
| 20 |
+
import zlib
|
| 21 |
+
from dataclasses import asdict, replace
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
|
| 24 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 25 |
+
sys.path.insert(0, str(ROOT))
|
| 26 |
+
import build_weights as bw # noqa: E402
|
| 27 |
+
import suggest as sg # noqa: E402
|
| 28 |
+
|
| 29 |
+
TARGET_PRECISION = 0.90
|
| 30 |
+
MAX_EXPORT_MB = 50
|
| 31 |
+
TAUS = [round(0.5 + 0.01 * i, 2) for i in range(46)] # 0.50 .. 0.95
|
| 32 |
+
SUPPORTS = [0, 2, 5, 10]
|
| 33 |
+
BASE = bw.Settings(n_prev=3, smoothing="wb", phrase_weight=5, prompt_weight=1, reply_weight=1, max_vocab=8000, min_count=2)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def heldout_texts() -> list[str]:
|
| 37 |
+
seen, out = set(), []
|
| 38 |
+
for line in (bw.DATA_DIR / "prompts.txt").read_text().splitlines():
|
| 39 |
+
toks = bw.pieces(line)
|
| 40 |
+
if toks and zlib.crc32(" ".join(toks).encode()) % 100 < 10:
|
| 41 |
+
text = bw.NORMALIZER.normalize_str(line.strip())
|
| 42 |
+
if text not in seen:
|
| 43 |
+
seen.add(text)
|
| 44 |
+
out.append(text)
|
| 45 |
+
return out
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def train_sources() -> dict:
|
| 49 |
+
src = bw.load_sources()
|
| 50 |
+
src["prompts"], _ = bw.heldout_split(src["prompts"])
|
| 51 |
+
return src
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def record(sugg: sg.Suggester, texts: list[str]) -> list[tuple[str, list]]:
|
| 55 |
+
"""For every prompt and every keystroke position, the unthresholded suggestion chain."""
|
| 56 |
+
return [(t, [None] + [sugg.steps(t[:i]) for i in range(1, len(t))]) for t in texts]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def evaluate(records, policy: sg.Policy) -> dict:
|
| 60 |
+
shown = correct = positions = chars = strokes = 0
|
| 61 |
+
for text, steps in records:
|
| 62 |
+
ghosts = [None] + [sg.Suggester.apply(st, policy) for st in steps[1:]]
|
| 63 |
+
for i in range(1, len(text)):
|
| 64 |
+
positions += 1
|
| 65 |
+
if ghosts[i]:
|
| 66 |
+
shown += 1
|
| 67 |
+
correct += text.startswith(ghosts[i], i)
|
| 68 |
+
pos, n = 0, 0
|
| 69 |
+
while pos < len(text):
|
| 70 |
+
pos, n = pos + 1, n + 1
|
| 71 |
+
g = ghosts[pos] if pos < len(text) else ""
|
| 72 |
+
if g and text.startswith(g, pos):
|
| 73 |
+
pos, n = pos + len(g), n + 1
|
| 74 |
+
chars, strokes = chars + len(text), strokes + n
|
| 75 |
+
return {
|
| 76 |
+
"precision": round(correct / shown, 4) if shown else None,
|
| 77 |
+
"coverage": round(shown / positions, 4),
|
| 78 |
+
"keystrokes_saved": round(1 - strokes / chars, 4),
|
| 79 |
+
"shown": shown,
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def best_policy(records) -> dict:
|
| 84 |
+
results = []
|
| 85 |
+
for tau in TAUS:
|
| 86 |
+
for sup in SUPPORTS:
|
| 87 |
+
m = evaluate(records, sg.Policy(tau=tau, min_support=sup))
|
| 88 |
+
results.append({"tau": tau, "min_support": sup, **m})
|
| 89 |
+
ok = [r for r in results if r["precision"] is not None and r["precision"] >= TARGET_PRECISION]
|
| 90 |
+
best = max(ok, key=lambda r: r["keystrokes_saved"]) if ok else None
|
| 91 |
+
return {"best": best, "grid": results}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def export_mb(ngram: bw.NGram, vocab: list[str], oov: dict) -> float:
|
| 95 |
+
"""Size of web/model.bin for these tables (see export_web.py)."""
|
| 96 |
+
size = sum(len(p.encode()) + 1 for p in vocab) + 8 * len(vocab) + sum(len(p.encode()) + 5 for p in oov)
|
| 97 |
+
for k in range(1, ngram.order + 1):
|
| 98 |
+
for table in ngram.tables[k].values():
|
| 99 |
+
size += 2 * k + 8 + 6 * len(table)
|
| 100 |
+
return round(size / 1e6, 1)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def run(s: bw.Settings, src: dict, texts: list[str]) -> dict:
|
| 104 |
+
t0 = time.time()
|
| 105 |
+
vocab, ngram, stats = bw.compile_tables(s, src)
|
| 106 |
+
oov = sg.oov_prefix_rates(s, src, vocab)
|
| 107 |
+
recs = record(sg.Suggester(vocab, ngram, oov), texts)
|
| 108 |
+
pol = best_policy(recs)
|
| 109 |
+
out = {
|
| 110 |
+
"settings": asdict(s),
|
| 111 |
+
**stats,
|
| 112 |
+
"export_mb": export_mb(ngram, vocab, oov),
|
| 113 |
+
"best": pol["best"],
|
| 114 |
+
"seconds": round(time.time() - t0, 1),
|
| 115 |
+
"grid": pol["grid"],
|
| 116 |
+
}
|
| 117 |
+
b = out["best"] or {}
|
| 118 |
+
print(
|
| 119 |
+
f"n_prev={s.n_prev} {s.smoothing} w=({s.phrase_weight},{s.prompt_weight},{s.reply_weight}) "
|
| 120 |
+
f"V={s.max_vocab} min={s.min_count} | {out['export_mb']} MB | "
|
| 121 |
+
f"saved={b.get('keystrokes_saved')} prec={b.get('precision')} cov={b.get('coverage')} "
|
| 122 |
+
f"tau={b.get('tau')} sup={b.get('min_support')} | {out['seconds']}s",
|
| 123 |
+
flush=True,
|
| 124 |
+
)
|
| 125 |
+
return out
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def score(r: dict) -> float:
|
| 129 |
+
if r["best"] is None or r["export_mb"] > MAX_EXPORT_MB:
|
| 130 |
+
return -1
|
| 131 |
+
return r["best"]["keystrokes_saved"]
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def main() -> None:
|
| 135 |
+
src, texts = train_sources(), heldout_texts()
|
| 136 |
+
print(f"held-out prompts: {len(texts)}, characters: {sum(map(len, texts))}", flush=True)
|
| 137 |
+
runs = []
|
| 138 |
+
|
| 139 |
+
print("== stage A: context length x smoothing", flush=True)
|
| 140 |
+
stage = [run(replace(BASE, n_prev=n, smoothing=sm), src, texts) for sm in ["wb", "kn"] for n in [3, 4, 5, 7, 9]]
|
| 141 |
+
runs += stage
|
| 142 |
+
best = max(stage, key=score)
|
| 143 |
+
|
| 144 |
+
print("== stage B: source weights", flush=True)
|
| 145 |
+
base = bw.Settings(**best["settings"])
|
| 146 |
+
stage = [
|
| 147 |
+
run(replace(base, phrase_weight=pw, reply_weight=rw), src, texts)
|
| 148 |
+
for pw in [1.0, 5.0]
|
| 149 |
+
for rw in [0.0, 0.3, 1.0]
|
| 150 |
+
if (pw, rw) != (base.phrase_weight, base.reply_weight)
|
| 151 |
+
]
|
| 152 |
+
runs += stage
|
| 153 |
+
best = max(runs, key=score)
|
| 154 |
+
|
| 155 |
+
print("== stage C: vocabulary x pruning", flush=True)
|
| 156 |
+
base = bw.Settings(**best["settings"])
|
| 157 |
+
stage = [
|
| 158 |
+
run(replace(base, max_vocab=v, min_count=mc), src, texts)
|
| 159 |
+
for v in [3000, 8000, 16000]
|
| 160 |
+
for mc in [2, 3]
|
| 161 |
+
if (v, mc) != (base.max_vocab, base.min_count)
|
| 162 |
+
]
|
| 163 |
+
runs += stage
|
| 164 |
+
best = max(runs, key=score)
|
| 165 |
+
|
| 166 |
+
print("== stage D: context length again, with the final settings", flush=True)
|
| 167 |
+
base = bw.Settings(**best["settings"])
|
| 168 |
+
stage = [run(replace(base, n_prev=n), src, texts) for n in [3, 4, 5, 7, 9] if n != base.n_prev]
|
| 169 |
+
runs += stage
|
| 170 |
+
best = max(runs, key=score)
|
| 171 |
+
|
| 172 |
+
out = ROOT / "docs" / "sweep.json"
|
| 173 |
+
out.write_text(json.dumps({
|
| 174 |
+
"target_precision": TARGET_PRECISION,
|
| 175 |
+
"max_export_mb": MAX_EXPORT_MB,
|
| 176 |
+
"heldout_prompts": len(texts),
|
| 177 |
+
"best": {k: v for k, v in best.items() if k != "grid"},
|
| 178 |
+
"runs": [{k: v for k, v in r.items() if k != "grid"} for r in runs],
|
| 179 |
+
"best_grid": best["grid"],
|
| 180 |
+
}, indent=2) + "\n")
|
| 181 |
+
print("BEST", json.dumps({k: v for k, v in best.items() if k != "grid"}, indent=2))
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
if __name__ == "__main__":
|
| 185 |
+
main()
|
suggest.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Confidence-gated ghost-text suggestions (reference implementation).
|
| 2 |
+
|
| 3 |
+
web/autocomplete.ts implements the same policy for the browser; the tests check
|
| 4 |
+
they agree. Nothing here is learned: the policy reads the compiled model's
|
| 5 |
+
next-token probabilities and only suggests when they clear fixed thresholds.
|
| 6 |
+
|
| 7 |
+
Given the text typed so far:
|
| 8 |
+
1. The last pre-token is the word being typed (just "Ġ" after a space). Only
|
| 9 |
+
vocabulary tokens that start with it are candidates ("fun" -> "function").
|
| 10 |
+
2. Candidate probabilities are renormalized over the candidates and scaled by
|
| 11 |
+
the chance that a word with this prefix is in the vocabulary at all
|
| 12 |
+
(counted from the source text), so rare out-of-vocabulary words don't
|
| 13 |
+
produce confident wrong completions.
|
| 14 |
+
3. The suggestion is shown only if that probability >= tau and the longest
|
| 15 |
+
matched context was seen >= min_support times. It then extends greedily,
|
| 16 |
+
word by word, while the product of probabilities stays >= tau, stopping
|
| 17 |
+
at end-of-text or after . ? ! :
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from collections import Counter
|
| 21 |
+
from dataclasses import dataclass
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
|
| 25 |
+
from build_weights import EOS_ID, UNK_ID, NGram, Settings, pieces, weighted_sources
|
| 26 |
+
|
| 27 |
+
STOP_AFTER = {".", "?", "!", ":"}
|
| 28 |
+
MAX_WORDS = 8
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass(frozen=True)
|
| 32 |
+
class Policy:
|
| 33 |
+
tau: float = 0.6 # min probability of the whole suggestion
|
| 34 |
+
min_support: float = 2 # min (weighted) count of the longest matched context
|
| 35 |
+
max_words: int = MAX_WORDS
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass(frozen=True)
|
| 39 |
+
class Step:
|
| 40 |
+
text: str # characters this step adds to the ghost text
|
| 41 |
+
prob: float # probability of the suggestion up to and including this step
|
| 42 |
+
support: float # count of the longest context matched for this step
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _byte_decoder() -> dict[str, int]:
|
| 46 |
+
bs = list(range(33, 127)) + list(range(161, 173)) + list(range(174, 256))
|
| 47 |
+
cs, n = bs[:], 0
|
| 48 |
+
for b in range(256):
|
| 49 |
+
if b not in bs:
|
| 50 |
+
bs.append(b)
|
| 51 |
+
cs.append(256 + n)
|
| 52 |
+
n += 1
|
| 53 |
+
return {chr(c): b for b, c in zip(bs, cs)}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
BYTE_DECODER = _byte_decoder()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def decode_piece(p: str) -> str:
|
| 60 |
+
return bytes(BYTE_DECODER[c] for c in p).decode("utf-8", errors="replace")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def oov_prefix_rates(s: Settings, sources: dict, vocab: list[str]) -> dict[str, float]:
|
| 64 |
+
"""P(word is out of vocabulary | word starts with prefix), from weighted source counts.
|
| 65 |
+
|
| 66 |
+
Only prefixes with a non-zero rate are stored; prefixes of in-vocabulary words that
|
| 67 |
+
never start an OOV word are omitted (rate 0).
|
| 68 |
+
"""
|
| 69 |
+
in_vocab = set(vocab)
|
| 70 |
+
total, oov = Counter(), Counter()
|
| 71 |
+
for lines, w in weighted_sources(s, sources):
|
| 72 |
+
for toks in lines:
|
| 73 |
+
for p in toks:
|
| 74 |
+
for i in range(1, len(p) + 1):
|
| 75 |
+
total[p[:i]] += w
|
| 76 |
+
if p not in in_vocab:
|
| 77 |
+
oov[p[:i]] += w
|
| 78 |
+
return {pre: oov[pre] / total[pre] for pre in oov}
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class Suggester:
|
| 82 |
+
def __init__(self, vocab: list[str], ngram: NGram, oov_rates: dict[str, float]):
|
| 83 |
+
self.vocab, self.ngram, self.oov = vocab, ngram, oov_rates
|
| 84 |
+
self.index = {p: i for i, p in enumerate(vocab)}
|
| 85 |
+
self.text = [decode_piece(p) for p in vocab]
|
| 86 |
+
self._dist, self._cands = {}, {}
|
| 87 |
+
|
| 88 |
+
def dist(self, ids: list[int]) -> tuple[np.ndarray, float]:
|
| 89 |
+
key = self.ngram.window(ids)
|
| 90 |
+
if key not in self._dist:
|
| 91 |
+
logits, support, _ = self.ngram.predict(ids)
|
| 92 |
+
p = np.exp(logits - logits.max())
|
| 93 |
+
self._dist[key] = (p / p.sum(), support)
|
| 94 |
+
return self._dist[key]
|
| 95 |
+
|
| 96 |
+
def candidates(self, partial: str) -> np.ndarray:
|
| 97 |
+
if partial not in self._cands:
|
| 98 |
+
self._cands[partial] = np.array(
|
| 99 |
+
[i for i, p in enumerate(self.vocab) if i > UNK_ID and p.startswith(partial)], dtype=np.int64
|
| 100 |
+
)
|
| 101 |
+
return self._cands[partial]
|
| 102 |
+
|
| 103 |
+
def steps(self, text: str, max_words: int = MAX_WORDS) -> list[Step]:
|
| 104 |
+
"""Greedy suggestion chain with cumulative probabilities (before any thresholding)."""
|
| 105 |
+
pcs = pieces(text)
|
| 106 |
+
if not pcs:
|
| 107 |
+
return []
|
| 108 |
+
partial = pcs[-1]
|
| 109 |
+
cands = self.candidates(partial)
|
| 110 |
+
if not len(cands):
|
| 111 |
+
return []
|
| 112 |
+
ids = [EOS_ID] + [self.index.get(p, UNK_ID) for p in pcs[:-1]]
|
| 113 |
+
probs, support = self.dist(ids)
|
| 114 |
+
pc = probs[cands]
|
| 115 |
+
best = int(cands[pc.argmax()])
|
| 116 |
+
prob = (1.0 - self.oov.get(partial, 0.0)) * pc.max() / pc.sum()
|
| 117 |
+
out = [Step(self.text[best][len(decode_piece(partial)) :], prob, support)]
|
| 118 |
+
ids.append(best)
|
| 119 |
+
while self.vocab[ids[-1]] not in STOP_AFTER and len(out) < max_words:
|
| 120 |
+
probs, support = self.dist(ids)
|
| 121 |
+
nxt = int(probs.argmax())
|
| 122 |
+
if nxt == EOS_ID:
|
| 123 |
+
break
|
| 124 |
+
prob *= probs[nxt]
|
| 125 |
+
out.append(Step(self.text[nxt], prob, support))
|
| 126 |
+
ids.append(nxt)
|
| 127 |
+
return out
|
| 128 |
+
|
| 129 |
+
@staticmethod
|
| 130 |
+
def apply(steps: list[Step], policy: Policy) -> str:
|
| 131 |
+
ghost = ""
|
| 132 |
+
for i, st in enumerate(steps[: policy.max_words]):
|
| 133 |
+
if st.prob < policy.tau or st.support < policy.min_support:
|
| 134 |
+
break
|
| 135 |
+
ghost += st.text
|
| 136 |
+
return ghost
|
| 137 |
+
|
| 138 |
+
def suggest(self, text: str, policy: Policy = Policy()) -> str:
|
| 139 |
+
return self.apply(self.steps(text, policy.max_words), policy)
|
test_model.py
CHANGED
|
@@ -24,15 +24,15 @@ CASES = {
|
|
| 24 |
"Thank you for your": "help.",
|
| 25 |
"I hope this email": "finds you well.",
|
| 26 |
"Can I have the": "menu, please?",
|
| 27 |
-
"Looking forward to": "
|
| 28 |
"Actions speak": "louder than words.",
|
| 29 |
"Where is the": "bathroom?",
|
| 30 |
"Happy birthday": ".",
|
| 31 |
"I'm so proud": "of you.",
|
| 32 |
-
# coding prompts (data/
|
| 33 |
"In Python,": "you can use the following code:",
|
| 34 |
-
"
|
| 35 |
-
"
|
| 36 |
}
|
| 37 |
|
| 38 |
|
|
@@ -103,5 +103,66 @@ class TestModel(unittest.TestCase):
|
|
| 103 |
self.assertEqual(res["completion"].strip(), self.complete(prompt))
|
| 104 |
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
if __name__ == "__main__":
|
| 107 |
unittest.main()
|
|
|
|
| 24 |
"Thank you for your": "help.",
|
| 25 |
"I hope this email": "finds you well.",
|
| 26 |
"Can I have the": "menu, please?",
|
| 27 |
+
"Looking forward to": "working with you.",
|
| 28 |
"Actions speak": "louder than words.",
|
| 29 |
"Where is the": "bathroom?",
|
| 30 |
"Happy birthday": ".",
|
| 31 |
"I'm so proud": "of you.",
|
| 32 |
+
# coding prompts and replies (data/*.txt)
|
| 33 |
"In Python,": "you can use the following code:",
|
| 34 |
+
"You can use the": "following code:",
|
| 35 |
+
"How to convert a": "string to a file in python",
|
| 36 |
}
|
| 37 |
|
| 38 |
|
|
|
|
| 103 |
self.assertEqual(res["completion"].strip(), self.complete(prompt))
|
| 104 |
|
| 105 |
|
| 106 |
+
@unittest.skipUnless(shutil.which("bun"), "bun not installed")
|
| 107 |
+
class TestWebExport(unittest.TestCase):
|
| 108 |
+
"""web/model.bin + web/autocomplete.ts (run `python export_web.py` first)."""
|
| 109 |
+
|
| 110 |
+
@classmethod
|
| 111 |
+
def setUpClass(cls):
|
| 112 |
+
import export_web
|
| 113 |
+
import suggest
|
| 114 |
+
|
| 115 |
+
cls.export_web, cls.suggest = export_web, suggest
|
| 116 |
+
cls.settings, cls.sources = build_weights.load_settings("web"), build_weights.load_sources()
|
| 117 |
+
cls.policy = export_web.load_policy()
|
| 118 |
+
vocab, ngram, _ = build_weights.compile_tables(cls.settings, cls.sources)
|
| 119 |
+
oov = suggest.oov_prefix_rates(cls.settings, cls.sources, vocab)
|
| 120 |
+
cls.suggester = suggest.Suggester(vocab, ngram, oov)
|
| 121 |
+
|
| 122 |
+
def cli(self, *args: str) -> list:
|
| 123 |
+
out = subprocess.run(["bun", "web/cli.ts", *args], cwd=MODEL_DIR, capture_output=True, text=True, check=True)
|
| 124 |
+
return json.loads(out.stdout)
|
| 125 |
+
|
| 126 |
+
def test_size(self):
|
| 127 |
+
self.assertLessEqual((MODEL_DIR / "web" / "model.bin").stat().st_size, 50_000_000)
|
| 128 |
+
|
| 129 |
+
def test_matches_python_policy(self):
|
| 130 |
+
"""Same suggestion chains (text, probability, support) as suggest.py on random prefixes."""
|
| 131 |
+
rng = random.Random(1)
|
| 132 |
+
lines = [ln for ln in (MODEL_DIR / "data" / "prompts.txt").read_text().splitlines() if len(ln) > 2]
|
| 133 |
+
prefixes = [t[: rng.randint(1, len(t) - 1)] for t in rng.sample(lines, 300)]
|
| 134 |
+
for p, res in zip(prefixes, self.cli("--json", *prefixes)):
|
| 135 |
+
with self.subTest(prefix=p):
|
| 136 |
+
py = self.suggester.steps(p, self.policy.max_words)
|
| 137 |
+
self.assertEqual([s.text for s in py], [s["text"] for s in res["steps"]])
|
| 138 |
+
for a, b in zip(py, res["steps"]):
|
| 139 |
+
self.assertAlmostEqual(a.prob, b["prob"], places=3)
|
| 140 |
+
self.assertAlmostEqual(a.support, b["support"], places=3)
|
| 141 |
+
if all(abs(s.prob - self.policy.tau) > 1e-3 for s in py): # skip float32 ties at the threshold
|
| 142 |
+
self.assertEqual(self.suggest.Suggester.apply(py, self.policy), res["ghost"])
|
| 143 |
+
|
| 144 |
+
def test_runtime_matches_dense_transformer(self):
|
| 145 |
+
"""The sparse runtime, run on the dense model's own tables, reproduces PyTorch."""
|
| 146 |
+
import tempfile
|
| 147 |
+
|
| 148 |
+
s = build_weights.load_settings("hf")
|
| 149 |
+
vocab, ngram, _ = build_weights.compile_tables(s, self.sources)
|
| 150 |
+
oov = self.suggest.oov_prefix_rates(s, self.sources, vocab)
|
| 151 |
+
texts = ["Once upon a", "write a python function to", "zebra", "how do i", "thank you for your"]
|
| 152 |
+
tok = AutoTokenizer.from_pretrained(MODEL_DIR)
|
| 153 |
+
model = AutoModelForCausalLM.from_pretrained(MODEL_DIR).eval()
|
| 154 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 155 |
+
path = Path(tmp) / "model.bin"
|
| 156 |
+
self.export_web.export(vocab, ngram, oov, self.policy, path)
|
| 157 |
+
results = self.cli("--model", str(path), "--dist", *texts)
|
| 158 |
+
with torch.no_grad():
|
| 159 |
+
for t, res in zip(texts, results):
|
| 160 |
+
with self.subTest(text=t):
|
| 161 |
+
lp = model(**tok(t, return_tensors="pt")).logits[0, -1].double().log_softmax(-1)
|
| 162 |
+
self.assertEqual([i for i, _ in res["top"]], lp.topk(5).indices.tolist())
|
| 163 |
+
for i, v in res["top"]:
|
| 164 |
+
self.assertAlmostEqual(v, lp[i].item(), places=3)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
if __name__ == "__main__":
|
| 168 |
unittest.main()
|
tokenizer.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Word-level tokenizer with GPT-2 byte-level pre-tokenization, matching tokenizer.json
|
| 2 |
+
// (NFKC + lowercase, add_prefix_space, GPT-2 split regex). No Node or DOM APIs, so it
|
| 3 |
+
// is shared by model.ts (Node/Bun) and web/autocomplete.ts (browser).
|
| 4 |
+
|
| 5 |
+
function bytesToUnicode(): Map<number, string> {
|
| 6 |
+
const bs: number[] = [];
|
| 7 |
+
for (let b = 33; b <= 126; b++) bs.push(b);
|
| 8 |
+
for (let b = 161; b <= 172; b++) bs.push(b);
|
| 9 |
+
for (let b = 174; b <= 255; b++) bs.push(b);
|
| 10 |
+
const cs = [...bs];
|
| 11 |
+
let n = 0;
|
| 12 |
+
for (let b = 0; b < 256; b++) {
|
| 13 |
+
if (!bs.includes(b)) {
|
| 14 |
+
bs.push(b);
|
| 15 |
+
cs.push(256 + n++);
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
return new Map(bs.map((b, i) => [b, String.fromCodePoint(cs[i])]));
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const BYTE_ENC = bytesToUnicode();
|
| 22 |
+
const BYTE_DEC = new Map([...BYTE_ENC].map(([b, c]) => [c, b]));
|
| 23 |
+
const GPT2_SPLIT = /'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu;
|
| 24 |
+
const encoder = new TextEncoder();
|
| 25 |
+
const decoder = new TextDecoder();
|
| 26 |
+
|
| 27 |
+
/** Byte-level pre-tokens ("Ġword" style), exactly as the Python tokenizer splits text. */
|
| 28 |
+
export function preTokenize(text: string): string[] {
|
| 29 |
+
let norm = text.normalize("NFKC").toLowerCase();
|
| 30 |
+
if (norm && !/^\s/.test(norm)) norm = " " + norm; // ByteLevel add_prefix_space
|
| 31 |
+
return (norm.match(GPT2_SPLIT) ?? []).map((piece) => [...encoder.encode(piece)].map((b) => BYTE_ENC.get(b)!).join(""));
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/** Byte-level piece(s) back to text. */
|
| 35 |
+
export function decodePieces(pieces: string): string {
|
| 36 |
+
return decoder.decode(new Uint8Array([...pieces].map((c) => BYTE_DEC.get(c) ?? 63)));
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export class WordTokenizer {
|
| 40 |
+
vocab: Map<string, number>;
|
| 41 |
+
inverse: string[];
|
| 42 |
+
special: Set<number>;
|
| 43 |
+
unkId: number;
|
| 44 |
+
bosId: number;
|
| 45 |
+
|
| 46 |
+
constructor(pieces: string[], unkId: number, bosId: number, special: Iterable<number>) {
|
| 47 |
+
this.unkId = unkId;
|
| 48 |
+
this.bosId = bosId;
|
| 49 |
+
this.inverse = pieces;
|
| 50 |
+
this.vocab = new Map(pieces.map((p, i) => [p, i]));
|
| 51 |
+
this.special = new Set(special);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/** tokenizer.json (WordLevel model, BOS template post-processor) -> tokenizer. */
|
| 55 |
+
static fromJson(json: any): WordTokenizer {
|
| 56 |
+
const pieces: string[] = [];
|
| 57 |
+
for (const [tok, id] of Object.entries<number>(json.model.vocab)) pieces[id] = tok;
|
| 58 |
+
const bos = json.post_processor?.single?.find((p: any) => p.SpecialToken)?.SpecialToken?.id;
|
| 59 |
+
const special = (json.added_tokens ?? []).filter((t: any) => t.special).map((t: any) => t.id);
|
| 60 |
+
return new WordTokenizer(pieces, json.model.vocab[json.model.unk_token], json.model.vocab[bos], special);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
idOf(piece: string): number {
|
| 64 |
+
return this.vocab.get(piece) ?? this.unkId;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
encode(text: string): number[] {
|
| 68 |
+
return [this.bosId, ...preTokenize(text).map((p) => this.idOf(p))];
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
decode(ids: number[]): string {
|
| 72 |
+
return decodePieces(ids.filter((id) => !this.special.has(id)).map((id) => this.inverse[id]).join(""));
|
| 73 |
+
}
|
| 74 |
+
}
|
web/autocomplete.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Browser runtime for the compiled autocomplete model (zero dependencies, no DOM;
|
| 2 |
+
// works on the main thread, in a Web Worker, or in Bun/Node).
|
| 3 |
+
//
|
| 4 |
+
// web/model.bin holds the compiled transformer in sparse form (see export_web.py):
|
| 5 |
+
// the always-on unigram neuron plus, for every MLP neuron, its context tokens and
|
| 6 |
+
// the logit changes it writes. Evaluating only the neurons whose context matches
|
| 7 |
+
// gives the same next-token distribution as the dense model, without the zeros.
|
| 8 |
+
// The confidence policy mirrors suggest.py.
|
| 9 |
+
//
|
| 10 |
+
// const ac = await CompiledAutocomplete.load("/model.bin");
|
| 11 |
+
// ac.suggest("write a python fun").ghost // "ction to"
|
| 12 |
+
|
| 13 |
+
import { decodePieces, preTokenize } from "../tokenizer.ts";
|
| 14 |
+
|
| 15 |
+
export type Policy = { tau: number; min_support: number; max_words: number };
|
| 16 |
+
export type Step = { text: string; prob: number; support: number };
|
| 17 |
+
export type Suggestion = { ghost: string; prob: number; steps: Step[] };
|
| 18 |
+
|
| 19 |
+
type IdArray = Uint16Array | Uint32Array;
|
| 20 |
+
type Order = {
|
| 21 |
+
k: number;
|
| 22 |
+
ctx: IdArray;
|
| 23 |
+
support: Float32Array;
|
| 24 |
+
offsets: Uint32Array;
|
| 25 |
+
dids: IdArray;
|
| 26 |
+
dvals: Float32Array;
|
| 27 |
+
index: Map<string, number>;
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
const MAGIC = "TACB";
|
| 31 |
+
|
| 32 |
+
export class CompiledAutocomplete {
|
| 33 |
+
readonly vocab: string[];
|
| 34 |
+
readonly nPrev: number;
|
| 35 |
+
readonly order: number;
|
| 36 |
+
readonly eosId: number;
|
| 37 |
+
readonly unkId: number;
|
| 38 |
+
readonly policy: Policy;
|
| 39 |
+
readonly bytes: number;
|
| 40 |
+
readonly contexts: number;
|
| 41 |
+
private text: string[];
|
| 42 |
+
private pieceIndex: Map<string, number>;
|
| 43 |
+
private base: Float32Array;
|
| 44 |
+
private orders: Order[];
|
| 45 |
+
private oov: Record<string, number>;
|
| 46 |
+
private stopAfter: Set<string>;
|
| 47 |
+
private candCache = new Map<string, Int32Array>();
|
| 48 |
+
private distCache = new Map<string, { probs: Float64Array; support: number }>();
|
| 49 |
+
|
| 50 |
+
/** Fetch (with optional progress callback) or wrap an already-loaded buffer. */
|
| 51 |
+
static async load(source: string | ArrayBuffer, onProgress?: (fraction: number) => void): Promise<CompiledAutocomplete> {
|
| 52 |
+
if (source instanceof ArrayBuffer) return new CompiledAutocomplete(source);
|
| 53 |
+
const resp = await fetch(source);
|
| 54 |
+
if (!resp.ok || !resp.body) throw new Error(`failed to fetch ${source}: ${resp.status}`);
|
| 55 |
+
const total = Number(resp.headers.get("content-length")) || 0;
|
| 56 |
+
const reader = resp.body.getReader();
|
| 57 |
+
const chunks: Uint8Array[] = [];
|
| 58 |
+
let received = 0;
|
| 59 |
+
for (;;) {
|
| 60 |
+
const { done, value } = await reader.read();
|
| 61 |
+
if (done) break;
|
| 62 |
+
chunks.push(value);
|
| 63 |
+
received += value.length;
|
| 64 |
+
if (total) onProgress?.(received / total);
|
| 65 |
+
}
|
| 66 |
+
const bytes = new Uint8Array(received);
|
| 67 |
+
let off = 0;
|
| 68 |
+
for (const c of chunks) {
|
| 69 |
+
bytes.set(c, off);
|
| 70 |
+
off += c.length;
|
| 71 |
+
}
|
| 72 |
+
onProgress?.(1);
|
| 73 |
+
return new CompiledAutocomplete(bytes.buffer);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
constructor(buf: ArrayBuffer) {
|
| 77 |
+
const view = new DataView(buf);
|
| 78 |
+
const magic = new TextDecoder().decode(new Uint8Array(buf, 0, 4));
|
| 79 |
+
if (magic !== MAGIC) throw new Error("not a tiny-autocomplete model.bin");
|
| 80 |
+
const headerLen = view.getUint32(8, true);
|
| 81 |
+
const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 12, headerLen)));
|
| 82 |
+
const dataStart = 12 + headerLen + ((8 - ((12 + headerLen) % 8)) % 8);
|
| 83 |
+
const sections = new Map<string, { offset: number; bytes: number }>();
|
| 84 |
+
for (const s of header.sections) sections.set(s.name, s);
|
| 85 |
+
const Ids = header.id_bytes === 2 ? Uint16Array : Uint32Array;
|
| 86 |
+
const f32 = (name: string) => {
|
| 87 |
+
const s = sections.get(name)!;
|
| 88 |
+
return new Float32Array(buf, dataStart + s.offset, s.bytes / 4);
|
| 89 |
+
};
|
| 90 |
+
const u32 = (name: string) => {
|
| 91 |
+
const s = sections.get(name)!;
|
| 92 |
+
return new Uint32Array(buf, dataStart + s.offset, s.bytes / 4);
|
| 93 |
+
};
|
| 94 |
+
const ids = (name: string) => {
|
| 95 |
+
const s = sections.get(name)!;
|
| 96 |
+
return new Ids(buf, dataStart + s.offset, s.bytes / header.id_bytes);
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
this.vocab = header.vocab;
|
| 100 |
+
this.text = this.vocab.map((p) => decodePieces(p));
|
| 101 |
+
this.pieceIndex = new Map(this.vocab.map((p, i) => [p, i]));
|
| 102 |
+
this.nPrev = header.n_prev;
|
| 103 |
+
this.order = this.nPrev + 1;
|
| 104 |
+
this.eosId = header.eos_id;
|
| 105 |
+
this.unkId = header.unk_id;
|
| 106 |
+
this.oov = header.oov_prefix_rates;
|
| 107 |
+
this.policy = header.policy;
|
| 108 |
+
this.stopAfter = new Set(header.stop_after);
|
| 109 |
+
this.bytes = buf.byteLength;
|
| 110 |
+
this.base = f32("base");
|
| 111 |
+
this.orders = header.orders.map(({ k, contexts }: { k: number; contexts: number }) => {
|
| 112 |
+
const ctx = ids(`ctx${k}`);
|
| 113 |
+
const index = new Map<string, number>();
|
| 114 |
+
for (let i = 0; i < contexts; i++) index.set(ctx.subarray(i * k, (i + 1) * k).join(","), i);
|
| 115 |
+
return { k, ctx, support: f32(`support${k}`), offsets: u32(`offsets${k}`), dids: ids(`dids${k}`), dvals: f32(`dvals${k}`), index };
|
| 116 |
+
});
|
| 117 |
+
this.contexts = this.orders.reduce((n, o) => n + o.index.size, 0);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/** Last `order` token ids, oldest first; positions before 0 read BOS like the attention heads. */
|
| 121 |
+
private window(ids: number[]): number[] {
|
| 122 |
+
const t = ids.length - 1;
|
| 123 |
+
const w: number[] = [];
|
| 124 |
+
for (let a = this.nPrev; a >= 0; a--) w.push(ids[Math.max(t - a, 0)]);
|
| 125 |
+
return w;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
/** Next-token distribution and support of the longest matched context. */
|
| 129 |
+
dist(ids: number[]): { probs: Float64Array; support: number } {
|
| 130 |
+
const window = this.window(ids);
|
| 131 |
+
const key = window.join(",");
|
| 132 |
+
const hit = this.distCache.get(key);
|
| 133 |
+
if (hit) return hit;
|
| 134 |
+
const logits = Float64Array.from(this.base);
|
| 135 |
+
let support = 0;
|
| 136 |
+
for (const o of this.orders) {
|
| 137 |
+
const i = o.index.get(window.slice(-o.k).join(","));
|
| 138 |
+
if (i === undefined) continue;
|
| 139 |
+
for (let j = o.offsets[i]; j < o.offsets[i + 1]; j++) logits[o.dids[j]] += o.dvals[j];
|
| 140 |
+
support = o.support[i];
|
| 141 |
+
}
|
| 142 |
+
let max = -Infinity;
|
| 143 |
+
for (const v of logits) if (v > max) max = v;
|
| 144 |
+
let z = 0;
|
| 145 |
+
const probs = new Float64Array(logits.length);
|
| 146 |
+
for (let i = 0; i < logits.length; i++) z += probs[i] = Math.exp(logits[i] - max);
|
| 147 |
+
for (let i = 0; i < probs.length; i++) probs[i] /= z;
|
| 148 |
+
if (this.distCache.size > 2000) this.distCache.clear();
|
| 149 |
+
const out = { probs, support };
|
| 150 |
+
this.distCache.set(key, out);
|
| 151 |
+
return out;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
private candidates(partial: string): Int32Array {
|
| 155 |
+
let c = this.candCache.get(partial);
|
| 156 |
+
if (!c) {
|
| 157 |
+
const out: number[] = [];
|
| 158 |
+
for (let i = this.unkId + 1; i < this.vocab.length; i++) if (this.vocab[i].startsWith(partial)) out.push(i);
|
| 159 |
+
c = Int32Array.from(out);
|
| 160 |
+
if (this.candCache.size > 5000) this.candCache.clear();
|
| 161 |
+
this.candCache.set(partial, c);
|
| 162 |
+
}
|
| 163 |
+
return c;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/** Greedy suggestion chain with cumulative probabilities, before thresholding. */
|
| 167 |
+
steps(text: string, maxWords = this.policy.max_words): Step[] {
|
| 168 |
+
const pcs = preTokenize(text);
|
| 169 |
+
if (!pcs.length) return [];
|
| 170 |
+
const partial = pcs[pcs.length - 1];
|
| 171 |
+
const cands = this.candidates(partial);
|
| 172 |
+
if (!cands.length) return [];
|
| 173 |
+
const ids = [this.eosId, ...pcs.slice(0, -1).map((p) => this.pieceIndex.get(p) ?? this.unkId)];
|
| 174 |
+
let { probs, support } = this.dist(ids);
|
| 175 |
+
let best = cands[0];
|
| 176 |
+
let sum = 0;
|
| 177 |
+
for (const c of cands) {
|
| 178 |
+
sum += probs[c];
|
| 179 |
+
if (probs[c] > probs[best]) best = c;
|
| 180 |
+
}
|
| 181 |
+
let prob = (1 - (this.oov[partial] ?? 0)) * (probs[best] / sum);
|
| 182 |
+
const out: Step[] = [{ text: this.text[best].slice(decodePieces(partial).length), prob, support }];
|
| 183 |
+
ids.push(best);
|
| 184 |
+
while (!this.stopAfter.has(this.vocab[ids[ids.length - 1]]) && out.length < maxWords) {
|
| 185 |
+
({ probs, support } = this.dist(ids));
|
| 186 |
+
let nxt = 0;
|
| 187 |
+
for (let i = 1; i < probs.length; i++) if (probs[i] > probs[nxt]) nxt = i;
|
| 188 |
+
if (nxt === this.eosId) break;
|
| 189 |
+
prob *= probs[nxt];
|
| 190 |
+
out.push({ text: this.text[nxt], prob, support });
|
| 191 |
+
ids.push(nxt);
|
| 192 |
+
}
|
| 193 |
+
return out;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
static apply(steps: Step[], policy: Policy): { ghost: string; prob: number } {
|
| 197 |
+
let ghost = "";
|
| 198 |
+
let prob = 0;
|
| 199 |
+
for (const st of steps.slice(0, policy.max_words)) {
|
| 200 |
+
if (st.prob < policy.tau || st.support < policy.min_support) break;
|
| 201 |
+
ghost += st.text;
|
| 202 |
+
prob = st.prob;
|
| 203 |
+
}
|
| 204 |
+
return { ghost, prob };
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/** Ghost text to show after `text`, or "" when the model isn't confident. */
|
| 208 |
+
suggest(text: string, policy: Policy = this.policy): Suggestion {
|
| 209 |
+
const steps = this.steps(text, policy.max_words);
|
| 210 |
+
return { ...CompiledAutocomplete.apply(steps, policy), steps };
|
| 211 |
+
}
|
| 212 |
+
}
|
web/cli.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Run the browser runtime from the command line (Bun or Node >= 23.6):
|
| 2 |
+
// bun web/cli.ts "write a python fun" "how do i "
|
| 3 |
+
// bun web/cli.ts --json ... full suggestion chains (used by test_model.py)
|
| 4 |
+
// bun web/cli.ts --dist ... top-5 next-token log-probs after each text
|
| 5 |
+
// bun web/cli.ts --model path/to/model.bin use another export (default web/model.bin)
|
| 6 |
+
|
| 7 |
+
import { readFileSync } from "node:fs";
|
| 8 |
+
import { join } from "node:path";
|
| 9 |
+
import { preTokenize } from "../tokenizer.ts";
|
| 10 |
+
import { CompiledAutocomplete } from "./autocomplete.ts";
|
| 11 |
+
|
| 12 |
+
const dir = import.meta.dirname ?? new URL(".", import.meta.url).pathname;
|
| 13 |
+
let args = process.argv.slice(2);
|
| 14 |
+
let modelPath = join(dir, "model.bin");
|
| 15 |
+
const mi = args.indexOf("--model");
|
| 16 |
+
if (mi >= 0) {
|
| 17 |
+
modelPath = args[mi + 1];
|
| 18 |
+
args = [...args.slice(0, mi), ...args.slice(mi + 2)];
|
| 19 |
+
}
|
| 20 |
+
const mode = args[0] === "--json" || args[0] === "--dist" ? args.shift() : "";
|
| 21 |
+
|
| 22 |
+
const buf = readFileSync(modelPath);
|
| 23 |
+
const ac = new CompiledAutocomplete(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
|
| 24 |
+
|
| 25 |
+
if (mode === "--dist") {
|
| 26 |
+
const out = args.map((t) => {
|
| 27 |
+
const ids = [ac.eosId, ...preTokenize(t).map((p) => ac.vocab.indexOf(p)).map((i) => (i < 0 ? ac.unkId : i))];
|
| 28 |
+
const { probs } = ac.dist(ids);
|
| 29 |
+
const top = [...probs.keys()].sort((a, b) => probs[b] - probs[a]).slice(0, 5);
|
| 30 |
+
return { text: t, top: top.map((i) => [i, Math.log(probs[i])]) };
|
| 31 |
+
});
|
| 32 |
+
console.log(JSON.stringify(out));
|
| 33 |
+
} else {
|
| 34 |
+
const results = args.map((t) => {
|
| 35 |
+
const start = performance.now();
|
| 36 |
+
const s = ac.suggest(t);
|
| 37 |
+
return { text: t, ...s, ms: performance.now() - start };
|
| 38 |
+
});
|
| 39 |
+
if (mode === "--json") console.log(JSON.stringify(results));
|
| 40 |
+
else for (const r of results) console.log(`${r.text}\x1b[90m${r.ghost}\x1b[0m ${r.ghost ? Math.round(r.prob * 100) + "%" : "(no confident suggestion)"} ${r.ms.toFixed(2)} ms`);
|
| 41 |
+
}
|
web/demo.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
function T(){let H=[];for(let q=33;q<=126;q++)H.push(q);for(let q=161;q<=172;q++)H.push(q);for(let q=174;q<=255;q++)H.push(q);let J=[...H],D=0;for(let q=0;q<256;q++)if(!H.includes(q))H.push(q),J.push(256+D++);return new Map(H.map((q,K)=>[q,String.fromCodePoint(J[K])]))}var $=T(),w=new Map([...$].map(([H,J])=>[J,H])),C=/'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu,j=new TextEncoder,y=new TextDecoder;function v(H){let J=H.normalize("NFKC").toLowerCase();if(J&&!/^\s/.test(J))J=" "+J;return(J.match(C)??[]).map((D)=>[...j.encode(D)].map((q)=>$.get(q)).join(""))}function W(H){return y.decode(new Uint8Array([...H].map((J)=>w.get(J)??63)))}var g="TACB";class Y{vocab;nPrev;order;eosId;unkId;policy;bytes;contexts;text;pieceIndex;base;orders;oov;stopAfter;candCache=new Map;distCache=new Map;static async load(H,J){if(H instanceof ArrayBuffer)return new Y(H);let D=await fetch(H);if(!D.ok||!D.body)throw Error(`failed to fetch ${H}: ${D.status}`);let q=Number(D.headers.get("content-length"))||0,K=D.body.getReader(),O=[],R=0;for(;;){let{done:X,value:Z}=await K.read();if(X)break;if(O.push(Z),R+=Z.length,q)J?.(R/q)}let U=new Uint8Array(R),F=0;for(let X of O)U.set(X,F),F+=X.length;return J?.(1),new Y(U.buffer)}constructor(H){let J=new DataView(H);if(new TextDecoder().decode(new Uint8Array(H,0,4))!==g)throw Error("not a tiny-autocomplete model.bin");let q=J.getUint32(8,!0),K=JSON.parse(new TextDecoder().decode(new Uint8Array(H,12,q))),O=12+q+(8-(12+q)%8)%8,R=new Map;for(let V of K.sections)R.set(V.name,V);let U=K.id_bytes===2?Uint16Array:Uint32Array,F=(V)=>{let Q=R.get(V);return new Float32Array(H,O+Q.offset,Q.bytes/4)},X=(V)=>{let Q=R.get(V);return new Uint32Array(H,O+Q.offset,Q.bytes/4)},Z=(V)=>{let Q=R.get(V);return new U(H,O+Q.offset,Q.bytes/K.id_bytes)};this.vocab=K.vocab,this.text=this.vocab.map((V)=>W(V)),this.pieceIndex=new Map(this.vocab.map((V,Q)=>[V,Q])),this.nPrev=K.n_prev,this.order=this.nPrev+1,this.eosId=K.eos_id,this.unkId=K.unk_id,this.oov=K.oov_prefix_rates,this.policy=K.policy,this.stopAfter=new Set(K.stop_after),this.bytes=H.byteLength,this.base=F("base"),this.orders=K.orders.map(({k:V,contexts:Q})=>{let M=Z(`ctx${V}`),L=new Map;for(let B=0;B<Q;B++)L.set(M.subarray(B*V,(B+1)*V).join(","),B);return{k:V,ctx:M,support:F(`support${V}`),offsets:X(`offsets${V}`),dids:Z(`dids${V}`),dvals:F(`dvals${V}`),index:L}}),this.contexts=this.orders.reduce((V,Q)=>V+Q.index.size,0)}window(H){let J=H.length-1,D=[];for(let q=this.nPrev;q>=0;q--)D.push(H[Math.max(J-q,0)]);return D}dist(H){let J=this.window(H),D=J.join(","),q=this.distCache.get(D);if(q)return q;let K=Float64Array.from(this.base),O=0;for(let Z of this.orders){let V=Z.index.get(J.slice(-Z.k).join(","));if(V===void 0)continue;for(let Q=Z.offsets[V];Q<Z.offsets[V+1];Q++)K[Z.dids[Q]]+=Z.dvals[Q];O=Z.support[V]}let R=-1/0;for(let Z of K)if(Z>R)R=Z;let U=0,F=new Float64Array(K.length);for(let Z=0;Z<K.length;Z++)U+=F[Z]=Math.exp(K[Z]-R);for(let Z=0;Z<F.length;Z++)F[Z]/=U;if(this.distCache.size>2000)this.distCache.clear();let X={probs:F,support:O};return this.distCache.set(D,X),X}candidates(H){let J=this.candCache.get(H);if(!J){let D=[];for(let q=this.unkId+1;q<this.vocab.length;q++)if(this.vocab[q].startsWith(H))D.push(q);if(J=Int32Array.from(D),this.candCache.size>5000)this.candCache.clear();this.candCache.set(H,J)}return J}steps(H,J=this.policy.max_words){let D=v(H);if(!D.length)return[];let q=D[D.length-1],K=this.candidates(q);if(!K.length)return[];let O=[this.eosId,...D.slice(0,-1).map((Q)=>this.pieceIndex.get(Q)??this.unkId)],{probs:R,support:U}=this.dist(O),F=K[0],X=0;for(let Q of K)if(X+=R[Q],R[Q]>R[F])F=Q;let Z=(1-(this.oov[q]??0))*(R[F]/X),V=[{text:this.text[F].slice(W(q).length),prob:Z,support:U}];O.push(F);while(!this.stopAfter.has(this.vocab[O[O.length-1]])&&V.length<J){({probs:R,support:U}=this.dist(O));let Q=0;for(let M=1;M<R.length;M++)if(R[M]>R[Q])Q=M;if(Q===this.eosId)break;Z*=R[Q],V.push({text:this.text[Q],prob:Z,support:U}),O.push(Q)}return V}static apply(H,J){let D="",q=0;for(let K of H.slice(0,J.max_words)){if(K.prob<J.tau||K.support<J.min_support)break;D+=K.text,q=K.prob}return{ghost:D,prob:q}}suggest(H,J=this.policy){let D=this.steps(H,J.max_words);return{...Y.apply(D,J),steps:D}}}var G=(H)=>document.getElementById(H),N=G("prompt"),z=G("ghost"),f=G("status"),_=G("guess"),S=G("tau"),A=G("tau-label"),I=G("load"),k=["write a python fun","how do i ","Write a function to find the ","how to convert a ","can you explain ","I hope this email ","Once upon a ","what is the difference between "];function P(H){return H.replace(/[&<>]/g,(J)=>({"&":"&","<":"<",">":">"})[J])}async function E(){let H=performance.now(),J=await Y.load("model.bin",(R)=>I.textContent=`downloading model... ${Math.round(R*100)}%`),D=performance.now()-H,q={...J.policy};S.value=String(q.tau),A.textContent=`${Math.round(q.tau*100)}%`,I.textContent=`${(J.bytes/1e6).toFixed(1)} MB · ${J.contexts.toLocaleString()} compiled contexts · `+`${J.order}-token context · ${J.vocab.length.toLocaleString()} words · loaded in ${Math.round(D)} ms`,N.disabled=!1,N.focus();let K="",O=()=>{let R=N.value,U=performance.now(),F=J.suggest(R,q),X=performance.now()-U;K=F.ghost,z.innerHTML=P(R)+"<span>"+P(K)+"</span>",N.style.height="auto",N.style.height=Math.max(N.scrollHeight,z.scrollHeight)+"px",f.textContent=R?`${X.toFixed(2)} ms`:"";let Z=F.steps.findIndex((Q)=>Q.text),V=Z<0?"":F.steps.slice(0,Z+1).map((Q)=>Q.text).join("");if(!R)_.textContent="";else if(K)_.textContent=`suggesting with ${Math.round(F.prob*100)}% confidence`;else if(V)_.textContent=`staying quiet: best guess "${V}" is only ${Math.round(F.steps[Z].prob*100)}% likely`;else _.textContent="staying quiet: nothing known for this prefix"};N.addEventListener("input",O),N.addEventListener("keydown",(R)=>{if(R.key==="Tab"&&K)R.preventDefault(),N.value+=K,O()}),S.addEventListener("input",()=>{q.tau=Number(S.value),A.textContent=`${Math.round(q.tau*100)}%`,O()});for(let R of k){let U=document.createElement("button");U.className="ex",U.textContent=R.trim(),U.onclick=()=>{N.value=R,O(),N.focus()},G("examples").appendChild(U)}O()}E().catch((H)=>I.textContent=String(H));
|
web/demo.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Static demo for web/autocomplete.ts. Bundle with:
|
| 2 |
+
// bun build web/demo.ts --outfile web/demo.js --target browser --minify
|
| 3 |
+
|
| 4 |
+
import { CompiledAutocomplete, type Policy } from "./autocomplete.ts";
|
| 5 |
+
|
| 6 |
+
const $ = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
|
| 7 |
+
const promptEl = $<HTMLTextAreaElement>("prompt");
|
| 8 |
+
const ghostEl = $("ghost");
|
| 9 |
+
const statusEl = $("status");
|
| 10 |
+
const guessEl = $("guess");
|
| 11 |
+
const tauEl = $<HTMLInputElement>("tau");
|
| 12 |
+
const tauLabel = $("tau-label");
|
| 13 |
+
const loadEl = $("load");
|
| 14 |
+
|
| 15 |
+
const EXAMPLES = [
|
| 16 |
+
"write a python fun", "how do i ", "Write a function to find the ", "how to convert a ",
|
| 17 |
+
"can you explain ", "I hope this email ", "Once upon a ", "what is the difference between ",
|
| 18 |
+
];
|
| 19 |
+
|
| 20 |
+
function esc(s: string): string {
|
| 21 |
+
return s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]!);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
async function main(): Promise<void> {
|
| 25 |
+
const t0 = performance.now();
|
| 26 |
+
const ac = await CompiledAutocomplete.load("model.bin", (f) => (loadEl.textContent = `downloading model... ${Math.round(f * 100)}%`));
|
| 27 |
+
const loadMs = performance.now() - t0;
|
| 28 |
+
const policy: Policy = { ...ac.policy };
|
| 29 |
+
tauEl.value = String(policy.tau);
|
| 30 |
+
tauLabel.textContent = `${Math.round(policy.tau * 100)}%`;
|
| 31 |
+
loadEl.textContent =
|
| 32 |
+
`${(ac.bytes / 1e6).toFixed(1)} MB · ${ac.contexts.toLocaleString()} compiled contexts · ` +
|
| 33 |
+
`${ac.order}-token context · ${ac.vocab.length.toLocaleString()} words · loaded in ${Math.round(loadMs)} ms`;
|
| 34 |
+
promptEl.disabled = false;
|
| 35 |
+
promptEl.focus();
|
| 36 |
+
|
| 37 |
+
let ghost = "";
|
| 38 |
+
const render = () => {
|
| 39 |
+
const text = promptEl.value;
|
| 40 |
+
const start = performance.now();
|
| 41 |
+
const s = ac.suggest(text, policy);
|
| 42 |
+
const ms = performance.now() - start;
|
| 43 |
+
ghost = s.ghost;
|
| 44 |
+
ghostEl.innerHTML = esc(text) + "<span>" + esc(ghost) + "</span>";
|
| 45 |
+
promptEl.style.height = "auto";
|
| 46 |
+
promptEl.style.height = Math.max(promptEl.scrollHeight, ghostEl.scrollHeight) + "px";
|
| 47 |
+
statusEl.textContent = text ? `${ms.toFixed(2)} ms` : "";
|
| 48 |
+
// First step that adds text (the first step is empty when the typed word is already complete).
|
| 49 |
+
const i = s.steps.findIndex((st) => st.text);
|
| 50 |
+
const guess = i < 0 ? "" : s.steps.slice(0, i + 1).map((st) => st.text).join("");
|
| 51 |
+
if (!text) guessEl.textContent = "";
|
| 52 |
+
else if (ghost) guessEl.textContent = `suggesting with ${Math.round(s.prob * 100)}% confidence`;
|
| 53 |
+
else if (guess) guessEl.textContent = `staying quiet: best guess "${guess}" is only ${Math.round(s.steps[i].prob * 100)}% likely`;
|
| 54 |
+
else guessEl.textContent = "staying quiet: nothing known for this prefix";
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
promptEl.addEventListener("input", render);
|
| 58 |
+
promptEl.addEventListener("keydown", (e) => {
|
| 59 |
+
if (e.key === "Tab" && ghost) {
|
| 60 |
+
e.preventDefault();
|
| 61 |
+
promptEl.value += ghost;
|
| 62 |
+
render();
|
| 63 |
+
}
|
| 64 |
+
});
|
| 65 |
+
tauEl.addEventListener("input", () => {
|
| 66 |
+
policy.tau = Number(tauEl.value);
|
| 67 |
+
tauLabel.textContent = `${Math.round(policy.tau * 100)}%`;
|
| 68 |
+
render();
|
| 69 |
+
});
|
| 70 |
+
for (const ex of EXAMPLES) {
|
| 71 |
+
const b = document.createElement("button");
|
| 72 |
+
b.className = "ex";
|
| 73 |
+
b.textContent = ex.trim();
|
| 74 |
+
b.onclick = () => {
|
| 75 |
+
promptEl.value = ex;
|
| 76 |
+
render();
|
| 77 |
+
promptEl.focus();
|
| 78 |
+
};
|
| 79 |
+
$("examples").appendChild(b);
|
| 80 |
+
}
|
| 81 |
+
render();
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
main().catch((e) => (loadEl.textContent = String(e)));
|
web/index.html
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>tiny-autocomplete · in-browser</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root { --bg: #fafaf9; --fg: #1c1917; --muted: #78716c; --line: #e7e5e4; --accent: #2563eb; --ghost: #a8a29e; --card: #fff; }
|
| 9 |
+
@media (prefers-color-scheme: dark) {
|
| 10 |
+
:root { --bg: #0c0a09; --fg: #f5f5f4; --muted: #a8a29e; --line: #292524; --accent: #60a5fa; --ghost: #57534e; --card: #1c1917; }
|
| 11 |
+
}
|
| 12 |
+
* { box-sizing: border-box; }
|
| 13 |
+
body { margin: 0; font: 16px/1.5 system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--fg); }
|
| 14 |
+
main { max-width: 720px; margin: 0 auto; padding: 48px 20px; }
|
| 15 |
+
h1 { font-size: 24px; margin: 0 0 4px; }
|
| 16 |
+
.sub { color: var(--muted); margin: 0 0 24px; font-size: 14px; }
|
| 17 |
+
.editor { position: relative; background: var(--card); border: 1px solid var(--line); border-radius: 12px; }
|
| 18 |
+
.editor:focus-within { border-color: var(--accent); }
|
| 19 |
+
.layer { font: 20px/1.5 system-ui, -apple-system, sans-serif; padding: 16px 18px; margin: 0; white-space: pre-wrap; word-wrap: break-word; }
|
| 20 |
+
#ghost { position: absolute; inset: 0; pointer-events: none; color: transparent; }
|
| 21 |
+
#ghost span { color: var(--ghost); }
|
| 22 |
+
#prompt { position: relative; width: 100%; min-height: 64px; border: 0; outline: 0; resize: none; background: transparent; color: var(--fg); overflow: hidden; }
|
| 23 |
+
.row { display: flex; justify-content: space-between; gap: 16px; color: var(--muted); font-size: 13px; margin: 8px 2px; }
|
| 24 |
+
#guess { min-height: 20px; }
|
| 25 |
+
kbd { border: 1px solid var(--line); border-radius: 4px; padding: 0 5px; font-size: 12px; background: var(--card); }
|
| 26 |
+
label { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 14px; margin: 20px 2px 28px; }
|
| 27 |
+
input[type=range] { flex: 1; accent-color: var(--accent); }
|
| 28 |
+
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 0 0 10px; font-weight: 600; }
|
| 29 |
+
.examples { display: flex; flex-wrap: wrap; gap: 8px; }
|
| 30 |
+
.ex { border: 1px solid var(--line); background: var(--card); color: inherit; border-radius: 999px; padding: 4px 12px; font: inherit; font-size: 14px; cursor: pointer; }
|
| 31 |
+
.ex:hover { border-color: var(--accent); }
|
| 32 |
+
</style>
|
| 33 |
+
</head>
|
| 34 |
+
<body>
|
| 35 |
+
<main>
|
| 36 |
+
<h1>tiny-autocomplete</h1>
|
| 37 |
+
<p class="sub" id="load">loading...</p>
|
| 38 |
+
|
| 39 |
+
<div class="editor">
|
| 40 |
+
<pre class="layer" id="ghost" aria-hidden="true"></pre>
|
| 41 |
+
<textarea class="layer" id="prompt" rows="1" placeholder="Type a coding prompt, e.g. write a python fun" spellcheck="false" disabled></textarea>
|
| 42 |
+
</div>
|
| 43 |
+
<div class="row"><span><kbd>Tab</kbd> accept suggestion</span><span id="status"></span></div>
|
| 44 |
+
<div class="row"><span id="guess"></span></div>
|
| 45 |
+
|
| 46 |
+
<label>Confidence threshold <input type="range" id="tau" min="0.3" max="0.99" step="0.01"> <span id="tau-label"></span></label>
|
| 47 |
+
|
| 48 |
+
<h2>Try</h2>
|
| 49 |
+
<div class="examples" id="examples"></div>
|
| 50 |
+
</main>
|
| 51 |
+
<script type="module" src="demo.js"></script>
|
| 52 |
+
</body>
|
| 53 |
+
</html>
|
web/model.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d770bf4ec6644ccab5646e2b1244b58b185378b371c52b1c263b5a0ebf3f8499
|
| 3 |
+
size 4494512
|