Building the loop

We had been reading about looped transformers without building one. The idea sounded approachable: reuse the same transformer block several times, giving the model more computation before asking for an answer. The part we wanted to try was the loop count. Once a model has learned something, what happens if you let it keep going?

Astra was the reason to finally open an editor. Fortune reports that it uses recurrent depth in part of its architecture. [1] We set up a Python virtual environment and built a much smaller experiment on the CPU: 20,264 parameters, eight possible answers, no downloaded model weights.

The task was to follow two links. Each input contains eight numbered nodes, each pointing to another node, plus a starting position. Starting at 3, for example, you might follow 3 to 4 and then 4 to 2. The answer is 2. A fresh permutation changes the links, so remembering that 3 goes to 2 won't get you very far.

This gave us a way to examine a wrong answer without debating what the question meant. We could also inspect the answer after every few loops. The model never produces a written explanation; a small output layer turns its current internal state into scores for the eight nodes.

The relevant part of the implementation fits in a few lines. self.block is one attention-and-feed-forward block with shared weights. The next pass receives the state left by the previous one.

experiment.py
h = self.initial(p, start)
for _ in range(loops):
    h = self.block(h)
return self.head(self.norm(h[:, -1]))

We first trained it to answer after four passes. Only that final answer contributed to the loss. There was no instruction to visit one node per pass, and nothing rewarding it for keeping a correct answer intact after pass four. That detail became more interesting once the model had learned the task.

It had the answer already

All three fixed-four runs answered every one of the 4,096 test inputs correctly at four loops. We then kept the weights and inputs unchanged and increased the loop count. By 24 loops, accuracy had fallen to 92.41%, 95.24% and 87.82% across the three seeds.

Here is one of those lost answers. This is the first test item in seed 11 that was correct at four loops and wrong at 24, replayed from the saved checkpoint. The points row tells you where each node in the row above it leads.

Terminal output
Seed 11 / fixed-four / evaluation item 21
node:    0 1 2 3 4 5 6 7
points:  1 7 0 4 2 5 3 6
start=3   path=3 -> 4 -> 2   expected=2
loops   answer   softmax of chosen answer
    2        2                   0.5975
    4        2                   0.9976
    8        2                   0.9973
   16        2                   0.8091
   24        5                   0.9322

The answer stays at 2 through loop 16. At loop 24, it is 5. We initially wondered whether continuing the loop simply made it follow too many links. But trace the input: 3 → 4 → 2 → 0 → 1 → 7 → 6 → 3. Node 5 is outside that cycle. You can follow those links forever without reaching it.

So this particular mistake isn't explained by an extra hop. The internal computation has moved somewhere that the output layer reads as 5. Its softmax score for that choice is 0.9322, higher than the score attached to its correct answer at loop 16. That number is an output score, not a measured probability of being right; here, it accompanies an answer we can disprove by inspection.

We had expected to spend most of the experiment waiting for the task to be learned. Watching a learned answer disappear was more interesting. It also suggested an obvious change to the training.

What if training stops at different depths?

For the second set of runs, we stopped training examples after two, four or six loops. Each group of three updates used all three depths in shuffled order. The model therefore had to produce the answer at several stopping points, with the loss still applied only at the selected final pass.

Everything else stayed the same: architecture, optimiser, batch size, number of updates, and the input stream for each seed. Both schedules used 4,800 block iterations during training. The mixed schedule averaged four loops per update; it didn't get a larger average loop budget.

The terminal report below includes every run. The test set contains 4,096 distinct inputs from a partition excluded from training. We used the same test inputs for both schedules and all three seeds.

Terminal output
$ python report.py
Task: follow two links in an eight-node permutation
20,264 parameters | CPU | 1,200 updates x 128 examples
4,096 distinct evaluation inputs, excluded from training
Each run: 4,800 training block iterations

                 Accuracy at inference loop
training seed       2       4       8      16      24
fixed      11   97.39  100.00   99.98   97.97   92.41
fixed      22   96.78  100.00  100.00   98.85   95.24
fixed      33   95.63  100.00   99.98   95.95   87.82
mixed      11  100.00  100.00  100.00  100.00  100.00
mixed      22  100.00  100.00  100.00   99.95   99.83
mixed      33  100.00  100.00  100.00  100.00  100.00
Accuracy across loop counts for the original two-hop task, showing every seed and schedule means.
Same task and test inputs throughout. Scroll the chart sideways on smaller screens.

At 24 loops, two mixed-depth runs still had all 4,096 answers correct. The other missed seven. We hadn't trained any of them beyond six loops. Varying the stopping point had made this little model much less likely to lose its answer when we extended the computation.

This wasn't a new failure mode waiting to be named. Loop, Think, & Generalize studies recurrent-depth models and describes degradation under excessive recurrence. [2] The useful part of building a small version was seeing how much the result changed with the training schedule. Our first curve, by itself, would have made continued looping look much less promising than the second one did.

Six hops still finished too quickly

We next asked for the whole six-hop path through a sixteen-node graph. Getting five positions right and one wrong counted as a wrong answer. The models trained at a fixed six loops all reached at least 99.4% complete-path accuracy there. At 64 loops, their scores were 8.79%, 64.50% and 1.46%.

That looked like a much bigger penalty for a longer answer, but individual hops survived better than complete paths. One changed position is enough to spoil the latter. The two panels below show both measurements; looking only at complete-answer accuracy makes the collapse look more comprehensive than it is.

