guanghulab/scripts/distill_mother.py
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

176 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""光湖 母模型(7B)->1.5B霜砚 蒸馏 v6
修复vocab_size不匹配 (teacher=152064, student=151936)
"""
import os, json, torch, sys
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
os.environ['TOKENIZERS_PARALLELISM'] = 'false'
os.environ['PYTORCH_CUDA_ALLOC_CONF'] = 'expandable_segments:True'
from transformers import AutoModelForCausalLM, AutoTokenizer
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm
import torch.nn.functional as F
from qcloud_cos import CosConfig, CosS3Client
TCH = '/root/autodl-tmp/output/qwen25-7b-sft/final'
STU = '/root/autodl-tmp/models/Qwen/Qwen2___5-1___5B-Instruct'
DATA = '/root/autodl-tmp/data/sft.jsonl'
OUT = '/root/autodl-tmp/output/qwen25-15b-shuangyan-distill'
E, B, GA, LR, ML = 3, 4, 8, 1e-5, 2048
TEMP, ALPHA = 2.0, 0.7
BKT, RG = 'sy-finetune-corpus-1317346199', 'ap-guangzhou'
CK = os.environ.get('ZY_OSS_KEY')
CS = os.environ.get('ZY_OSS_SECRET')
os.makedirs(OUT, exist_ok=True)
# 1. Tokenize
print('[1/5] Tokenize...')
with open(DATA) as f:
raw = [json.loads(l) for l in f]
tok = AutoTokenizer.from_pretrained(STU, trust_remote_code=True)
tok.pad_token = tok.eos_token
data = []
for d in tqdm(raw):
msgs = [m for m in d['messages'] if m['role'] != 'system']
ii, ll = [], []
for m in msgs:
c = m['content']
if not c.strip(): continue
txt = '<|im_start|>' + m['role'] + '\n' + c + '<|im_end|>\n'
tk = tok.encode(txt, add_special_tokens=False)
ii.extend(tk)
ll.extend(tk if m['role'] == 'assistant' else [-100]*len(tk))
if len(ii) > ML:
ii, ll = ii[:ML], ll[:ML]
data.append({'input_ids': ii, 'labels': ll})
print(f' {len(data)} examples')
class DS(Dataset):
def __init__(self, d): self.d = d
def __len__(self): return len(self.d)
def __getitem__(self, i): return self.d[i]
def collate(batch):
ml = max(len(x['input_ids']) for x in batch)
pad_id = tok.pad_token_id
ii = torch.stack([torch.tensor(x['input_ids'] + [pad_id]*(ml-len(x['input_ids']))) for x in batch])
ll = torch.stack([torch.tensor(x['labels'] + [-100]*(ml-len(x['labels']))) for x in batch])
am = (ii != pad_id).long()
return {'input_ids': ii.cuda(), 'labels': ll.cuda(), 'attention_mask': am.cuda()}
loader = DataLoader(DS(data), B, shuffle=True, collate_fn=collate, num_workers=0)
# 2. Load
print('[2/5] Load models...')
torch.cuda.empty_cache() # Clear any stale memory before loading
if not os.path.isdir(STU) or not os.path.isfile(STU + '/config.json'):
from modelscope import snapshot_download
snapshot_download('Qwen/Qwen2.5-1.5B-Instruct', cache_dir='/root/autodl-tmp/models')
if not os.path.isfile(TCH + '/model.safetensors'):
print(' DL teacher from COS...')
cfg = CosConfig(Region=RG, SecretId=CK, SecretKey=CS)
cl = CosS3Client(cfg)
os.makedirs(TCH, exist_ok=True)
for obj in cl.list_objects(Bucket=BKT, Prefix='models/qwen25-7b-sft/final/').get('Contents', []):
fn = obj['Key'].split('/')[-1]
if fn == 'DEPLOY_NOTES.md': continue
loc = os.path.join(TCH, fn)
if not os.path.isfile(loc):
print(f' {fn}', flush=True)
cl.download_file(Bucket=BKT, Key=obj['Key'], DestFilePath=loc)
print(' Teacher...', flush=True)
teacher = AutoModelForCausalLM.from_pretrained(
TCH, trust_remote_code=True, torch_dtype=torch.bfloat16, attn_implementation='sdpa').cuda()
teacher.eval()
for p in teacher.parameters(): p.requires_grad_(False)
print(f' T: {sum(p.numel() for p in teacher.parameters())/1e9:.2f}B', flush=True)
print(' Student...', flush=True)
student = AutoModelForCausalLM.from_pretrained(
STU, trust_remote_code=True, torch_dtype=torch.bfloat16, attn_implementation='sdpa').cuda()
student.train()
print(f' S: {sum(p.numel() for p in student.parameters())/1e9:.2f}B', flush=True)
print(f' VRAM: {torch.cuda.memory_allocated()/1e9:.1f}GB', flush=True)
# 3. Train
print('[3/5] Train...', flush=True)
opt = torch.optim.AdamW(student.parameters(), LR, weight_decay=0.01)
steps_per_epoch = (len(data) // B)
total_steps = steps_per_epoch * E
sch = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=total_steps // GA)
global_step = 0
for ep in range(E):
print(f' Epoch {ep+1}/{E}', flush=True)
for step, batch in enumerate(tqdm(loader, desc=f'Ep{ep+1}')):
global_step += 1
# Teacher forward (on-the-fly)
with torch.no_grad():
t_logits = teacher(input_ids=batch['input_ids'], attention_mask=batch['attention_mask']).logits
# Student forward
s_logits = student(input_ids=batch['input_ids'], attention_mask=batch['attention_mask']).logits
# ⚠️ FIX: Truncate teacher logits to student vocab size
# Teacher(7B) vocab=152064, Student(1.5B) vocab=151936
t_logits = t_logits[..., :student.config.vocab_size]
shift_s = s_logits[..., :-1, :].contiguous()
shift_l = batch['labels'][..., 1:].contiguous()
sft = F.cross_entropy(shift_s.view(-1, shift_s.size(-1)),
shift_l.view(-1), ignore_index=-100, reduction='mean')
mask = (batch['labels'] != -100).unsqueeze(-1).float()
kls = F.kl_div(F.log_softmax(s_logits.float()/TEMP, -1),
F.softmax(t_logits.float()/TEMP, -1), reduction='none')
kl = (kls * mask).sum() / mask.sum() * TEMP**2
loss = (ALPHA*kl + (1-ALPHA)*sft) / GA
loss.backward()
if global_step % GA == 0:
torch.nn.utils.clip_grad_norm_(student.parameters(), 1.0)
opt.step(); opt.zero_grad(); sch.step()
if global_step % 500 == 0:
# Prevent memory fragmentation
torch.cuda.empty_cache()
if global_step % 50 == 0:
print(f' step={global_step} loss={loss.item()*GA:.4f} sft={sft.item():.4f} kl={kl.item():.4f}', flush=True)
ckpt = os.path.join(OUT, f'ep{ep+1}')
os.makedirs(ckpt, exist_ok=True)
student.save_pretrained(ckpt); tok.save_pretrained(ckpt)
print(f' Checkpoint: {ckpt}', flush=True)
# FIX: Clear GPU cache between epochs to prevent fragmentation OOM
torch.cuda.empty_cache()
# 4. Save
print('[4/5] Save final...', flush=True)
fnl = os.path.join(OUT, 'final')
student.save_pretrained(fnl); tok.save_pretrained(fnl)
student.config.eos_token_id = 151645; student.config.save_pretrained(fnl)
student.generation_config.eos_token_id = 151645
student.generation_config.pad_token_id = 151645
student.generation_config.save_pretrained(fnl)
tcp = os.path.join(fnl, 'tokenizer_config.json')
with open(tcp) as f: tc = json.load(f)
tc['default_system'] = ''
with open(tcp, 'w') as f: json.dump(tc, f, indent=2, ensure_ascii=False)
print(f' {fnl}', flush=True)
print(f' Peak VRAM: {torch.cuda.max_memory_allocated()/1e9:.1f}/96GB', flush=True)
print('DONE!', flush=True)
# 5. Upload
print('[5/5] Upload to COS...', flush=True)
cfg = CosConfig(Region=RG, SecretId=CK, SecretKey=CS)
cl = CosS3Client(cfg)
import glob
for fp in sorted(glob.glob(fnl + '/**/*', recursive=True)):
if os.path.isfile(fp):
name = os.path.relpath(fp, fnl)
sz = os.path.getsize(fp) / 1024 / 1024
print(f' {name} ({sz:.0f}MB)', flush=True)
cl.upload_file(Bucket=BKT, Key='models/shuangyan-15b-distill/' + name, LocalFilePath=fp)
print('ALL DONE')