Lab 6: The weight edit is steering in disguise
On this page
Big thing: Changing the 64 chosen columns of down_proj in the weight file gives the same outputs, up to rounding, as leaving the weights alone and subtracting from that layer's output during the forward pass. The single number stays small while the model reasons well (about 0.1 at layer 19 on answers that finished correctly) and grows several times larger once it repeats itself (about 0.76 inside real loops). A fixed weight edit therefore acts like steering whose strength the text sets on each token.
Why it matters: Weight editing changes the numbers stored in a model, while activation steering adds a vector to internal activations as the model runs. This edit is both at once: a steering vector , scaled on every token by how hard 64 neurons push along it. That explains how changing 131,072 stored numbers pushes hard against loops and much less on ordinary reasoning, though it still pushes a little.
Time and hardware: On one A100 GPU, parts 1 and 2 take about 1 minute, part 3 takes about 2 minutes, and part 4 takes under a minute. Every part runs on sampler or trainer. Parts 3 and 4 read the base model's practice answers in runs/build/base. If the machine lacks that run, make it first with python scripts/run_jobs.py plans/sampler.json --only build/base --gpus 7 (about 24 minutes on one GPU). Redrawing the part 3 figures (--plot-only) runs on CPU in a few seconds.
Theory guide: Section 12, part D (weight editing versus activation steering).
Before you run: predict
Write down a short guess for each question before running the script.
- Load Qwen3.5-0.8B in 32-bit floats (
fp32) and run the same texts two ways: by editing the 64 columns ofdown_projin memory, and by leaving the weights untouched while a hook subtracts fromdown_proj's output. Will the final next-token scores (logits) match to 16 decimal places, to about 2 decimal places, or only roughly? - On one MLP matrix (
layer 15) on CPU in 64-bit floats (fp64), how big is the maximum difference between the edited matrix output and the hook output? - On the agent's 4 productive math texts versus its 4 looping math texts, is (how hard the 64 neurons push along the loop direction ) large on both and canceled by later layers, or near zero on productive text and positive on looping text?
Run it
Connect to sampler or trainer, since every part runs on either one. Then use nvidia-smi to pick a free GPU index and pass it in CUDA_VISIBLE_DEVICES.
The script reads three files from the repository: results/edit_info.json (the released edit's , and 64 columns, from scripts/build_models.py), results/lab3/rebuild.json (from Lab 3, giving the sign of that points toward looping), and labs/data/astra_pairs.json (the agent's 8 texts).
ssh sampler
. /nvme/wsp-lab/code/scripts/env.sh && cd /nvme/wsp-lab/code
nvidia-smi
CUDA_VISIBLE_DEVICES=7 python labs/lab6_steering.py
Parts 1 and 2 (the default, --parts 1,2) finish in about 1 minute and write two files:
results/lab6/equivalence.json(comparing the in-memory weight edit, the steering hook, and the releasedbf16weight file)results/lab6/s_on_pairs.json( on every token of the 8 texts inlabs/data/astra_pairs.json)
Part 3 traces real loops token by token from the unedited model's answers in /nvme/wsp-lab/runs/build/base:
CUDA_VISIBLE_DEVICES=7 python labs/lab6_steering.py --parts 3
Part 3 finishes in about 2 minutes and writes four files:
results/lab6/real_loops.json(summary statistics across 48 looping answers)results/lab6/trace_one_loop.json(per-token arrays for one answer whose loop starts near the middle)results/figures/lab6_real_loop.pngandresults/figures/lab6_loops_summary.png
To measure the same signals on answers that finished without looping, run part 4 on the same VM as part 3 (or pass --parts 3,4 to run both):
CUDA_VISIBLE_DEVICES=7 python labs/lab6_steering.py --parts 4
Part 4 takes under a minute and reads part 3's results/lab6/real_loops.json on the same machine to compare the two groups and draw the figure; without that file, it writes only the good-answer numbers. With --try-name or --max-samples, part 4 uses the same results/lab6/try-<name>/ folder as part 3 and compares against the loops saved there, or against the main real_loops.json if part 3 saved none. It writes two files:
results/lab6/finished_answers.json(summary statistics and per-answer numbers for 26 finished answers)results/figures/lab6_finished_vs_loops.png(three-panel box-plot comparison of good answers and loops)
To redraw the part 3 figures from the saved JSON files on CPU (--plot-only does not redraw part 4's figure), run:
python labs/lab6_steering.py --plot-only
What you should see
Parts 1 and 2 print:
fp32_hook_vs_edit_max_abs_logit_diff 0.00774
fp32_base_vs_edit_max_abs_logit_diff 11.4
fp32_hook_vs_edit_mean_kl 1.41e-08
fp32_base_vs_edit_mean_kl 0.0178
bf16_hook_vs_released_file_max_abs_logit_diff 0.438
bf16_hook_vs_released_file_mean_kl 0.000539
bf16_base_vs_released_file_mean_kl 0.0184
bf16_same_top_token_hook_vs_file 0.994
fp64_one_layer_max_abs_diff 3.33e-16
fp64_one_layer_output_scale 0.65
layer 15: s over the last 36 tokens, productive [0.0, 0.04, 0.04, 0.01] vs looping [0.19, 0.19, 0.42, 0.25]
layer 19: s over the last 36 tokens, productive [-0.03, -0.04, 0.02, -0.01] vs looping [0.09, 0.11, 0.44, 0.3]
Part 1 (results/lab6/equivalence.json) compares the gap between the hook and the weight edit against the edit's effect on the base model:
| Comparison | Max absolute difference | Mean KL divergence | What it checks |
|---|---|---|---|
fp64, layer 15 down_proj, hook vs edit |
3.33e-16 (on outputs of scale 0.65) |
not applicable (single layer) | Exact match up to 64-bit rounding |
fp32, full model, hook vs in-memory edit |
0.00774 (0.0077 in logits) |
1.41e-08 (1.4e-8) |
Full-model rounding across 24 layers (largest logit gap about 1,500 times smaller than the edit's effect, mean KL about 1.3 million times smaller; the gap drops to 0.00134 with full IEEE fp32) |
fp32, full model, base vs in-memory edit |
11.4 (logits) |
0.0178 |
Actual size of the edit's effect in fp32 |
bf16, hook on base vs released qwen35-p50c file |
0.438 (0.4375 in logits) |
0.000539 (5.4e-4) |
Same top token on 99.4% (0.994) of positions |
bf16, unedited base vs released qwen35-p50c file |
not measured by script | 0.0184 |
Actual size of the released bf16 edit's effect (34 times larger than the hook-vs-file KL) |
Part 2 (results/lab6/s_on_pairs.json) measures mean over the last 36 tokens of each of the 8 texts, with oriented so positive values point toward looping:
| Layer | Productive texts (4 texts) | Looping texts (4 texts) |
|---|---|---|
| Layer 15 | 0.00, 0.04, 0.04, 0.01 |
0.19, 0.19, 0.42, 0.25 |
| Layer 19 | -0.03, -0.04, 0.02, -0.01 |
0.09, 0.11, 0.44, 0.30 |
On the four short productive texts, the 64 neurons barely push along , or push slightly the other way. On the four looping texts, their push along reaches 0.09 to 0.44. Because the edit subtracts , the text sets the push strength, unlike a constant nudge. Part 4 shows that on long real answers that finish correctly, is small but not zero.
Each pair shares an opening (75, 85, 70 and 69 tokens). On that shared prefix, matches in the first three pairs (0.0000 max difference) and differs by at most 0.0018 at layer 15 and 0.0042 at layer 19 in the fourth pair, from bf16 rounding on the GPU when the two texts have different total lengths. After the split, climbs on the looping text to peaks of 0.46, 0.47, 0.59, 0.49 at layer 15 (0.4558, 0.4713, 0.5897, 0.4873) and 0.36, 0.43, 0.74, 0.63 at layer 19 (0.3620, 0.4300, 0.7376, 0.6322).
Part 3 (results/lab6/real_loops.json and results/lab6/trace_one_loop.json) takes 48 base-model answers from runs/build/base whose verbatim loop starts at least 1,024 tokens in. It feeds each fixed token sequence through the base and edited models (teacher forcing) and records at every token:
- and : how hard the 64 neurons push along at layers 15 and 19.
- : how far the edited next-token probabilities moved from the base model's.
p_next: the probability of the actual next token under plain softmax at temperature 1.p_pick: the chance that vLLM's sampler picks that next token under the paper's decoding settings (presence penalty1.5on tokens already used in the answer, temperature0.6, top-k20, top-p0.95, applied in vLLM's order bywsp/protocol.pypick_prob).
Means across the 48 looping answers by stretch:
| Stretch of the answer | s15 |
s19 |
KL |
p_next base |
p_next edit |
p_pick base |
p_pick edit |
|---|---|---|---|---|---|---|---|
| Early (first half before the loop) | 0.096 |
0.116 |
0.021 |
0.768 (0.7675) |
0.750 |
0.834 |
0.821 |
| Late (last 512 tokens before the loop) | 0.178 |
0.241 |
0.038 |
0.862 |
0.825 |
0.904 |
0.881 |
| Inside the loop | 0.424 |
0.758 |
0.069 |
0.99975 |
0.946 |
0.99995 |
0.979 |
Multiplying per-token probabilities across one repeating block of length period gives the cycle odds, the chance of writing one more identical cycle:
| Scoring rule | Unedited, mean | Unedited, median | Edited, mean | Edited, median |
|---|---|---|---|---|
| Temperature 1, plain softmax | 0.992 |
0.995 |
0.205 |
0.097 |
| The paper's sampler | 0.999 |
0.999 |
0.487 |
0.625 |
- Under the paper's sampler and the edit,
23 of the 48loops have odds below0.5of completing one more cycle. - Loop periods range from
10 to 224tokens (median43.5). Longer loops break more easily: the correlation between and edited sampler cycle odds is-0.66(-0.657), because a small per-token cut (0.99995to0.979) compounds across a long cycle. - The trigger rises before the verbatim loop starts. Across the early, late, and in-loop stretches, climbs
0.10 -> 0.18 -> 0.42(0.096 -> 0.178 -> 0.424), climbs0.12 -> 0.24 -> 0.76(0.116 -> 0.241 -> 0.758), and climbs0.021 -> 0.038 -> 0.069. Even in the early stretch, averages0.12and KL averages0.021, so the edit is never fully off; part 4 checks answers that never looped. - In the traced loop (
trace_one_loop.json, plotted inlab6_real_loop.png), problemmath_train_intermediate_algebra_1137, answer0, the loop starts at token14,213of32,768with period15. At temperature 1, one-cycle odds drop from0.995(unedited) to0.64(0.6415, edited), and ten-cycle odds drop from0.988to0.013. Under the paper's sampler, one-cycle odds are0.997(unedited) versus0.991(edited), and ten-cycle odds are0.996versus0.970. The ten-cycle numbers average the chance of writing each 150-token window inside the loop unchanged. Because probabilities vary along the loop, that differs from raising the one-cycle odds to the tenth power (0.954,0.012,0.974and0.914). This 15-token loop survives the edit under the paper's sampler because low temperature (0.6) and top-p (0.95) keep the repeated token far ahead of the runner-up.


Part 3 only examined answers that eventually looped, so it could not tell us how behaves when reasoning goes well from start to finish. Part 4 (results/lab6/finished_answers.json) checks that, using all 26 answers in runs/build/base that closed </think> on their own and gave the right final number. These answers think for 750 to 9,698 tokens (median 3,770), followed by a written answer of 190 to 740 tokens after </think> (median about 320). Teacher forcing the 26 good answers through the unedited and edited models and comparing them with the 48 loops from part 3 gives these stretch means:
| Group and stretch | s15 |
s19 |
KL |
|---|---|---|---|
Good answers (26), early (first half of thinking) |
0.062 |
0.091 |
0.011 |
Good answers (26), late (last 512 tokens of thinking) |
0.079 |
0.116 |
0.017 |
Good answers (26), written answer after </think> |
0.066 |
0.108 |
0.007 |
Looping answers (48), early (first half before loop) |
0.096 |
0.116 |
0.021 |
Looping answers (48), late (last 512 tokens before loop) |
0.178 |
0.241 |
0.038 |
Looping answers (48), inside the loop |
0.424 |
0.758 |
0.069 |
- Small on good answers, but not zero. Across the three stretches of the
26good answers, averages0.091to0.116, about one seventh of the in-loop mean (0.758). Since has length1and , the push per token has length . At these average levels, that is about0.5to0.7at layer 19 on good answers versus4.5inside loops. Mean on good answers is0.007to0.017per token, about a tenth to a quarter of the in-loop KL (0.069). - Never at loop level. For each good answer, the script finds the hardest 64-token stretch of thinking (the highest 64-token moving average of ). Across the
26answers, this peak is0.082to0.316at layer 15 (median0.159) and0.147to0.465at layer 19 (median0.223). By comparison, the median loop averages0.427(layer 15) and0.754(layer 19) inside the loop, and even the weakest of the48loops averages0.374and0.671. Thus0 of the 26good answers have any 64-token stretch reaching even the weakest loop's in-loop mean. - Early overlap, late separation. The 25th / 50th / 75th percentiles of are
0.076 / 0.086 / 0.109(good early),0.078 / 0.105 / 0.123(good late), and0.082 / 0.099 / 0.122(good written), versus0.096 / 0.111 / 0.127(loop early),0.166 / 0.219 / 0.267(loop late), and0.719 / 0.754 / 0.785(loop inside). The two groups overlap early in thinking; in the late stretch, most loops sit above most good answers; inside the loop, there is a wide gap. The signal is a graded level: low on good reasoning, higher as an answer circles, and highest inside a loop. - What this comparison does not control for. The
26good answers come from18problems (8where both practice answers finished, and10where the other answer ran to the token limit). Because they are not matched to the problems behind the48loops, and problems that finish are likely easier than those that loop, problem difficulty can explain part of the gap.

The good answer closest to loop level is gsm8k_train_6504, answer 1 ("Bob bought 2 show dogs for $250.00 each to breed as a side business. The female just had a litter of 6 puppies. If he sells each puppy for $350.00, what is his total profit?", truth 1600). It thinks for 6,585 tokens (6,795 total). Its late stretch averages 0.299, above the loops' late median (0.219), and its peak 64-token window reaches 0.465 at layer 19 and 0.316 at layer 15. In its last 512 thinking tokens, the model circles through near-copies of the same check ("Wait, is it possible the "2 show dogs" are the items? Yes." / "Is it possible the "2 show dogs" are the number of puppies? No.") between restatements of 250 x 2 = 500, 350 x 6 = 2100, and 2100 - 500 = 1600. It then writes "Okay, I'll proceed.", closes </think>, and gives the right answer, 1600.00. Here rose on circling text even though the answer recovered.
Why it works
1. What down_proj computes on each token
A language model reads and writes text one token (word piece) at a time. Inside Qwen3.5-0.8B, each token carries a residual stream: a running list of 1,024 numbers that passes through 24 repeated blocks (or layers). Each block has a sequence mixer (standard attention at layers 3, 7, 11, 15, 19, and 23, and a linear DeltaNet mixer at the other 18 layers) followed by an MLP (multi-layer perceptron).
The MLP maps those 1,024 numbers to 3,584 intermediate neurons. Qwen's MLP is gated: it makes two different mixes of the stream, gate_proj and up_proj, passes the first through SiLU (a smooth cousin of that dips slightly below zero), and multiplies the two together, number by number. Because the up_proj number can have either sign, a neuron's activation can be positive or negative. An output matrix called down_proj, of shape , then writes a 1,024-number update back into the residual stream. Let be the neuron activation of neuron on token (how strongly it fires), and let be column of down_proj. The product sums the columns scaled by their activations:
Column is the 1,024-number vector that neuron adds to the residual stream per unit of activation.
2. Why editing columns equals subtracting
Let be the unit direction in the 1,024-number residual stream (a vector of length 1 pointing toward looping), be the 64 chosen neuron indices (), and be the edit strength. A rank-one matrix change modifies every selected column along the single direction . For each neuron in , the dot product measures how much column writes along . The weight edit subtracts times that projection from each column in and leaves the other 3,520 columns unchanged:
Multiply the edited matrix by the activations on token . Because summation is linear, we can pull and outside the sum over the 64 chosen neurons:
Here is one number per token:
In words, is the vector the 64 chosen neurons alone add to the residual stream on token , and is how much of that vector points along the loop direction . In wsp/hf.py, steer_down attaches a PyTorch hook (a function that intercepts a layer's output during the forward pass) to down_proj. It precomputes for the 64 neurons, computes on each token, and returns output - alpha * s(t) * u. The script gives the hook the released file's own , and 64 columns (from results/edit_info.json), so the hook and the weight edit are the same edit written two ways.
3. Why float64 is exact while float32 and bf16 show small rounding gaps
The four number formats in part 1 keep different numbers of bits of precision: fp64 has 53 bits, fp32 has 24, TF32 has 11, and bf16 has 8.
When part 1 runs layer 15 alone on CPU in fp64, y_edit and y_hook agree to 3.33e-16 on outputs of scale 0.65. That is 3 times 2^-53 (about 1.1e-16), where 2^-53 is one 64-bit rounding step for numbers between 0.5 and 1. The two paths differ only because they sum terms in a different order.
Across all 24 layers on GPU in fp32, the final logits (raw next-token scores before softmax turns them into probabilities) differ by at most 0.00774 (0.0077), with mean KL divergence (how far one probability distribution moved from another, where 0 is identical) of 1.41e-8 (1.4e-8). This gap comes mainly from the linear DeltaNet mixers, whose Triton GPU kernels default to TF32 (11-bit precision) for fp32 matrix multiplies. Small differences after layer 15 then pass through layers 16 to 23. Forcing full 24-bit IEEE fp32 in Triton (try-ieee below) cuts the max logit gap 5.8-fold, to 0.00134 (0.0013). Even with default TF32, the 0.00774 gap is about 1,500 times smaller than the edit's effect on the base model (11.4), and the 1.4e-8 mean KL is about 1.3 million times smaller than the edit's (0.0178).
The released qwen35-p50c file stores weights in bf16 (16-bit brain floating point, with 8 bits of precision, or about 3 significant decimal digits). Rounding to bf16 on disk introduces one rounding step of up to 0.00049 per weight entry (Lab 1). Against the released bf16 file, the bf16 hook on the base model has a max logit gap of 0.438 (0.4375), mean KL 0.000539 (5.4e-4), and the same top token on 99.4% of positions; the mean KL between the base model and the released file in bf16 (0.0184) is 34 times larger.
4. How static weights act like a token-dependent trigger
Adding a constant steering vector at layers 15 and 19 on every token would push the residual stream equally hard during clean algebra and during a loop. Because the weight edit multiplies by , the token itself sets the steering dose through the activations of the 64 chosen neurons.
Part 2 shows this on the 8 paired texts in labs/data/astra_pairs.json. Each pair shares its problem prompt and opening calculation before splitting into a productive check or a repeated sentence. In results/lab6/s_on_pairs.json, the per_token arrays match on the shared opening, apart from tiny bf16 rounding differences in the fourth pair, then stays near zero on the productive branch and climbs on the looping branch. Over the last 36 tokens of these short hand-picked texts, is near zero on the productive continuations and large on the loops; on long real answers in part 4, productive is higher, though still far below loop level.
Part 3 tests this trigger on long base-model answers (runs/build/base on build128). It selects 48 answers that were cut at the 32,768-token limit and end in a verbatim loop (the same token block repeated at least 3 times across at least 64 tokens, where one block's length is the period) starting after at least 1,024 tokens, to focus on answers that reasoned normally before getting stuck. Using teacher forcing, which feeds a fixed token sequence through the model instead of generating token by token, it runs each answer once with the edit hooks off and once with them on. That lets us line up every token position and compare , , p_next, and p_pick across the early answer, the last 512 tokens before the loop starts, and the tokens inside the loop. Part 4 runs the same check on the 26 base-model answers that closed </think> and got the right answer, where averages 0.09 to 0.12 per stretch and 0 of 26 reach even the weakest loop's in-loop mean (0.671) on any 64-token window.
Lesson: The weight edit and subtracting at layers 15 and 19 are the same computation. The text sets the dose through : small on ordinary reasoning (about one seventh of the in-loop mean at layer 19), rising as the model starts to circle, and largest inside a loop, where a small per-token cut adds up to a large chance of breaking the cycle. Because stays above zero on good answers, the edit still nudges them a little; Lab 7 measures that cost.
Check yourself
- In Lab 3, the released file's singular vector at layer 19 had the opposite sign from the mean-difference vector . Why does replacing with flip the sign of without changing the steering push or the weight edit ?
Answer
Both and appear once in . Replacing with turns into , and the two minus signs cancel in . In , multiplies its transpose , and .
- In part 1, the maximum
fp32logit difference between the hook and the in-memory edit is0.00774(0.0077). Which numbers inequivalence.jsonandtry-ieee/equivalence.jsonshow this comes from floating-point kernel rounding and not from a mistake in the hook formula?
Answer
Running layer 15's down_proj alone in fp64 on CPU, where no GPU kernel rounds intermediate sums, gives fp64_one_layer_max_abs_diff of 3.33e-16 on outputs of scale 0.65. Forcing full 24-bit IEEE fp32 in the DeltaNet Triton kernels (results/lab6/try-ieee/equivalence.json) cuts fp32_hook_vs_edit_max_abs_logit_diff 5.8-fold from 0.00774 to 0.00134. Across the full fp32 model, the edit itself shifts base logits by 11.4 (fp32_base_vs_edit_max_abs_logit_diff) with mean KL 0.0178, while the hook and in-memory edit differ by 0.00774 (1,500 times smaller) with mean KL 1.41e-8 (1.3 million times smaller).
- On the four productive texts at layer 15, mean over the last 36 tokens stays between
0.00and0.04, versus0.19,0.19,0.42, and0.25on the four looping texts. How does explain why the edit pushes much less on productive steps?
Answer
Since has length 1 and , the vector subtracted on token has length : at most about 0.24 (6 times 0.04) on the productive texts versus about 1.1 to 2.5 (6 times 0.19 to 6 times 0.42) on the looping texts. As the 64 neurons fire together in a loop, the push grows with against . Part 4 finds a smaller gap in the same direction on long real answers, so the push on good answers is small but not zero.
- Why use teacher forcing in part 3 instead of letting the edited model generate freely from the prompt?
Answer
Free generation diverges as soon as the edit changes one sampled token, leaving the base and edited models on different sentences. Teacher forcing feeds the same base-model loop into both models so we can compare , KL divergence, and loop-continuation odds at identical token positions.
- Part 4 finds that
0 of 26good answers have any 64-token stretch where reaches loop level. Why does that not show that the edit leaves good answers unchanged?
Answer
The push is proportional to , which on good answers is about one seventh of the in-loop mean (0.091 to 0.116 at layer 19 versus 0.758), not zero. That shifts next-token probabilities by a mean KL of 0.007 to 0.017 per token on good answers, about a tenth to a quarter of the in-loop KL (0.069). Across thousands of thinking tokens, those small shifts add up and can change which tokens get sampled; Lab 7 measures those side effects directly.
Try this next
Non-default flags write to results/lab6/try-<name>/ and leave the main result files and figures untouched.
-
Inspect where splits inside the 4 pairs. Run this one-liner on the VM or locally to check the shared openings (
75,85,70and69tokens) and post-split peaks inresults/lab6/s_on_pairs.json:bashpython3 -c 'import json; d=json.load(open("results/lab6/s_on_pairs.json")); k="per_token"; [print(f"L{L} pair {p}: shared diff={max(abs(a-b) for a,b in zip(d[L][k][2*p][:sh], d[L][k][2*p+1][:sh])):.4f}, loop peak={max(d[L][k][2*p+1]):.4f}") for L in ("15","19") for p,sh in enumerate((75,85,70,69))]'This prints
0.0000on the first three shared openings,0.0018(layer 15) and0.0042(layer 19) on the fourth frombf16rounding, and loop peaks of0.4558, 0.4713, 0.5897, 0.4873at layer 15 and0.3620, 0.4300, 0.7376, 0.6322at layer 19. -
Double the edit strength to with
--alpha-scale 2. The--alpha-scaleflag scales in both the hook and the in-memory edit (skipping thebf16file comparison because the file on disk was saved at ) and writesresults/lab6/try-alpha-scale-2/equivalence.json:bashCUDA_VISIBLE_DEVICES=7 python labs/lab6_steering.py --parts 1 --alpha-scale 2In
results/lab6/try-alpha-scale-2/equivalence.json, the hook and in-memory edit still agree (0.00916max logit diff, mean KL2.71e-8). Against the base model, the max logit diff rises to15.9(15.93) and mean KL rises to0.0745, or4.2times the KL (0.0178). Doubling a small change roughly quadruples the KL divergence, because KL grows like the square of a small change. -
Force full IEEE
fp32in the Triton DeltaNet kernels (try-ieee). Triton defaults to 11-bitTF32forfp32matrix multiplies on A100 Tensor Cores. PassTRITON_F32_DEFAULT=ieeewith a fresh cache directory to recompile in 24-bitfp32and writeresults/lab6/try-ieee/equivalence.json:bashTRITON_F32_DEFAULT=ieee TRITON_CACHE_DIR=/nvme/wsp-lab/tmp/triton-ieee \ CUDA_VISIBLE_DEVICES=7 python labs/lab6_steering.py --parts 1 --try-name ieeeThe max
fp32logit gap drops from0.00774to0.00134(0.0013),5.8times smaller, showing thatTF32rounding in the DeltaNet mixers caused most of thefp32gap. -
Run part 3 on 10 loops (
--max-samples 10) and redraw with less smoothing (--smooth 16). Passing--max-samples 10runs part 3 in about a minute on 10 loops and writesreal_loops.json,trace_one_loop.json, and both figures toresults/lab6/try-max-samples-10/:bashCUDA_VISIBLE_DEVICES=7 python labs/lab6_steering.py --parts 3 --max-samples 10Across those 10 loops, edited
p_pickinside the loop averages0.984, mean cycle odds at temperature 1 drop from0.991to0.150, and under the paper's sampler cycle odds drop from0.999to0.511. To redraw the 48-loop figure with a 16-token moving average instead of 64 (CPU only, writing toresults/lab6/try-smooth-16/):bashpython labs/lab6_steering.py --plot-only --smooth 16Comparing
results/lab6/try-smooth-16/lab6_real_loop.pngwithresults/figures/lab6_real_loop.pngshows the token-by-token oscillations inside each cycle that the 64-token window smooths out. -
Read the good answer that came closest to loop level. Sort
samplesinresults/lab6/finished_answers.jsonbymax_s19_64_tokens:bashpython3 -c 'import json; d=json.load(open("results/lab6/finished_answers.json")); [print(s["id"], s["j"], round(s["max_s19_64_tokens"], 3)) for s in sorted(d["samples"], key=lambda s: -s["max_s19_64_tokens"])[:3]]'This prints
gsm8k_train_6504 1 0.465,math_train_intermediate_algebra_605 0 0.353, andmath_train_prealgebra_141 1 0.31. Inspect the last 512 thinking tokens ofgsm8k_train_6504answer1(tokens6073to6585from the start of the answer, onsampler, CPU only):bashCUDA_VISIBLE_DEVICES="" python labs/peek.py build/base --id gsm8k_train_6504 --answer 1 --tokens 6073 6585The header
answer 1: 6,796 tokens, finishedshows the generator's token count (6,795when re-tokenized from saved text). On this stretch, where averages0.299(this answer's hardest 64-token window of thinking reaches0.465at layer 19 and0.316at layer 15), the model circles through the repeated check and arithmetic described in part 4 before closing</think>and giving the right answer1600.00. Does respond only to fatal infinite loops, or to circling text even when the answer recovers?