@@ -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 ()
318508def main (spec : str = "heb-diac-small" , epochs : int = 3 ) -> None :
319509 mode = SPECS [spec ].get ("mode" , "logit" )
0 commit comments