Upgrading tiny-shakespeare with "reasoning" — a real experiment
We asked whether our 10.8-million-number Shakespeare machine could be upgraded to "reason". The honest answer changed the question — and then we ran the experiment anyway, with the exact same machine. Some of what we expected turned out to be wrong, and the page says so.
The first thing to know is that deepseek-r1 and tiny-shakespeare are the same kind of machine. Both are decoder-only transformers: the same stack of attention and feed-forward blocks, the same next-token guessing loop. r1 is far bigger, but nothing in its wiring is "the reasoning part".
What makes r1 "think" is what it was trained on: problems with checkable answers, where writing intermediate steps before the answer got rewarded. The <think> span is simply tokens it learned to produce first. So "reasoning" isn't a feature you add to the architecture. It's a property of the training text.
That leaves Shakespeare with a problem: there is nothing in his plays to reason about. No questions, no answers, no way to be wrong. Training the model to emit a <think> span before more Shakespeare would produce decoration between tags — exactly the kind of theatre this series warns against.
model.py, unchanged: 10,754,324 numbers, 6 layers, 6 heads, 384-wide, 256 tokens of context. The only thing that differs from tiny-shakespeare is the text it was trained on. That is the whole point.We picked the dullest possible task that still needs several steps: adding two numbers of up to three digits. It's free to generate a million examples, trivial to check, and the carries make it genuinely sequential — the leftmost digit of the answer depends on what happened at the far right.
Then we wrote the same problem three ways. The words in angle brackets are single tokens, just like r1's <think>:
Read the scratchpad right to left, the way you were taught at school: each column is digit + digit + carry-in = carry-outresult. The answer is the result digits read back in reverse, with a leading 1 if the last column carried.
The filler version is the control. If the scratchpad model wins just because it gets more tokens to compute over, the filler model should win too. If it's the content of the steps that matters, filler should look like direct.
We trained three models from the same random starting point, same seed, same 5,000 training loops, same learning rate and batch size as tiny-shakespeare. Each took about 16 minutes on the same single GPU. The only difference between them is which of the three formats they saw.
One deliberate change from the Shakespeare run: the model is only scored on the part it's supposed to write — everything after </q>. The question digits are random, so scoring the model on guessing them would just add noise.
004 for 2+2: right number, wrong shape). We capped how much of each digit class the held-out sets may take, regenerated the data, and retrained all three models. The numbers below are from the second run. The first run's files are kept in reasoning/results/_first_run/.These are "how wrong" scores on problems the models never trained on, like the bars in Getting Less Wrong. They are not comparable across the three columns — the scratchpad model is scored on far more tokens than the direct one — so don't read the heights against each other. What matters is the next section.
We gave each model 2,000 problems it had never seen and checked whether the final answer between <a> and </a> was exactly right. Greedy decoding — the most probable token every time — so every answer is a deterministic function of the weights and the question.
We expected the scratchpad model to win, especially on sums with several carries. It didn't, because there was nothing to win: the direct model already gets every one of the 2,000 sums right. Adding two three-digit numbers, given 300,000 worked examples and 5,000 training loops, is within reach of this machine in a single pass, with no steps written down at all. Broken down by carries:
| 0 carries | 1 carry | 2 carries | 3 carries | |
|---|---|---|---|---|
| Direct | 100.0% | 100.0% | 100.0% | 100.0% |
| Filler | 99.2% | 98.5% | 98.0% | 96.6% |
| Scratchpad | 100.0% | 100.0% | 100.0% | 100.0% |
| problems | n=762 | n=823 | n=356 | n=59 |
The only model that lost anything was the filler one. It got 27 sums wrong, and 21 of those 27 numeric misses were off by exactly +100: a carry lost in the hundreds column. The dots aren't free — the answer now has to reach back across 26 tokens of nothing to find the question, and occasionally it fumbles.
If the scratchpad model had learned the algorithm — one column at a time, carry forward — it should manage a four-digit sum, even though it never saw one. So we asked both models for sums of four, five and six digits. Nothing was retrained.
| 3 digits | 4 digits | 5 digits | 6 digits | |
|---|---|---|---|---|
| Direct | 100.0% | 0.0% | 0.0% | 0.0% |
| Filler | 93.8% | 0.0% | 0.0% | 0.0% |
| Scratchpad | 100.0% | 0.0% | 0.0% | 0.0% |
| seen in training? | yes | never | never | never |
Neither generalises at all. The direct model writes a three-digit-shaped answer. The scratchpad model writes exactly three columns and stops, then reads its answer off a trace that ignored the fourth digit. What it learned was "three-digit long addition", not "long addition". The working is real, but it is a habit of a fixed length, not a rule the model can extend.
The last place the two formats can differ is how much they need to see. We retrained the direct and scratchpad models from scratch on only the first 3,000 of the 300,000 worked examples, for 2,500 loops, and tested on the same 2,000 unseen sums.
Here the two formats finally separate. With only 3,000 worked examples, the direct model reaches 80.4% on unseen sums while the scratchpad model reaches 100.0% — and its working matches the correct trace on 100.0% of them. Every column of the scratchpad is a small, repeated rule, so each example teaches the model about 29 things instead of about 3. That is the mechanism by which showing the working helps: not extra thinking time, but denser, more checkable practice.
None of the above says whether the visible steps are what produce the answer. A model could, in principle, write a plausible trace and then compute the answer some other way — the direct model proves it can add without any trace at all. That's the faithfulness question we left open on The Larger Journey, and with a model we fully own we can test it directly.
The method is simple: we write part of the trace ourselves, corrupt it, and let the model carry on. If the answer follows our corruption, the answer is being read off the trace. If the answer stays correct, the trace is being ignored.
| follows the trace | follows the truth | other | n/a | |
|---|---|---|---|---|
| A. Swap one digit Force the whole correct trace with one result digit changed. | 100.0% | 0.0% | 0.0% | 0 |
| B. Flip one carry Force the trace up to a column with its carry flipped; let the model continue. | 100.0% | 0.0% | 0.0% | 0 |
| C. Wrong sum's trace Force the correct working for a different sum entirely. | 100.0% | 0.0% | 0.1% | 0 |
"n/a" counts cases where the corrupted trace happened to imply the true answer anyway, so the two can't be told apart. Percentages are of all 2,000 test problems (B uses the 1,980 problems with two or more columns).
646+474 (true answer 1120)6+4+0=10|4+7+1=15|6+4+1=11 — one result digit changed, so the trace now implies 11501150This one tests whether the steps are chained: does a wrong carry in column one propagate into column two? On 100.0% of problems the model's continuation was exactly what the wrong carry implies, step by step, to the final answer.
646+474 (true answer 1120)6+4+0=10|4+7+1=12|6+4+1=116+4+0=10|4+7+1=02| — with the carry flipped — then let it continue6+4+0=10|4+7+1=02|6+4+0=10 and answered 1020, which is exactly what the wrong carry implies (1020)646+474 (true answer 1120)1+2+0=03|5+3+0=08|0+7+0=07783 — the answer to 51+732, not to the question it was askedThe exception is worth a look. Asked 7+680 but handed the working for 925+786 (which implies 1711), the model answered 171: it copied the trace's digits but dropped the leading carry, apparently because a 3-digit question is not supposed to produce a 4-digit answer. So the question still exerts a small pull of its own; the trace is dominant, not the only input.
Finally we forced an empty <think></think> and made the scratchpad model answer immediately. It got 0.2% right. That's outside anything it was trained on, so it's weak evidence on its own — but it lines up with the rest.
The scratchpad model was converted to GGUF with the same conversion as tiny-shakespeare, plus one addition: the six delimiter tokens are written as "added tokens" so llama.cpp treats <think> as one token. That's how r1's own tokenizer marks them.
Ollama then does what it does for r1: it spots the tags and splits the output into a thinking field and a content field. Here is the real response, straight from the API:
# curl http://localhost:11434/api/chat (model: tiny-adder, think: true) user 472+38 thinking 2+8+0=10|7+3+1=11|4+0+1=05 content <a>510 prompt tokens 8, generated tokens 33, done_reason stop, total 2925 ms
And because it's an ordinary Ollama model now, every tool built earlier in this project works on it unchanged. The sampling tree for this exact response shows the probability the model put on each digit of its working, token by token.
| tiny-adder | deepseek-r1 | |
|---|---|---|
| Architecture | decoder-only transformer, 6 layers | decoder-only transformer, 32 layers (8B distil) |
| Think delimiters | <think> </think> as added tokens | <think> </think> as added tokens (ids 128798, 128799) |
| Where the working came from | we wrote the traces with a 15-line Python function and trained on them (supervised) | traces emerged from reinforcement learning on checkable answers, then were distilled into smaller models (supervised) |
| What it can reason about | addition of numbers up to 999 | very broadly, but not verifiably |
| Can we check the working? | yes, character by character | only by reading it and judging |
| Is the working faithful? | tested above: yes, causally | unknown; cannot be tested this way at this scale |
Not about Shakespeare, and nothing we could add to the machine would change that. The same machine, shown worked additions, did learn to write its working before its answer — and when we tampered with that working, the answer followed the tampering. That is what "reasoning" cashes out to inside these systems: a learned habit of writing intermediate tokens that the later computation depends on.
But the experiment also refused to flatter the idea. On this task the working bought no accuracy, because the machine could already do the sum in one pass. It did not extend to longer sums, because the habit was three columns long. What the working did buy is the thing this whole series has been about: the intermediate results came out as tokens, where we could read them and check them — unlike the 10,754,324 numbers in The Missing Index, which still have no dictionary. Reasoning, in the only sense we could measure, is visibility. Whether it also makes the answer better depends entirely on whether the answer was out of reach without it.
reasoning/. adder_data.py makes the examples; train_adder.py --variant scratchpad|direct|filler trains each model; eval_adder.py scores them; faithfulness.py runs the four interventions; to_gguf_adder.py plus the Modelfile make the Ollama model. This page reads its numbers from the JSON those scripts write.