Skip to content

Commit a7fe5d1

Browse files
author
Ronald Tse
committed
feat(gkd): on-policy distillation wiring + spec (armed, launch gated)
gpu/gkd.py: pure-torch helpers (anneal schedule, token logprobs, reverse-KL with student-only gradients; 7 unit tests). distill_sequence wiring: teacher kept resident for scoring, student samples every sample_every steps (sub-batch, capped), reverse-KL at the annealed weight — control-verbatim otherwise. Spec ara-diac-small-2-gkd per the EXPERIMENTS registration. Fixes a pre-existing B905 in scripts/figures/frontier.py (main-red).
1 parent 95bf0a2 commit a7fe5d1

5 files changed

Lines changed: 171 additions & 1 deletion

File tree

‎scripts/figures/frontier.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def main() -> None:
6262
pts = [(r[1], r[2]) for r in RUNS if r[2] is not None]
6363
labels = [r[0] for r in RUNS if r[2] is not None]
6464
ax.plot([p[0] for p in pts], [p[1] for p in pts], "o-")
65-
for (x, y), lab in zip(pts, labels):
65+
for (x, y), lab in zip(pts, labels, strict=True):
6666
ax.annotate(lab, (x, y), fontsize=7, xytext=(4, 4),
6767
textcoords="offset points")
6868
ax.set_xscale("log")

‎src/gpu/distill_specs.yaml‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,33 @@ ara-diac-small-2:
180180
labels_file: teacher_labels_r7.jsonl
181181
mode: sequence
182182
note: 'r7 teacher + Muon; E4 gate: beat the shipped 8.259 by >= 2pp'
183+
ara-diac-small-2-gkd:
184+
# GKD rung (EXPERIMENTS.md "GKD — on-policy distillation rung"):
185+
# control run-006 verbatim; delta = reverse-KL on student-sampled
186+
# sequences scored by the frozen r7 teacher, ratio 0.3, annealed to
187+
# zero over the final third. Gate <= 4.5218; prediction 4.30-4.65.
188+
teacher: rababa_arabic_byt5/run-007-news/best
189+
teacher_volume: rababa
190+
student_init: google/byt5-small
191+
optimizer: muon
192+
muon_lr: '0.01'
193+
train: r5-units/domain.txt
194+
train_extra:
195+
- r5-units/replay.txt
196+
unit_limits:
197+
- 24000
198+
- 6000
199+
max_len: 1450
200+
label_beams: '1'
201+
out: rababa_arabic_distill_small/run-011-r7-muon-gkd
202+
labels_file: teacher_labels_r7.jsonl
203+
mode: sequence
204+
gkd:
205+
ratio: 0.3
206+
temperature: 1.0
207+
sample_every: 4
208+
sample_sub: 2
209+
sample_cap: 1024
183210
ara-diac-small-layerdrop:
184211
teacher: rababa_arabic_byt5/run-006-morph/best
185212
teacher_volume: rababa

