Serving, X-raying and training from the browser — and the two measurements that decided how the server was written.
A local page, no framework, no build and no CDN — it works offline. Five tabs over the same FastAPI API:
| Tab | Used for |
|---|---|
| Playground | generating with gpt2-xl or with our models, with streaming and decoding controls |
| X-Ray | tokenization, next-token distribution, logit lens, attention map |
| Detector | the two RoBERTa models side by side |
| Models | one card per saved training run, with the curve and the geometry measurements |
| Laboratory | running any script in the project, with a live log |
The Playground selector has two groups: OpenAI (only gpt2-xl) and Ours (all checkpoints). The other four HuggingFace models remain downloaded and keep serving the labs — they just left the selector, so it does not become a list of things nobody picks.
Before optimizing anything, we measured. gpt2, 23-token prompt, 32 tokens generated, 8 threads:
| Stage | Time | % of total |
|---|---|---|
| tokenize | 0.1 ms | 0.0% |
| prefill (23 tokens at once) | 120.1 ms | 6.7% |
| decode (32 tokens) | 1,666.6 ms | 93.3% |
| detokenize | 0.0 ms | 0.0% |
Prefill processes the whole prompt in one pass — parallel, cheap per token. Decoding is serial by definition: each token needs the previous one. Optimizing tokenization would be wasting time on 0.0% of the problem.
| Configuration | Time | tok/s |
|---|---|---|
| with cache | 1.86 s | 17.2 |
| without cache | 3.02 s | 10.6 |
1.62× — less than the "10×" one usually hears, because with 32 tokens the context is still short and the gain is quadratic in length. The cache is only possible because the mask is causal: the past never changes, so its
| Batch | total tok/s | Gain |
|---|---|---|
| 1 | 30.3 | 1.00× |
| 2 | 63.7 | 2.10× |
| 16 | 140.8 | 4.65× |
Each generated token reads all of the model's weights from memory. With batch 1, that read serves one request; with batch 16, sixteen — for the same cost. It is nearly free throughput, and it is the reason continuous batching exists in TGI and vLLM.
Our server handles one at a time. For a study bench that is right; for production, it is the first bottleneck to attack. It is documented as a conscious limitation, not as an oversight.
contexto 512 → 29,6 ms/token contexto 1000 → 34,6 ms/token contexto 1024 → PAREDE: posição 1024 não existe na wpe (ela tem 1024 linhas: 0 a 1023)
The error that broke the measurement script on its first run is literally GPT-2's architectural limitation — and it is exactly what RoPE eliminates (chapter 02).
The first version kept every loaded model in memory, forever. The seven models in float32 add up to ~13 GB — above the virtual memory of an ordinary development machine — and the operating system kills the process once it fills up:
OSError: the paging file is too small for this operation to complete. (os error 1455)
There was a second bug alongside it, more subtle: the cache key included whether the model had been loaded with an attention map or not. Opening the Playground and then the X-Ray on the same model put two copies of it in memory. Nobody noticed — only the RAM disappearing.
The fix was a cache with a budget: an LRU with a ceiling (LAB_CACHE_GB) that evicts the least-used model when it is exceeded, with GET /info showing the state. Tested:
gpt2 → 0.46 GB + medium → 1.79 GB + large → 4.67 GB + gpt2-xl → 5.80 GB ← evicted the three smallest + det-large → 1.32 GB ← evicted the xl
And attn_implementation="eager" became always-on — one entry per model, and it is what the X-Ray needs anyway.
The project's nine scripts became forms, generated from a catalog on the server. The log arrives line by line via Server-Sent Events while it runs; closing the tab does not kill the process, and coming back shows the log from the beginning.
The button sends CTRL_BREAK_EVENT, not kill. Training receives the signal, saves the checkpoint and exits cleanly — running again with the same run name continues from where it stopped. A kill would lose the progress since the last evaluation.
An open port that executes arbitrary commands is an open port that executes arbitrary commands, even on localhost. The design is a closed list: each script declares its arguments with type, minimum and maximum, and the argv is assembled by code — never concatenated into a shell string.
montar('treinar', {'rodada': 'a; rm -rf /'})
-> rodada: use apenas letras, numeros, - _ . e virgula
montar('treinar', {'passos': 999999})
-> passos: maximo 200000
One card per training run saved in model/runs/, with the curve drawn in pure SVG — training loss in gray, validation in green, anisotropy in dashed purple. The badges are read from the folder name: moderno, GPT-2 fiel, λ=1.
Below, a field that measures bits/character across all models at once and highlights the best. It is the honest metric across different tokenizers — perplexity is measured per token, and comparing PPL across vocabularies means nothing.
| Endpoint | Origin | Does |
|---|---|---|
POST /generate | TGI | completes text (HF and ours) |
POST /generate_stream | TGI | token by token (SSE) |
POST /embed | TEI | vectors |
POST /predict | TEI | Fake/Real classification |
POST /attention | ours | n×n map + head profile |
POST /logit_lens | ours | layer-by-layer prediction |
GET /modelos | ours | our training runs, with curve and metrics |
POST /nosso/perplexidade | ours | loss, PPL and bits/character |
POST /lab/rodar | ours | launches a script |
GET /lab/log/{id} | ours | live log (SSE) |
The compass parameter on /generate is the extension chapter 05 describes: {"alvo": "…", "alpha": 0, "beta": 8}.
The HuggingFace models come with generate(), cache, streamer and an entire ecosystem. Ours is a 400-line nn.Module. Instead of pretending they are the same, the difference lives in server/ours.py, in one place only, and the rest of the server deals with a single interface.
Generation decodes incrementally and sends only what grew — byte-level BPE may need two tokens to form one character, and decoding each token in isolation would put garbage on the screen.
Result: our mini generates at 127 tok/s against ~7 for gpt2. Two hundred times smaller, eighteen times faster.
Chapter 09 shows the whole architecture, aligned with the world model.
Chapters 04 and 05 — the lineage of collapse and the rudder — wait for the experiments to close: writing them earlier would be making up numbers.