Skip to content

Commit 2836d60

Browse files
author
Ronald Tse
committed
fix(distill): distill_sequence was never inserted (heredoc silently failed)
1 parent 9692ef3 commit 2836d60

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

‎src/gpu/modal_distill.py‎

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,196 @@ def metrics(model) -> dict:
314314
return {"teacher": metrics(teacher), "student": metrics(student)}
315315

316316

317+
318+
319+
@app.function(
320+
gpu="A10G",
321+
cpu=8,
322+
memory=32 * 1024,
323+
timeout=5 * 3600,
324+
volumes={
325+
"/datasets": DATASETS,
326+
"/checkpoints": CHECKPOINTS,
327+
"/secryst-checkpoints": SECRYST_CHECKPOINTS,
328+
"/secryst-datasets": SECRYST_DATASETS,
329+
"/persian-checkpoints": PERSIAN_CHECKPOINTS,
330+
},
331+
)
332+
def distill_sequence(spec_id: str, epochs: int = 3) -> dict:
333+
"""Cross-tokenizer sequence-level distillation: teacher generates
334+
labels with ITS tokenizer, student trains CE on those labels with
335+
the byte tokenizer. The only sound approach when teacher and student
336+
occupy different vocab spaces (umt5 sentencepiece vs ByT5 bytes)."""
337+
import json
338+
from pathlib import Path
339+
340+
import torch
341+
from torch.utils.data import DataLoader, Dataset
342+
from transformers import (
343+
AutoModelForSeq2SeqLM,
344+
AutoTokenizer,
345+
get_cosine_schedule_with_warmup,
346+
)
347+
348+
spec = SPECS[spec_id]
349+
teacher_vol = spec.get("teacher_volume", "rababa")
350+
351+
vol_map = {
352+
"rababa": "/checkpoints",
353+
"secryst": "/secryst-checkpoints",
354+
"persian": "/persian-checkpoints",
355+
}
356+
teacher_root = vol_map[teacher_vol]
357+
teacher_path = Path(teacher_root) / spec["teacher"]
358+
359+
data_vol = "/secryst-datasets" if teacher_vol == "secryst" else "/datasets"
360+
train_path = Path(data_vol) / spec["train"]
361+
val_path = Path(data_vol) / spec["val"]
362+
363+
# Teacher: use its OWN tokenizer (sentencepiece for umt5)
364+
teacher_tok = AutoTokenizer.from_pretrained(str(teacher_path))
365+
teacher = (
366+
AutoModelForSeq2SeqLM.from_pretrained(str(teacher_path))
367+
.to("cuda", dtype=torch.float16)
368+
.eval()
369+
)
370+
for p in teacher.parameters():
371+
p.requires_grad_(False)
372+
373+
# Student: byte-level ByT5
374+
student_tok = AutoTokenizer.from_pretrained("google/byt5-small")
375+
student = AutoModelForSeq2SeqLM.from_pretrained(spec["student_init"]).to("cuda")
376+
student.train()
377+
378+
class Pairs(Dataset):
379+
def __init__(self, path: Path, max_len: int = 384):
380+
self.rows = []
381+
for line in path.read_text(encoding="utf-8").splitlines():
382+
if not line.strip():
383+
continue
384+
try:
385+
row = json.loads(line)
386+
except json.JSONDecodeError:
387+
continue
388+
s, t = (row.get("src") or "").strip(), (row.get("tgt") or "").strip()
389+
if s and t and len(s.encode()) <= max_len and len(t.encode()) <= max_len:
390+
self.rows.append((s, t))
391+
392+
def __len__(self):
393+
return len(self.rows)
394+
395+
def __getitem__(self, i):
396+
return self.rows[i]
397+
398+
def collate(batch):
399+
src = student_tok([s for s, _ in batch], padding=True, return_tensors="pt")
400+
labels = student_tok(
401+
[t for _, t in batch], padding=True, return_tensors="pt"
402+
).input_ids
403+
labels[labels == student_tok.pad_token_id] = -100
404+
return src.input_ids, src.attention_mask, labels
405+
406+
train_ds = Pairs(train_path)
407+
print(f"[{spec_id}] train pairs: {len(train_ds)}", flush=True)
408+
409+
# Step 1: teacher generates labels (greedy) for the full corpus
410+
out_root = Path(teacher_root) / spec["out"]
411+
out_root.mkdir(parents=True, exist_ok=True)
412+
teacher_labels_path = out_root / "teacher_labels.jsonl"
413+
414+
if not teacher_labels_path.exists():
415+
print(f"[{spec_id}] generating teacher labels...", flush=True)
416+
with teacher_labels_path.open("w", encoding="utf-8") as fh:
417+
for start in range(0, len(train_ds), 32):
418+
batch = train_ds.rows[start : start + 32]
419+
enc = teacher_tok(
420+
[s for s, _ in batch],
421+
padding=True,
422+
truncation=True,
423+
max_length=384,
424+
return_tensors="pt",
425+
).to("cuda")
426+
with torch.no_grad():
427+
out = teacher.generate(**enc, max_new_tokens=384, num_beams=1)
428+
preds = teacher_tok.batch_decode(out, skip_special_tokens=True)
429+
for (src, _), pred in zip(batch, preds, strict=True):
430+
fh.write(
431+
json.dumps(
432+
{"src": src, "teacher": pred.strip()}, ensure_ascii=False
433+
)
434+
+ "\n"
435+
)
436+
if start % 320 == 0:
437+
print(
438+
f" labeled {start + len(batch)}/{len(train_ds)}", flush=True
439+
)
440+
else:
441+
print(f"[{spec_id}] teacher labels already exist", flush=True)
442+
443+
# Step 2: student trains on teacher labels
444+
teacher_labels = []
445+
for line in teacher_labels_path.read_text(encoding="utf-8").splitlines():
446+
if line.strip():
447+
row = json.loads(line)
448+
teacher_labels.append((row["src"], row["teacher"]))
449+
450+
class TeacherPairs(Dataset):
451+
def __len__(self):
452+
return len(teacher_labels)
453+
454+
def __getitem__(self, i):
455+
return teacher_labels[i]
456+
457+
train_loader = DataLoader(
458+
TeacherPairs(),
459+
batch_size=8,
460+
shuffle=True,
461+
collate_fn=collate,
462+
num_workers=2,
463+
drop_last=True,
464+
)
465+
total_steps = len(train_loader) * epochs
466+
optimizer = torch.optim.AdamW(student.parameters(), lr=1e-4)
467+
scheduler = get_cosine_schedule_with_warmup(
468+
optimizer, total_steps // 20, total_steps
469+
)
470+
471+
save_every = 500
472+
step = 0
473+
474+
for epoch in range(epochs):
475+
for ids, am, labels in train_loader:
476+
ids, am, labels = ids.to("cuda"), am.to("cuda"), labels.to("cuda")
477+
loss = student(input_ids=ids, attention_mask=am, labels=labels).loss
478+
loss.backward()
479+
torch.nn.utils.clip_grad_norm_(student.parameters(), 1.0)
480+
optimizer.step()
481+
scheduler.step()
482+
optimizer.zero_grad()
483+
step += 1
484+
if step % 50 == 0:
485+
print(
486+
f"[{spec_id} step {step}/{total_steps}] ce={float(loss):.4f}",
487+
flush=True,
488+
)
489+
if step % save_every == 0:
490+
ck = out_root / f"step-{step}"
491+
ck.mkdir(exist_ok=True)
492+
torch.save(student.state_dict(), ck / "student.pt")
493+
CHECKPOINTS.commit()
494+
SECRYST_CHECKPOINTS.commit()
495+
PERSIAN_CHECKPOINTS.commit()
496+
497+
best = out_root / "best"
498+
best.mkdir(exist_ok=True)
499+
student.save_pretrained(str(best))
500+
student_tok.save_pretrained(str(best))
501+
CHECKPOINTS.commit()
502+
SECRYST_CHECKPOINTS.commit()
503+
PERSIAN_CHECKPOINTS.commit()
504+
return {"spec": spec_id, "steps": step}
505+
506+
317507
@app.local_entrypoint()
318508
def main(spec: str = "heb-diac-small", epochs: int = 3) -> None:
319509
mode = SPECS[spec].get("mode", "logit")

0 commit comments

Comments
 (0)