Skip to content

New Record: Partition Residual Dims and Symmetric Value Embeddings (-0.7s)#194

Closed
photomz wants to merge 4 commits into
KellerJordan:masterfrom
photomz:release
Closed

New Record: Partition Residual Dims and Symmetric Value Embeddings (-0.7s)#194
photomz wants to merge 4 commits into
KellerJordan:masterfrom
photomz:release

Conversation

@photomz

@photomz photomz commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

Updates on PR#190:

  • Let gates operate on different chunks of the residual stream
  • Reuse 0th value embedding in U-Net pattern

I can retime for PR#191 later.

Update: Retiming on PR#191 holds up on 15 steps and -0.7s improvement exactly.

Gates

Currently, we have $4$ very distinct gates: smear, skip, attention, and value embedding. For initial embedding $x_t$ at token $t$, a smear gate shifts $x_{t-1}$, and assigns $x_t := x_t + g \times x_{t-1}$. The smear is a cheap proxy for the token shift that attention heads have already been observed to learn at higher cost. A skip gate enables a standard skip connection, and an attention gate controls the standard attention before MLP. Finally, a value embedding adds extra sparse embeddings that are mixed into the value of attention layers.

Each gate serves distinct roles; by the induction heads hypothesis, separate concerns live on separate subspaces of the residual vector. However, the WR had designed the gate to be a function of the first $c = 12$ dimensions, namely
$$g(x) = \sigma(w_g \cdot x_{1:c}), \quad w_g: \mathbb{R}^c$$
which can "crowd" the residual stream with noise. To cleanly separate concerns, we modify gates to operate on distinct chunks of $x$ (with minor overlap).

If gates are "similar" enough in function, we find that some dim overlap can boost the learning signal, especially in early training. We arrived on this configuration below. Although we could grid search the ideal overlap, the main idea is to reduce the pressure on the first 12 dimensions in the residual stream, so this is a reasonable starting point.

smear_dims = (0, 12)
skip_dims = (6, 18)
attn_dims = (18, 30)
ve_dims = (18, 30)

Value Embeddings

After pruning some layers in prior WRs, the placement of the value embeddings became $W_v \in \mathbb{R}^{v \times d}$, for embedding size $d = 768$ and vocabulary $v = 50257$. These embeddings add token-specific information to enrich the block output. We applied this method in a U-Net style over the $11$ Transformer blocks, namely

[.,1,2,...,0,1,2]

where shared indices share $W_v$. However, we noticed the 0th embedding was only used once, wasting a very large matrix, so we modified the pattern to be symmetric like

[0,1,2,...0,1,2]

Each embedding matrix is $v \times d = 50257 \times 768 \approx 38.6$ million parameters, and we want to ensure huge parameters are used efficiently.

Timing

Retiming record of PR#190 at 1840 steps

import scipy.stats
import torch

# timed at 1840 steps

losses = [3.279107093811035, 3.277017831802368, 3.2793996334075928, 3.2764358520507812, 3.2769775390625]
times = [112789.1205839951, 113037.54114699404, 113017.72899700882, 112976.08575701088, 112929.27418399995]

print("p=%.4f" % scipy.stats.ttest_1samp(losses, 3.28, alternative="less").pvalue)
# p=0.0110

print("losses:", torch.std_mean(torch.tensor(losses)))
# losses: (tensor(0.0014), tensor(3.2778))

print("time:", torch.std_mean(torch.tensor(times)))
# time: (tensor(99.0496), tensor(112949.9453))

New record timing at 1825 steps

import scipy.stats
import torch

# timed at 1825 steps

losses = [3.2781171798706055, 3.2748520374298096, 3.279273271560669, 3.2784266471862793, 3.2790513038635254, 3.2811760902404785, 3.279404640197754, 3.279543876647949, 3.2793352603912354, 3.2784993648529053, 3.2771427631378174, 3.2776403427124023, 3.278602123260498, 3.276858329772949, 3.2774949073791504, 3.277669668197632, 3.275831937789917, 3.27740216255188, 3.2751529216766357, 3.278644323348999, 3.2792952060699463, 3.280611276626587, 3.277050256729126, 3.2816121578216553, 3.2805702686309814] 
times = [111995.66340900128, 112302.4280989921, 112244.41819799904, 112321.48777899783, 112167.75724300896, 112261.21659600176, 112135.15866100352, 112225.81498399813, 112338.56836100313, 112230.02111700043, 112104.64710800079, 112202.92374401106, 112226.64850599904, 112347.86479799732, 112235.14115900252, 112221.45126000032, 112160.53612099495, 112237.91594400245, 112309.55856299624, 112266.23290100542, 112132.67970199377, 112144.1444540178, 112117.29600099716, 112303.44185298964, 112176.70059901138]

print("p=%.4f" % scipy.stats.ttest_1samp(losses, 3.28, alternative="less").pvalue)
# p=0.0000

print("losses:", torch.std_mean(torch.tensor(losses)))
# losses: (tensor(0.0017), tensor(3.2784))

print("time:", torch.std_mean(torch.tensor(times)))
# time: (tensor(84.2895), tensor(112216.3906))

If no changes, will merge at latest WR - improvement = 109.2s-(0.7s) = 108.5s to be consistent with 0.7s improvement.

@photomz photomz changed the title New Record: Even Load of Residual Dims and Value Embeddings (-0.7s) New Record: Partition Residual Dims and Symmetric Value Embeddings (-0.7s) Jan 8, 2026
@ClassicLarry

Copy link
Copy Markdown
Collaborator

Cool, hopefully this holds up when reran on the latest PR. Can you please drop the pushover file, and make sure train.py includes the updated step count?