‎src/gpu/gkd.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""On-policy GKD helpers (EXPERIMENTS.md "GKD — on-policy distillation
2+
rung"). Pure torch, no modal import — unit-testable outside CI's GPU
3+
image. The training-side wiring lives in gpu.modal_distill."""
4+
5+
from __future__ import annotations
6+
7+
import torch
8+
import torch.nn.functional as F
9+
10+
11+
def gkd_weight(step: int, total_steps: int, ratio: float,
12+
anneal_from: float = 2 / 3) -> float:
13+
"""Full ratio until `anneal_from` of training, then linear to zero
14+
at total_steps (the registered anneal over the final third)."""
15+
if total_steps <= 0 or step >= total_steps:
16+
return 0.0
17+
frac = step / total_steps
18+
if frac < anneal_from:
19+
return ratio
20+
return ratio * max(0.0, 1.0 - (frac - anneal_from) / (1.0 - anneal_from))
21+
22+
23+
def token_logprobs(logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
24+
"""Per-token log-probs of `targets` under `logits` (teacher-forced
25+
shift): logits [B, L, V], targets [B, L] -> [B, L-1]."""
26+
lp = F.log_softmax(logits[:, :-1].float(), dim=-1)
27+
return lp.gather(-1, targets[:, 1:].unsqueeze(-1)).squeeze(-1)
28+
29+
30+
def reverse_kl(student_logits: torch.Tensor, teacher_logits: torch.Tensor,
31+
targets: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
32+
"""KL(student || teacher) on the student's own sampled tokens:
33+
mean over (masked) positions of logp_s - logp_t. Gradient flows
34+
through the student term only — call under no_grad for teacher."""
35+
s_lp = token_logprobs(student_logits, targets)
36+
with torch.no_grad():
37+
t_lp = token_logprobs(teacher_logits, targets)
38+
diff = s_lp - t_lp
39+
if mask is not None:
40+
m = mask[:, 1:].to(diff.dtype)
41+
return (diff * m).sum() / m.sum().clamp(min=1.0)
42+
return diff.mean()

‎src/gpu/modal_distill.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,15 @@ def distill_sequence(spec_id: str, epochs: int = 3) -> dict:
751751
mtp_head = build_mtp(student, steps=int(mtp_cfg.get("steps", 3)))
752752
n = sum(q.numel() for q in mtp_head.parameters()) / 1e6
753753
print(f"[{spec_id}] mtp_aux head: {n:.2f}M params", flush=True)
754+
gkd_cfg = spec.get("gkd")
755+
if gkd_cfg:
756+
print(
757+
f"[{spec_id}] gkd: ratio={gkd_cfg.get('ratio', 0.3)} "
758+
f"temp={gkd_cfg.get('temperature', 1.0)} "
759+
f"every={gkd_cfg.get('sample_every', 4)} "
760+
f"sub={gkd_cfg.get('sample_sub', 2)} cap={gkd_cfg.get('sample_cap', 1024)}",
761+
flush=True,
762+
)
754763
student.train()
755764

756765
class Pairs(Dataset):
@@ -1033,6 +1042,12 @@ def accept_label(src: str, label: str) -> None:
10331042
student.gradient_checkpointing_enable()
10341043
if mtp_head is not None: # built while student was still on cpu
10351044
mtp_head.to("cuda")
1045+
if gkd_cfg is not None: # on-policy scoring needs the teacher resident
1046+
teacher.to("cuda")
1047+
teacher.eval()
1048+
_ensure_src_path()
1049+
from gpu.gkd import gkd_weight, reverse_kl
1050+
print(f"[{spec_id}] gkd: teacher resident on cuda for scoring", flush=True)
10361051

10371052
class TeacherPairs(Dataset):
10381053
def __len__(self):
@@ -1131,6 +1146,32 @@ def _usable(ck: Path) -> bool:
11311146
loss = s_out.loss + beta * aux
11321147
else:
11331148
loss = student(input_ids=ids, attention_mask=am, labels=labels).loss
1149+
if (
1150+
gkd_cfg is not None
1151+
and step % int(gkd_cfg.get("sample_every", 4)) == 0
1152+
):
1153+
w = gkd_weight(step, total_steps, float(gkd_cfg.get("ratio", 0.3)))
1154+
if w > 0:
1155+
sub = int(gkd_cfg.get("sample_sub", 2))
1156+
with torch.no_grad():
1157+
gen = student.generate(
1158+
input_ids=ids[:sub], attention_mask=am[:sub],
1159+
do_sample=True,
1160+
temperature=float(gkd_cfg.get("temperature", 1.0)),
1161+
max_new_tokens=int(gkd_cfg.get("sample_cap", 1024)),
1162+
)
1163+
# gen is the decoder-side sample; score both models on
1164+
# (prompt -> sample). Skip the decoder-start token.
1165+
cont_mask = torch.zeros_like(gen, dtype=torch.bool)
1166+
cont_mask[:, 1:] = True
1167+
s_logits = student(
1168+
input_ids=ids[:sub], attention_mask=am[:sub], labels=gen
1169+
).logits
1170+
with torch.no_grad():
1171+
t_logits = teacher(
1172+
input_ids=ids[:sub], attention_mask=am[:sub], labels=gen
1173+
).logits
1174+
loss = loss + w * reverse_kl(s_logits, t_logits, gen, cont_mask)
11341175
loss.backward()
11351176
torch.nn.utils.clip_grad_norm_(student.parameters(), 1.0)
11361177
optimizer.step()

‎tests/test_gkd.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import pytest
2+
3+
torch = pytest.importorskip("torch")
4+
5+
from gpu.gkd import gkd_weight, reverse_kl, token_logprobs # noqa: E402
6+
7+
8+
class TestGkdWeight:
9+
def test_full_ratio_before_anneal(self):
10+
assert gkd_weight(0, 300, 0.3) == 0.3
11+
assert gkd_weight(199, 300, 0.3) == 0.3
12+
13+
def test_linear_anneal_over_final_third(self):
14+
assert gkd_weight(200, 300, 0.3) == pytest.approx(0.3) # anneal starts at full
15+
assert gkd_weight(250, 300, 0.3) == pytest.approx(0.15)
16+
assert gkd_weight(299, 300, 0.3) == pytest.approx(0.003)
17+
18+
def test_zero_at_end_and_degenerate(self):
19+
assert gkd_weight(300, 300, 0.3) == 0.0
20+
assert gkd_weight(0, 0, 0.3) == 0.0
21+
22+
23+
class TestTokenLogprobs:
24+
def test_shift_and_gather(self):
25+
torch.manual_seed(0)
26+
logits = torch.randn(2, 5, 7)
27+
targets = torch.randint(0, 7, (2, 5))
28+
lp = token_logprobs(logits, targets)
29+
assert lp.shape == (2, 4)
30+
ref = torch.log_softmax(logits[0, :-1].float(), -1)
31+
expect = ref.gather(-1, targets[0, 1:].unsqueeze(-1)).squeeze(-1)
32+
assert torch.allclose(lp[0], expect)
33+
34+
35+
class TestReverseKl:
36+
def test_identical_distributions_zero(self):
37+
torch.manual_seed(0)
38+
logits = torch.randn(2, 6, 7)
39+
targets = torch.randint(0, 7, (2, 6))
40+
assert reverse_kl(logits, logits.clone(), targets).abs() < 1e-6
41+
42+
def test_mask_restricts_positions(self):
43+
torch.manual_seed(0)
44+
s = torch.randn(1, 6, 7)
45+
t = torch.randn(1, 6, 7)
46+
targets = torch.randint(0, 7, (1, 6))
47+
mask = torch.zeros(1, 6, dtype=torch.bool)
48+
mask[:, 4:] = True # only the continuation region (positions >= 4)
49+
masked = reverse_kl(s, t, targets, mask)
50+
full = reverse_kl(s, t, targets)
51+
assert not torch.allclose(masked, full)
52+
53+
def test_gradient_flows_through_student_only(self):
54+
s = torch.randn(1, 4, 5, requires_grad=True)
55+
t = torch.randn(1, 4, 5, requires_grad=True)
56+
targets = torch.randint(0, 5, (1, 4))
57+
loss = reverse_kl(s, t, targets)
58+
loss.backward()
59+
assert s.grad is not None
60+
assert t.grad is None

0 commit comments

Comments
 (0)