Six-hop complete-path accuracy declines more sharply than individual-hop accuracy; mixed-depth training retains more correct answers.
Same task and test inputs throughout. Scroll the chart sideways on smaller screens.

This still didn't address the case we wanted most: a model that needs dozens of iterations to arrive at an answer in the first place. Some of these six-hop models solved the task in three or four passes. Their attention could see the whole graph; the number of links wasn't a reliable measure of how many loops they needed.

A task that cannot finish in six loops

For that test, we changed how information could move. Each node could attend only to itself and its next neighbour. We put a random 16-bit payload at one end of a chain and asked the other end to recover it. Across 32 links, information about that payload cannot arrive before the 32nd pass.

That lower bound comes from the attention restriction we imposed. This is a controlled test of carrying and preserving information through repeated computation; copying the payload doesn't become a difficult reasoning problem merely because we made the chain long. The restriction does let us rule out an answer obtained through a global-attention shortcut.

Getting even that to work took a few attempts. Starting directly at 32 links left the model near guessing. A curriculum learned short chains but struggled as they grew. We then normalized the state after each residual update and trained through chains of 4, 8, 16 and 32 links, allowing two loops per link. The last stage therefore read the answer at loop 64.

We kept 512 distinct payloads out of training and evaluated the same checkpoints out to 256 loops. Once the coarse sweep showed a peak, we went back and sampled every loop from 32 to 96. No weights changed during that closer look.

Terminal output
$ python dense_report.py
32 links / 16-bit answer / 512 held-out payloads
Same checkpoints; evaluation sampled one loop at a time

seed   best loop   complete answers    at 64    at 80
  11          64       50/512           50        2
  22          61        2/512            2        1
  33          64      406/512          406        6

The third seed recovered 406 of the 512 complete answers at loop 64. The other two recovered 50 and 2. That difference matters: the training wasn't reliably solving the long task across runs. But in the run that learned it best, we could finally watch an answer arrive after dozens of passes and then disappear.

Two more loops were enough to lose a bit

The following is the first payload in seed 33 that was correct at loop 64 and wrong at loop 80. All sixteen bits have to match. We included the earlier attempts as well as the correct answer; you can see it getting closer before it reaches the target.

Terminal output
Seed 33 / item 0
expected 1000000110000000
loops    predicted answer  correct bits
   48    0010100110000010   12/16
   56    1000000110000110   14/16
   60    1000000010000100   14/16
   62    1000000110000100   15/16
   64    1000000110000000   16/16
   66    1000000110000010   15/16
   68    1000001110000010   14/16
   72    1010001110000010   13/16
   80    1010001110000010   13/16
   96    1010001110000010   13/16

At 62 loops, one bit is still wrong. At 64, they all match. At 66, a different bit has changed. Nothing new entered the model between those readings. We simply applied its block again.

32-link experiment: all three seeds peak near their final training depth. Seed 33 reaches 79.3 percent at loop 64 and falls to 1.2 percent at loop 80. The zoom samples every loop.
Same task and test inputs throughout. Scroll the chart sideways on smaller screens.

Across the full test set, that seed fell from 406 correct answers at loop 64 to six at loop 80. By 96, none were completely correct. Individual examples can deteriorate a bit at a time while the complete-answer score falls sharply; a single flipped bit removes an example from that score.

The location of the peak is hard to miss. Sixty-four loops was also the stopping point used in the last training stage. We had trained the model to deliver an answer there. We had not required the answer to remain usable indefinitely afterwards.

Trying to widen the window

Changing the training depths had helped the first, smaller model. We tried a related change here: another 1,200 updates, this time supervising nodes that could have received the payload at several intermediate loops. The idea was to make a correct representation useful at more than the final stopping point.

It wasn't a successful fix. In seed 33, complete-answer accuracy at loop 64 fell from 79.3% to 8.2%. At loop 128 it improved from zero to 5.7%. We had kept a few answers alive longer while losing most of the strong result at the original stopping point. The other seeds remained weak.

Terminal output
$ python refinement_report.py
32 links / complete 16-bit answers, percent correct
Extra training also changed the supervision objective.

seed stage          64      80     128     256
  11 before     9.77    0.39    0.00    0.00
  11 after      0.98    1.17    0.78    0.39
  22 before     0.39    0.20    0.00    0.00
  22 after      2.15    1.76    0.20    0.00
  33 before    79.30    1.17    0.00    0.00
  33 after      8.20    8.01    5.66    0.78

We changed the objective as well as adding training, so this comparison can't isolate either one's effect. What it does show is that the promising result from varying depths on the small task wasn't a ready-made solution for the longer one.

Astra is the larger-scale connection that prompted the experiment, and this gives us a specific behaviour to wonder about there: an answer can become correct during recurrent computation and then be lost. Here we could see the last missing bit arrive at loop 64. We haven't yet found a training setup that reliably lets the following passes preserve the whole answer.

What does this mean?

There’s a sweet spot in how long you let the model work. A simple question may be answered early, leaving later iterations to disturb something that was already correct. A complex question needs more passes; stop too soon and you get an answer that the model could still have improved.

You can see both sides in the bit trace. At 62 loops, the answer wasn’t finished. At 64, it was correct. At 66, it was wrong again. The useful stopping point sat between unfinished work and a result that had started to deteriorate.

That makes deciding when to stop part of solving the problem. A fixed loop budget has to serve questions of different difficulty. An adaptive one has to recognise when further computation is helping, and when it should leave the answer alone.

Run it locally

The Python scripts, exact dependency versions, raw predictions and generated report are included. The README covers the setup and the test split.

Download the experiment (.zip)