Import the tools
This cell loads PyTorch and the lab helpers, fixes the random seed, and prints the GPU this session runs on. The helpers handle model loading, sampling, grading and hooks so the TODO cells can focus on the edit itself.
Load Qwen3.5-0.8B
We load the real Qwen3.5-0.8B in bfloat16. The model has 24 decoder layers, and each MLP ends in a down_proj matrix of shape 1024 by 3584. Every column of that matrix is the 1024-number vector one MLP neuron adds to the residual stream, which is why a column is the natural unit to edit. A vLLM engine holds a second copy of the model for fast sampling, and getting it ready takes about three minutes. The model starts loading as soon as your session starts, so it is usually ready by the time you reach this cell. If it is still loading, the cell waits for it and prints how long loading has taken so far. Every edit you make later is copied into the engine in place, so it never has to load again.
Load the math problems
The problems come from GSM8K. The 64 held-out problems are drawn from the test split and are only used to measure the model before and after the edit. The practice set comes from the training split, so you can read extra problems without touching the held-out ones.
Run the unedited model
The model now answers every held-out problem with thinking turned on and the paper's sampling settings, which are temperature 0.6, top-p 0.95, top-k 20 and a presence penalty of 1.5. Each answer may use up to 8,192 tokens, and the 64 answers take a little over a minute. In our test run only 7 of the 64 answers were correct and 58 ran into the cap. While the batch runs, the output follows one question, problem 12 about fabric for curtains, token by token. It comes back when that answer starts repeating itself, so you can watch the loop form. The edited model answers the same question later in the lab, so you can compare the two. Short progress lines report how many answers have finished, and the summary prints once all 64 are done. Pass live=False to generate_answers if you only want the summary. Read the tail that gets printed at the end and you will usually see the model repeating the same sentence instead of finishing.
Measure the loops
TODO 1Before changing anything we need one number that says how often the model gets stuck. An answer counts as stuck when it runs into the token cap or when the helper flags its ending as a loop. Each result is a dictionary with the keys hit_cap and looping.
Return the share of results that hit the token cap or were flagged as looping.
- results is a list of dictionaries, each with the boolean keys hit_cap and looping
Returns: A float between 0 and 1, and 0.0 for an empty list.
Check: For the unedited model the rate is around 0.9, because most answers loop until the cap.
Show hint
Count the results where either flag is True and divide by the number of results.
Write a loop detector
TODO 2A looping answer keeps writing the same phrases. You can catch that by splitting the last few hundred words into overlapping groups of n words and checking how many of those groups appear more than once. A healthy answer has almost no repeats, while a stuck answer is mostly repeats.
Return True when at least threshold of the n-word groups in the last tail_words words appear more than once.
- text is the answer as one string
- n is the number of words in each group
- tail_words is how many words from the end to inspect
- threshold is the share of repeated groups that counts as a loop
Returns: A bool, and False when the tail has fewer than n words.
Check: True for a text that repeats one sentence many times, False for normal reasoning, and agreement with the helper on every answer.
Show hint
Use text.split() for words, build tuples of n consecutive words, count them with Counter, then divide the number of groups whose count is above 1 by the number of groups.
Read the looping and productive pairs
To find what drives a loop we compare two versions of the same reasoning. Each pair shares a problem and an opening, then one version loops and the other keeps working. These four pairs are the ones the authors used, taken from their published logs. The helper feeds each text through the model and records the residual stream after layers 15 and 19, averaged over the last 36 tokens, and the input to each down_proj, averaged over the last 24 tokens.
Find the loop direction
TODO 3If looping has a direction in the residual stream, the average gap between the repetitive and productive texts should point along it. You will turn that gap into a unit vector u for each layer. The cell then checks that u separates the two sides and compares the two layers.
Average the repetitive minus productive residual vectors over the pairs and scale the result to length 1.
- h_rep is a float tensor of shape 4 by 1024 for the repetitive texts
- h_prod is a float tensor of shape 4 by 1024 for the productive texts
Returns: A float32 tensor u of shape 1024 with length 1 that points toward the repetitive side.
Check: Each u has length 1.000, and the repetitive side scores higher along u than the productive side.
Show hint
Take (h_rep - h_prod).mean(0) and divide it by its norm.
Pick the columns that write the loop
Not every neuron matters. A column is worth editing when it writes a lot along u and its neuron fires more on the repetitive texts. The helper multiplies those two numbers for each of the 3584 columns and keeps the top 64 per layer, then reports how many match the 64 columns the authors chose. With the authors' four pairs you should share all 64 columns in layer 15 and 63 of 64 in layer 19.
Edit the weights
TODO 4Now we write the edit by hand. For each chosen column we take its component along u and subtract six times that amount, which flips the column so it pushes away from the loop instead of toward it. The math is done in float32 on the original weights and only then copied into the bfloat16 model and the vLLM engine. Columns that were not chosen must stay exactly the same.
Return W0 minus alpha times the outer product of u and the masked projection u @ W0, where the mask keeps only the chosen columns.
- W0 is the original float32 weight of shape 1024 by 3584
- u is the unit direction of shape 1024
- columns is a tensor of 64 column indices
- alpha is the strength of the edit
Returns: A new float32 tensor of shape 1024 by 3584. W0 itself must not change.
Check: Only the 64 chosen columns differ from W0, which gives about 131,072 changed weights across the two layers.
Show hint
Build a zero mask of length 3584, set it to 1 at columns, then compute W0 - alpha * torch.outer(u, mask * (u @ W0)).
Run the edited model
The edited model answers the same held-out problems with the same sampling settings, the same seed and the same 8,192-token cap. This run is a little faster because fewer answers loop to the cap. The live view follows the same question as before the edit, problem 12 about fabric for curtains. In our test run it stopped thinking after about 1,800 tokens instead of looping, and the output shows it writing its final answer token by token. The cell prints the first problem that looped before the edit and is now correct, so you can read how its ending changed.
Measure again
TODO 5The last step puts the two runs side by side. Accuracy is the share of correct answers, the loop rate is your TODO 1 function, and mean tokens is the average answer length.
Return a table with accuracy, loop_rate and mean_tokens for both runs and the change between them.
- before is the list of results from the unedited model
- after is the list of results from the edited model
Returns: A dict with the keys accuracy, loop_rate and mean_tokens, where each value is a dict with before, after and change, and change is after minus before.
Check: Accuracy goes up and the loop rate goes down after the edit. In our test run accuracy rose from 0.11 to 0.33 and the loop rate fell from 0.92 to 0.66.
Show hint
Write a small summary function for one run, call it on both runs, then build each row from the two summaries.
Undo the edit
The edit lives only in this session. This cell copies the original weights back and confirms that no weight is still changed, so you can try a different alpha or a different number of columns.