distill_mother.py v6: fix vocab_size mismatch (7B=152064, 1.5B=151936)
This commit is contained in:
parent
ec8939f5c0
commit
7d53003fb1
@ -1,229 +1,167 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""光湖母模型→1.5B霜砚模板 蒸馏脚本
|
"""光湖 母模型(7B)->1.5B霜砚 蒸馏 v6
|
||||||
Teacher: Qwen2.5-7B (SFT后的母模型)
|
修复:vocab_size不匹配 (teacher=152064, student=151936)
|
||||||
Student: Qwen2.5-1.5B (将学会霜砚的思维方式)
|
|
||||||
|
|
||||||
蒸馏方法:软蒸馏 (KL散度) + 混合SFT
|
|
||||||
|
|
||||||
使用方法:
|
|
||||||
nohup python3 -u distill_mother.py > distill_mother.log 2>&1 &
|
|
||||||
|
|
||||||
配置:
|
|
||||||
- 模型路径需根据实际存储位置修改
|
|
||||||
- Teacher路径:本地或COS上的SFT输出
|
|
||||||
- Student路径:ModelScope/HuggingFace原始模型
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os, json, torch, sys
|
import os, json, torch, sys
|
||||||
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
|
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
|
||||||
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
os.environ['TOKENIZERS_PARALLELISM'] = 'false'
|
||||||
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||||
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
|
from torch.utils.data import Dataset, DataLoader
|
||||||
from datasets import Dataset
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
import torch.nn.functional as F
|
import torch.nn.functional as F
|
||||||
|
from qcloud_cos import CosConfig, CosS3Client
|
||||||
|
|
||||||
# ========== 配置 ==========
|
TCH = '/root/autodl-tmp/output/qwen25-7b-sft/final'
|
||||||
TEACHER_PATH = "/root/autodl-tmp/output/qwen25-7b-sft/final" # 母模型SFT输出
|
STU = '/root/autodl-tmp/models/Qwen/Qwen2___5-1___5B-Instruct'
|
||||||
STUDENT_PATH = "/root/autodl-tmp/cache/Qwen/Qwen2___5-1___5B" # 1.5B学生
|
DATA = '/root/autodl-tmp/data/sft.jsonl'
|
||||||
DATA = "/root/autodl-tmp/data/sft.jsonl" # 主语料(也可用shuangyan专属语料)
|
OUT = '/root/autodl-tmp/output/qwen25-15b-shuangyan-distill'
|
||||||
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
|
||||||
EPOCHS = 3
|
BKT, RG = 'sy-finetune-corpus-1317346199', 'ap-guangzhou'
|
||||||
BS = 4 # 1.5B可以更大batch
|
CK = os.environ.get('ZY_OSS_KEY')
|
||||||
GA = 8
|
CS = os.environ.get('ZY_OSS_SECRET')
|
||||||
LR = 1e-5
|
|
||||||
MAX_LEN = 2048
|
|
||||||
TEMP = 2.0 # 蒸馏温度(越高分布越平滑)
|
|
||||||
ALPHA = 0.7 # 蒸馏loss权重 (0.7蒸馏 + 0.3SFT)
|
|
||||||
|
|
||||||
os.makedirs(OUT, exist_ok=True)
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
|
||||||
# ========== 1. 加载数据 ==========
|
# 1. Tokenize
|
||||||
print("[1/6] Loading data...")
|
print('[1/5] Tokenize...')
|
||||||
with open(DATA) as f:
|
with open(DATA) as f:
|
||||||
raw = [json.loads(line) for line in f]
|
raw = [json.loads(l) for l in f]
|
||||||
raw = [{"messages": [m for m in obj["messages"] if m["role"] != "system"]} for obj in raw]
|
tok = AutoTokenizer.from_pretrained(STU, trust_remote_code=True)
|
||||||
print(f" {len(raw)} examples")
|
tok.pad_token = tok.eos_token
|
||||||
|
data = []
|
||||||
# ========== 2. 加载Teacher + Student ==========
|
for d in tqdm(raw):
|
||||||
print("[2/6] Loading teacher (7B) and student (1.5B)...")
|
msgs = [m for m in d['messages'] if m['role'] != 'system']
|
||||||
|
ii, ll = [], []
|
||||||
tokenizer = AutoTokenizer.from_pretrained(STUDENT_PATH, trust_remote_code=True)
|
for m in msgs:
|
||||||
tokenizer.pad_token = tokenizer.eos_token
|
c = m['content']
|
||||||
|
|
||||||
print(" Loading teacher...")
|
|
||||||
teacher = AutoModelForCausalLM.from_pretrained(
|
|
||||||
TEACHER_PATH, trust_remote_code=True,
|
|
||||||
torch_dtype=torch.bfloat16, attn_implementation="sdpa",
|
|
||||||
).cuda()
|
|
||||||
teacher.eval()
|
|
||||||
for p in teacher.parameters():
|
|
||||||
p.requires_grad = False # Teacher不训练
|
|
||||||
print(f" Teacher: {sum(p.numel() for p in teacher.parameters())/1e9:.2f}B")
|
|
||||||
|
|
||||||
print(" Loading student...")
|
|
||||||
student = AutoModelForCausalLM.from_pretrained(
|
|
||||||
STUDENT_PATH, trust_remote_code=True,
|
|
||||||
torch_dtype=torch.bfloat16, attn_implementation="sdpa",
|
|
||||||
).cuda()
|
|
||||||
student.train()
|
|
||||||
print(f" Student: {sum(p.numel() for p in student.parameters())/1e9:.2f}B")
|
|
||||||
|
|
||||||
# ========== 3. Tokenize(生成teacher logits) ==========
|
|
||||||
print("[3/6] Tokenizing data and generating teacher logits...")
|
|
||||||
|
|
||||||
processed = []
|
|
||||||
for d in tqdm(raw, desc="Tokenize+Teacher"):
|
|
||||||
ids, labs = [], []
|
|
||||||
for msg in d["messages"]:
|
|
||||||
c = msg["content"]
|
|
||||||
if not c.strip(): continue
|
if not c.strip(): continue
|
||||||
t = f"<|im_start|>{msg['role']}\n{c}<|im_end|>\n"
|
txt = '<|im_start|>' + m['role'] + '\n' + c + '<|im_end|>\n'
|
||||||
tok = tokenizer.encode(t, add_special_tokens=False)
|
tk = tok.encode(txt, add_special_tokens=False)
|
||||||
ids.extend(tok)
|
ii.extend(tk)
|
||||||
labs.extend(tok if msg["role"] == "assistant" else [-100] * len(tok))
|
ll.extend(tk if m['role'] == 'assistant' else [-100]*len(tk))
|
||||||
if len(ids) > MAX_LEN:
|
if len(ii) > ML:
|
||||||
ids, labs = ids[:MAX_LEN], labs[:MAX_LEN]
|
ii, ll = ii[:ML], ll[:ML]
|
||||||
|
data.append({'input_ids': ii, 'labels': ll})
|
||||||
# Teacher生成logits(蒸馏目标)
|
print(f' {len(data)} examples')
|
||||||
with torch.no_grad():
|
|
||||||
inp = torch.tensor([ids]).cuda()
|
|
||||||
t_out = teacher(input_ids=inp)
|
|
||||||
t_logits = t_out.logits[0].float().cpu() # [seq_len, vocab_size]
|
|
||||||
|
|
||||||
processed.append({
|
|
||||||
"input_ids": ids, "labels": labs, "attention_mask": [1]*len(ids),
|
|
||||||
"teacher_logits": t_logits # 保存teacher的logits
|
|
||||||
})
|
|
||||||
|
|
||||||
ds = Dataset.from_list(processed)
|
class DS(Dataset):
|
||||||
total_tok = sum(len(d["input_ids"]) for d in processed)
|
def __init__(self, d): self.d = d
|
||||||
print(f" Dataset: {len(ds)} ex, {total_tok:,} tokens")
|
def __len__(self): return len(self.d)
|
||||||
|
def __getitem__(self, i): return self.d[i]
|
||||||
|
|
||||||
# ========== 4. 配置训练 ==========
|
def collate(batch):
|
||||||
print("[4/6] Training config...")
|
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()}
|
||||||
|
|
||||||
def distill_collate(features):
|
loader = DataLoader(DS(data), B, shuffle=True, collate_fn=collate, num_workers=0)
|
||||||
"""collate函数:处理padding + 蒸馏loss计算"""
|
|
||||||
max_len = max(len(f["input_ids"]) for f in features)
|
|
||||||
batch = {}
|
|
||||||
for k in ["input_ids", "labels", "attention_mask"]:
|
|
||||||
pad = tokenizer.pad_token_id if k != "labels" else -100
|
|
||||||
batch[k] = torch.tensor([f[k] + [pad]*(max_len-len(f[k])) for f in features])
|
|
||||||
# teacher_logits需要特殊padding(用0填充)
|
|
||||||
vocab_size = features[0]["teacher_logits"].size(-1)
|
|
||||||
tl = []
|
|
||||||
for f in features:
|
|
||||||
t = f["teacher_logits"]
|
|
||||||
pad_len = max_len - t.size(0)
|
|
||||||
if pad_len > 0:
|
|
||||||
tl.append(torch.cat([t, torch.zeros(pad_len, vocab_size)], dim=0))
|
|
||||||
else:
|
|
||||||
tl.append(t[:max_len])
|
|
||||||
batch["teacher_logits"] = torch.stack(tl)
|
|
||||||
return batch
|
|
||||||
|
|
||||||
class DistillTrainer(Trainer):
|
# 2. Load
|
||||||
"""自定义Trainer:蒸馏loss + SFT loss混合"""
|
print('[2/5] Load models...')
|
||||||
|
if not os.path.isdir(STU) or not os.path.isfile(STU + '/config.json'):
|
||||||
def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
|
from modelscope import snapshot_download
|
||||||
# 前向传播
|
snapshot_download('Qwen/Qwen2.5-1.5B-Instruct', cache_dir='/root/autodl-tmp/models')
|
||||||
outputs = model(
|
if not os.path.isfile(TCH + '/model.safetensors'):
|
||||||
input_ids=inputs["input_ids"],
|
print(' DL teacher from COS...')
|
||||||
attention_mask=inputs["attention_mask"],
|
cfg = CosConfig(Region=RG, SecretId=CK, SecretKey=CS)
|
||||||
use_cache=False,
|
cl = CosS3Client(cfg)
|
||||||
)
|
os.makedirs(TCH, exist_ok=True)
|
||||||
student_logits = outputs.logits # [batch, seq_len, vocab_size]
|
for obj in cl.list_objects(Bucket=BKT, Prefix='models/qwen25-7b-sft/final/').get('Contents', []):
|
||||||
|
fn = obj['Key'].split('/')[-1]
|
||||||
# SFT loss (交叉熵,只计算assistant部分)
|
if fn == 'DEPLOY_NOTES.md': continue
|
||||||
shift_logits = student_logits[..., :-1, :].contiguous()
|
loc = os.path.join(TCH, fn)
|
||||||
shift_labels = inputs["labels"][..., 1:].contiguous()
|
if not os.path.isfile(loc):
|
||||||
sft_loss = F.cross_entropy(
|
print(f' {fn}', flush=True)
|
||||||
shift_logits.view(-1, shift_logits.size(-1)),
|
cl.download_file(Bucket=BKT, Key=obj['Key'], DestFilePath=loc)
|
||||||
shift_labels.view(-1),
|
|
||||||
ignore_index=-100,
|
|
||||||
reduction="mean",
|
|
||||||
)
|
|
||||||
|
|
||||||
# KL蒸馏loss(teacher vs student)
|
|
||||||
teacher_logits = inputs["teacher_logits"] # [batch, seq_len, vocab_size]
|
|
||||||
# 只对assistant部分计算KL
|
|
||||||
mask = (inputs["labels"] != -100).unsqueeze(-1).float() # [batch, seq_len, 1]
|
|
||||||
|
|
||||||
# 软化分布
|
|
||||||
s_logits_soft = student_logits / TEMP
|
|
||||||
t_logits_soft = teacher_logits / TEMP
|
|
||||||
|
|
||||||
kl_loss = F.kl_div(
|
|
||||||
F.log_softmax(s_logits_soft, dim=-1),
|
|
||||||
F.softmax(t_logits_soft, dim=-1),
|
|
||||||
reduction="none",
|
|
||||||
)
|
|
||||||
kl_loss = (kl_loss * mask).sum() / mask.sum()
|
|
||||||
kl_loss = kl_loss * (TEMP ** 2) # 温度缩放
|
|
||||||
|
|
||||||
# 混合loss
|
|
||||||
total_loss = ALPHA * kl_loss + (1 - ALPHA) * sft_loss
|
|
||||||
|
|
||||||
return total_loss
|
|
||||||
|
|
||||||
args = TrainingArguments(
|
print(' Teacher...', flush=True)
|
||||||
output_dir=OUT, num_train_epochs=EPOCHS,
|
teacher = AutoModelForCausalLM.from_pretrained(
|
||||||
per_device_train_batch_size=BS, gradient_accumulation_steps=GA,
|
TCH, trust_remote_code=True, torch_dtype=torch.bfloat16, attn_implementation='sdpa').cuda()
|
||||||
learning_rate=LR, warmup_ratio=0.05, lr_scheduler_type="cosine",
|
teacher.eval()
|
||||||
bf16=True, tf32=True, logging_steps=10,
|
for p in teacher.parameters(): p.requires_grad_(False)
|
||||||
save_strategy="epoch", save_total_limit=3,
|
print(f' T: {sum(p.numel() for p in teacher.parameters())/1e9:.2f}B', flush=True)
|
||||||
remove_unused_columns=False, dataloader_num_workers=4,
|
|
||||||
gradient_checkpointing=True, optim="adamw_torch",
|
|
||||||
report_to="none", ddp_find_unused_parameters=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
trainer = DistillTrainer(
|
print(' Student...', flush=True)
|
||||||
model=student, args=args,
|
student = AutoModelForCausalLM.from_pretrained(
|
||||||
train_dataset=ds, data_collator=distill_collate,
|
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)
|
||||||
|
|
||||||
# ========== 5. 启动训练 ==========
|
# 3. Train
|
||||||
print("[5/6] Starting distillation!")
|
print('[3/5] Train...', flush=True)
|
||||||
gpu = torch.cuda.get_device_name(0)
|
opt = torch.optim.AdamW(student.parameters(), LR, weight_decay=0.01)
|
||||||
mem = torch.cuda.get_device_properties(0).total_memory / 1e9
|
steps_per_epoch = (len(data) // B)
|
||||||
t_params = sum(p.numel() for p in teacher.parameters())
|
total_steps = steps_per_epoch * E
|
||||||
s_params = sum(p.numel() for p in student.parameters())
|
sch = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=total_steps // GA)
|
||||||
print(f" GPU: {gpu} ({mem:.1f}GB)")
|
global_step = 0
|
||||||
print(f" Teacher: {t_params/1e9:.2f}B | Student: {s_params/1e9:.2f}B")
|
|
||||||
print(f" Temp={TEMP}, Alpha={ALPHA}, Eff batch={BS*GA}, LR={LR}")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
trainer.train()
|
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
|
||||||
|
|
||||||
# ========== 6. 保存 ==========
|
# ⚠️ FIX: Truncate teacher logits to student vocab size
|
||||||
print("[6/6] Saving distilled model...")
|
# Teacher(7B) vocab=152064, Student(1.5B) vocab=151936
|
||||||
final = os.path.join(OUT, "final")
|
t_logits = t_logits[..., :student.config.vocab_size]
|
||||||
trainer.save_model(final)
|
|
||||||
tokenizer.save_pretrained(final)
|
|
||||||
|
|
||||||
# ⚠️ 关键修复:Qwen chat template 使用 <|im_end|> (151645) 作为对话EOS
|
shift_s = s_logits[..., :-1, :].contiguous()
|
||||||
# 但默认 eos_token_id=151643 (<|endoftext|>)
|
shift_l = batch['labels'][..., 1:].contiguous()
|
||||||
# 不修复会导致部署时模型无限生成 → 死循环乱码
|
sft = F.cross_entropy(shift_s.view(-1, shift_s.size(-1)),
|
||||||
# 注意:必须同时修复 config.json 和 generation_config.json!
|
shift_l.view(-1), ignore_index=-100, reduction='mean')
|
||||||
model.config.eos_token_id = 151645
|
|
||||||
model.config.save_pretrained(final)
|
|
||||||
|
|
||||||
model.generation_config.eos_token_id = 151645
|
mask = (batch['labels'] != -100).unsqueeze(-1).float()
|
||||||
model.generation_config.pad_token_id = 151645
|
kls = F.kl_div(F.log_softmax(s_logits.float()/TEMP, -1),
|
||||||
model.generation_config.save_pretrained(final)
|
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()
|
||||||
|
|
||||||
# 修复 tokenizer 默认system prompt(避免 "You are Qwen...")
|
if global_step % GA == 0:
|
||||||
import json as _json
|
torch.nn.utils.clip_grad_norm_(student.parameters(), 1.0)
|
||||||
_tok_cfg_path = os.path.join(final, "tokenizer_config.json")
|
opt.step(); opt.zero_grad(); sch.step()
|
||||||
with open(_tok_cfg_path) as _f:
|
|
||||||
_tok_cfg = _json.load(_f)
|
|
||||||
_tok_cfg["default_system"] = ""
|
|
||||||
with open(_tok_cfg_path, "w") as _f:
|
|
||||||
_json.dump(_tok_cfg, _f, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
peak = torch.cuda.max_memory_allocated() / 1e9
|
if global_step % 50 == 0:
|
||||||
print(f" Model: {final}")
|
print(f' step={global_step} loss={loss.item()*GA:.4f} sft={sft.item():.4f} kl={kl.item():.4f}', flush=True)
|
||||||
print(f" Peak VRAM: {peak:.2f}GB / {mem:.1f}GB")
|
|
||||||
print("DONE!")
|
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)
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user