Skip to content

Commit fbea2d7

Browse files
author
Ronald Tse
committed
feat: recipe arms — headwise Muon + Sinkhorn embedding update
Two single-variable arms off the 2.1 recipe (run-007, 4.5701), DeepSeek-V4.1-Flash sec 2.5 / Alg 1: - ara-diac-small-2-1-hwmuon: head-wise Muon on Q/K (existing flag, first recipe run) -> run-014-hwmuon - ara-diac-small-2-1-skembed: sinkhorn_embed routes the 2D embedding-like tensors (tied table, prediction heads) from AdamW to SinkhornUpdate via the existing side-opt hook -> run-015-skembed muon.embed_named extracts the 2D embedding-like predicate so the selector and split_parameters share one classification (specs cover the MECE contract: exactly split's 2D AdamW set, never Muon's). Also fixes an engram-side routing bug: split_parameters handed _engram.table.weight to Muon while the side-opt stepped it too — double-stepped in run-013-engram. Tables are now excluded from muon_params (that run's FLAT verdict stands as measured; this corrects future engram arms).
1 parent 9ee5d21 commit fbea2d7

4 files changed

Lines changed: 133 additions & 16 deletions

File tree

‎src/gpu/distill_specs.yaml‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,53 @@ ara-diac-small-2-1-engram:
380380
mode: sequence
381381
optimizer: muon
382382
note: TODO.impl/10 lexical-memory rung; gate vs 2.1 (4.5701)
383+
ara-diac-small-2-1-hwmuon:
384+
# TODO.impl/03 recipe arm: the 2.1 recipe (run-007, 4.5701) plus
385+
# head-wise Muon on the Q/K projections (DeepSeek V4.1 Flash sec
386+
# 2.5; GLM-5 and Kimi-K3 validate the same split). Single variable.
387+
teacher: rababa_arabic_byt5/run-007-news/best
388+
teacher_volume: rababa
389+
out_volume: rababa
390+
student_init: google/byt5-small
391+
train: r5-units/domain.txt
392+
train_extra:
393+
- r5-units/replay.txt
394+
unit_limits:
395+
- 24000
396+
- 6000
397+
max_len: 1450
398+
label_beams: '1'
399+
out: rababa_arabic_distill_small/run-014-hwmuon
400+
labels_file: teacher_labels_r7.jsonl
401+
labels_complete: 'true'
402+
mode: sequence
403+
optimizer: muon
404+
headwise_muon: 'true'
405+
note: TODO.impl/03 recipe arm; gate vs 2.1 (4.5701)
406+
ara-diac-small-2-1-skembed:
407+
# TODO.impl/06 recipe arm: the 2.1 recipe plus the Sinkhorn-balanced
408+
# embedding update (Alg. 1) on the tied table / prediction head in
409+
# place of AdamW. Single variable vs run-007.
410+
teacher: rababa_arabic_byt5/run-007-news/best
411+
teacher_volume: rababa
412+
out_volume: rababa
413+
student_init: google/byt5-small
414+
train: r5-units/domain.txt
415+
train_extra:
416+
- r5-units/replay.txt
417+
unit_limits:
418+
- 24000
419+
- 6000
420+
max_len: 1450
421+
label_beams: '1'
422+
out: rababa_arabic_distill_small/run-015-skembed
423+
labels_file: teacher_labels_r7.jsonl
424+
labels_complete: 'true'
425+
mode: sequence
426+
optimizer: muon
427+
sinkhorn_embed: 'true'
428+
sinkhorn_lr: '0.00026'
429+
note: TODO.impl/06 recipe arm; gate vs 2.1 (4.5701)
383430
ara-diac-tiny-max:
384431
# THE gapless title test: the 30M class with the campaign's FULL
385432
# lever set — full corpus (24k+6k, not the 12k subset the collapse

‎src/gpu/modal_distill.py‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,31 +1110,45 @@ def __getitem__(self, i):
11101110

11111111
tables, projs = engram_param_split(student)
11121112
sinkhorn_tables = tables
1113-
proj_ids = {id(p) for p in projs}
1114-
muon_params = [p for p in muon_params if id(p) not in proj_ids]
1113+
# the table rides SinkhornUpdate only — split_parameters
1114+
# would otherwise also hand it to Muon (double-stepped;
1115+
# observed in the run-013-engram wiring)
1116+
side_ids = {id(p) for p in tables + projs}
1117+
muon_params = [p for p in muon_params if id(p) not in side_ids]
11151118
headwise = []
11161119
if spec.get("headwise_muon"):
11171120
from gpu.muon import qk_named
11181121

11191122
headwise = [p for _, p in qk_named(named)]
11201123
headwise_ids = {id(p) for p in headwise}
11211124
muon_params = [p for p in muon_params if id(p) not in headwise_ids]
1125+
sinkhorn_embed = []
1126+
if spec.get("sinkhorn_embed"):
1127+
from gpu.muon import embed_named
1128+
1129+
sinkhorn_embed = [p for _, p in embed_named(named)]
1130+
embed_ids = {id(p) for p in sinkhorn_embed}
1131+
adamw_params = [p for p in adamw_params if id(p) not in embed_ids]
11221132
optimizer = Muon(
11231133
muon_params, lr=float(spec.get("muon_lr", 0.01)),
11241134
momentum=0.95, weight_decay=0.01,
11251135
)
11261136
if headwise:
11271137
heads = int(spec.get("student_config", {}).get("num_heads", 6))
11281138
optimizer.add_headwise_group(headwise, heads=heads)
1129-
if sinkhorn_tables:
1139+
if sinkhorn_tables or sinkhorn_embed:
11301140
from gpu.sinkhorn_update import SinkhornUpdate
11311141

1132-
table_opt = SinkhornUpdate(sinkhorn_tables, lr=float(spec.get("engram_lr", 5e-4)))
1142+
table_opt = SinkhornUpdate(
1143+
sinkhorn_tables + sinkhorn_embed,
1144+
lr=float(spec.get("sinkhorn_lr", spec.get("engram_lr", 2.6e-4))),
1145+
)
11331146
optimizer._engram_table_opt = table_opt # stepped alongside
11341147
optimizer.add_adamw_group(adamw_params, lr=1e-4, weight_decay=0.0)
11351148
print(
11361149
f"[{spec_id}] muon: {len(muon_params)} matrix / "
11371150
f"{len(headwise)} headwise q/k / "
1151+
f"{len(sinkhorn_embed)} sinkhorn-embed / "
11381152
f"{len(adamw_params)} embedding-like params",
11391153
flush=True,
11401154
)

‎src/gpu/muon.py‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,30 @@ def _adamw_step(self, group) -> None:
153153
p.addcdiv_(exp_avg / bias_c1, denom, value=-group["lr"])
154154

155155

156+
def _is_embedding_like(name: str, p) -> bool:
157+
return (
158+
p.ndim < 2
159+
or "embed_tokens" in name
160+
or name == "shared.weight" # T5 tied byte embedding (transformers 5.x)
161+
or "lm_head" in name
162+
or "relative_attention" in name
163+
or "memory.values" in name
164+
or "memory.k1" in name
165+
or "memory.k2" in name
166+
)
167+
168+
169+
def embed_named(named_params):
170+
"""The 2D embedding-like tensors (tied embedding, prediction heads,
171+
memory tables) — SinkhornUpdate's surface: rows are token identity,
172+
columns features. 1D params (layer norms) are excluded; they have
173+
no row/column structure to balance."""
174+
return [
175+
(name, p) for name, p in named_params
176+
if p.requires_grad and p.ndim >= 2 and _is_embedding_like(name, p)
177+
]
178+
179+
156180
def split_parameters(named_params):
157181
"""The standard split: orthogonalizable 2D hidden weights vs
158182
embedding-like tensors (1D params, embeddings, tied head, relative
@@ -161,17 +185,7 @@ def split_parameters(named_params):
161185
for name, p in named_params:
162186
if not p.requires_grad:
163187
continue
164-
embedding_like = (
165-
p.ndim < 2
166-
or "embed_tokens" in name
167-
or name == "shared.weight" # T5 tied byte embedding (transformers 5.x)
168-
or "lm_head" in name
169-
or "relative_attention" in name
170-
or "memory.values" in name
171-
or "memory.k1" in name
172-
or "memory.k2" in name
173-
)
174-
(adamw if embedding_like else muon).append(p)
188+
(adamw if _is_embedding_like(name, p) else muon).append(p)
175189
return muon, adamw
176190

177191

‎tests/test_muon_headwise.py‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
torch = pytest.importorskip("torch")
1616

17-
from gpu.muon import Muon, qk_named, split_parameters # noqa: E402
17+
from gpu.muon import Muon, embed_named, qk_named, split_parameters # noqa: E402
1818

1919

2020
def _grad_like(shape, seed):
@@ -121,3 +121,45 @@ def test_split_parameters_unchanged_when_headwise_unused() -> None:
121121
muon, adamw = split_parameters(named)
122122
assert [p.shape for p in muon] == [torch.Size([4, 3])]
123123
assert [p.shape for p in adamw] == [torch.Size([3, 4]), torch.Size([3])]
124+
125+
126+
def test_embed_named_selects_2d_embedding_like_only() -> None:
127+
"""The Sinkhorn surface: 2D tensors with token-row structure (tied
128+
embedding, prediction heads). 1D layer norms stay with AdamW; 2D
129+
hidden matrices are not embedding-like."""
130+
named = [
131+
("shared.weight", torch.zeros(259, 4, requires_grad=True)),
132+
("lm_head.weight", torch.zeros(259, 4, requires_grad=True)),
133+
("encoder.block.0.layer.0.layer_norm.weight",
134+
torch.zeros(4, requires_grad=True)),
135+
("encoder.block.0.layer.1.DenseReluDense.wi_0.weight",
136+
torch.zeros(8, 4, requires_grad=True)),
137+
("encoder.embed_tokens.weight", torch.zeros(259, 4, requires_grad=True)),
138+
]
139+
assert {n for n, _ in embed_named(named)} == {
140+
"shared.weight", "lm_head.weight", "encoder.embed_tokens.weight",
141+
}
142+
143+
144+
def test_embed_named_agrees_with_split_parameters() -> None:
145+
"""MECE: every 2D param split_parameters routes to AdamW that
146+
embed_named claims must be exactly embed_named's set — rerouting
147+
them cannot strand or double-count a tensor."""
148+
named = [
149+
("shared.weight", torch.zeros(259, 4, requires_grad=True)),
150+
("decoder.lm_head.weight", torch.zeros(259, 4, requires_grad=True)),
151+
("encoder.block.0.layer.0.layer_norm.weight",
152+
torch.zeros(4, requires_grad=True)),
153+
("encoder.block.0.layer.1.DenseReluDense.wi_0.weight",
154+
torch.zeros(8, 4, requires_grad=True)),
155+
("encoder.block.0.layer.0.SelfAttention.q.weight",
156+
torch.zeros(4, 4, requires_grad=True)),
157+
("memory.values", torch.zeros(8, 4, requires_grad=True)),
158+
("encoder.block.0.layer.0.EncDecAttention.relative_attention_bias.weight",
159+
torch.zeros(8, 4, requires_grad=True)),
160+
]
161+
muon, adamw = split_parameters(named)
162+
claimed = {id(p) for _, p in embed_named(named)}
163+
adamw_2d = [p for p in adamw if p.ndim == 2]
164+
assert {id(p) for p in adamw_2d} == claimed
165+
assert not (claimed & {id(p) for p in muon})

0 commit comments

Comments
 (0)