Been hoping for a while that someone could get the value embedding to work, maybe the addition of the value embed gates is what finally made it worthwhile, as it was not worthwhile in the past for some odd reason. When I added the ve_gates I did one test run with them on the last dimensions and got worse results so I kept them on the first 12. So that one surprises me a bit, glad you were able to get it to work. At the time the gates were in Muon and had much more volatile behavior... perhaps now that they are in Adam and not monopolizing the residual stream dimensions completely, they function better spread out, not sure. Splitting the dims with the skip gate is an interesting idea.

@photomz

photomz commented Jan 9, 2026

Copy link
Copy Markdown
Contributor Author

I reran on #191 and confirmed the 15 step difference holds up.

Retiming

Condition Step p-value (H₁: loss < 3.28) Loss (mean ± sd) Time (mean ± sd)
Treatment 1785 0.0024 3.2770 ± 0.0012 109,681.1 ± 110.0
Control 1800 0.0012 3.2755 ± 0.0015 110,466.95 ± 61.63

Treatment

# Treatment at 1745+40 (5x), timed at 1785 steps

import scipy.stats
import torch

losses = [
    3.277315139770508,
    3.276496648788452,
    3.2764573097229004,
    3.2788822650909424,
    3.27580189704895,
]

times = [
    109531.29593900303,
    109799.04070798875,
    109788.34327799996,
    109647.2150749978,
    109639.68391300295,
]

print("p=%.4f" % scipy.stats.ttest_1samp(losses, 3.28, alternative="less").pvalue)
# p=0.0024

print("losses:", torch.std_mean(torch.tensor(losses)))
# losses: (tensor(0.0012), tensor(3.2770))

print("time:", torch.std_mean(torch.tensor(times)))
# time: (tensor(110.0), tensor(109681.1))

Control

# Control at 1760+40 (5x), timed at 1800 steps

import scipy.stats
import torch

losses = [
    3.277698040008545,
    3.2759218215942383,
    3.2743494510650635,
    3.2755558490753174,
    3.2739055156707764,
]

times = [
    110410.41018200485,
    110524.66757500588,
    110523.31244999732,
    110481.97357999743,
    110394.39703700191,
]

print("p=%.4f" % scipy.stats.ttest_1samp(losses, 3.28, alternative="less").pvalue)
# p=0.0012

print("losses:", torch.std_mean(torch.tensor(losses)))
# losses: (tensor(0.0015), tensor(3.2755))

print("time:", torch.std_mean(torch.tensor(times)))
# time: (tensor(61.6309), tensor(110466.9531))

My new machine took 20 more steps than reported by the prior WR, which is rather large, but it was consistent over all runs so I'll attribute it to inter-machine variance. Hence I updated the new steps to prior WR - change = 1775 - 15 = 1760 steps total.

@photomz

photomz commented Jan 9, 2026

Copy link
Copy Markdown
Contributor Author

When I added the ve_gates I did one test run with them on the last dimensions and got worse results so I kept them on the first 12.

@ClassicLarry My intuition is that these gates function slightly better when their inputs are more "spread out", i.e. they have the chance to learn both shared and independent features. If there weren't enough gates, I believe you that moving the ve_gates to the end probably wouldn't have done anything.

I didn't probably ablate what the "ideal overlap" is of shared/independent features, albeit I don't think there's much gains left in optimizing this.

@ClassicLarry

Copy link
Copy Markdown
Collaborator

I’ll do some validation runs and ablations later. It’s not yet clear this is a new record since the step count is increased from 1775 and the loss is higher. Different machines shouldn’t cause a step count change, just a change in runtime.

@photomz

photomz commented Jan 9, 2026

Copy link
Copy Markdown
Contributor Author

Fair point, I overrode the default harness to make running repeats easier -- I ran repeats as a for-loop over the Python train script, reinitializing the model every time. It's possible that changed the baseline architecture for non-obvious reasons. Thanks for checking, I'll try retiming on a fresh machine on my end too.

@ClassicLarry

ClassicLarry commented Jan 10, 2026

Copy link
Copy Markdown
Collaborator

Some runs:
this PR: [3.2786, 3.2789, 3.2784, 3.2759]
remove gate dims change: [3.2775, 3.2780, 3.2775]
baseline drop 12 steps to match time: [3.2793, 3.2787, 3.2793]

I dont think the gate update is doing anything. Its easy to see a drop from 3.28 to 3.276 for a single run and mistake noise for an improvement. The value embed update looks to be worthwhile, and makes the model arch more symmetric and simpler. I'll merge once you can show p value for the updated step count, for any combination of updates that works.

For repeat runs you can do something like for i in {1..10}; do torchrun --standalone --nproc_per_node=8 train_gpt.py; done.

Not looking to merge in the harness code.

If this doesn't quite get p value at 15 steps (I think it will), you can probably add mantissa to Adam or turn more short windows into PairedHeadLayers to get over the threshold.

photomz and others added 4 commits January 25, 2026 20:56
* PI config; Trying residual dims separation

* Profiler

* value embed is symmetric

* Agg treatment before flight, ~ -20steps

* Easy trials

* add WR record; better trial

* Cleanup for push

* More cleanup

* more

---------

Co-authored-by: Ubuntu <[email protected]>
Co-authored-by: Dummy Name <[email protected]>
Co-authored-by: dummy user <email>
@photomz photomz closed this by deleting the head repository Jan 26, 2026
@photomz

photomz commented Jan 26, 2026

Copy link
Copy Markdown
Contributor Author

@ClassicLarry Thanks for timing my ideas. Unfortunately, after the other WRs in between these 2 weeks, this idea isn't significant anymore. I did further testing and found a new improvement.

This is superceded by #209

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants