| |
|
|
| """ |
| SFT (Supervised Fine-Tuning) 全量微调 |
| 加载预训练权重 → 在对话数据上训练 → 保存SFT权重 |
| """ |
|
|
| import os |
| from dataclasses import dataclass |
| from typing import List, Dict, Optional |
|
|
| import torch |
| import torch.nn as nn |
| from torch.nn import functional as F |
| from torch.utils.data import Dataset, DataLoader |
|
|
| os.environ["TIKTOKEN_CACHE_DIR"] = "./tiktoken_cache" |
| import tiktoken |
|
|
| from model import GPT |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class SFTConfig: |
| |
| PREtrained_path: str = "log/model_00999_CampGPT_X.pt" |
| SFTtrained_path: str = "sft_output/sft_best.pt" |
| sft_data_path: str = "sft_dataset.json" |
| output_dir: str = "sft_output" |
| |
| |
| num_epochs: int = 100 |
| batch_size: int = 8 |
| max_seq_len: int = 512 |
| learning_rate: float = 2e-5 |
| min_lr: float = 2e-6 |
| warmup_steps: int = 10 |
| weight_decay: float = 0.01 |
| grad_clip: float = 1.0 |
| grad_accum_steps: int = 1 |
| |
| |
| system_prompt: str = "You are a helpful university assistant that answers questions about student policies and regulations." |
| |
| |
| |
| user_token: str = "<|user|>" |
| assistant_token: str = "<|assistant|>" |
| end_token: str = "<|end|>" |
| |
| |
| log_every: int = 10 |
| save_every_epoch: bool = False |
| eval_ratio: float = 0.05 |
|
|
|
|
| |
| |
| |
|
|
| class SFTTrainer: |
| """SFT 全量微调训练器""" |
| |
| def __init__(self, sft_config: SFTConfig): |
| self.sft_config = sft_config |
| self.device = "cuda" if torch.cuda.is_available() else "cpu" |
| self.device_type = "cuda" if self.device.startswith("cuda") else "cpu" |
| |
| |
| |
| self._load_models_SFT() |
|
|
|
|
|
|
| def _load_model_PreTrain(self): |
| """加载预训练权重""" |
| print(f"[SFT] Loading pretrained model from {self.sft_config.PREtrained_path}") |
| |
| checkpoint = torch.load(self.sft_config.PREtrained_path, map_location="cpu") |
| model_config = checkpoint["config"] |
| |
| |
| self.model_config = model_config |
| self.model = GPT(model_config) |
| |
| |
| state_dict = checkpoint["model"] |
| |
| cleaned = {} |
| for k, v in state_dict.items(): |
| k = k.replace("module.", "").replace("_orig_mod.", "") |
| cleaned[k] = v |
| |
| self.model.load_state_dict(cleaned, strict=True) |
| self.model.to(self.device) |
| |
| |
| self.model.set_gradient_checkpointing(False) |
| |
| pretrain_step = checkpoint.get("step", "unknown") |
| pretrain_loss = checkpoint.get("val_loss", "unknown") |
| print(f"[SFT] Loaded pretrained model (step={pretrain_step}, val_loss={pretrain_loss})") |
| print(f"[SFT] Model params: {sum(p.numel() for p in self.model.parameters()):,}") |
| |
| |
|
|
|
|
| def _load_models_SFT(self): |
| print(f"[DPO] Loading SFT model from {self.sft_config.SFTtrained_path}") |
| |
| checkpoint = torch.load(self.sft_config.SFTtrained_path, map_location="cpu") |
| self.model_config = checkpoint["config"] |
| self.chat_template = checkpoint.get("chat_template", {}) |
| |
| state_dict = checkpoint["model"] |
| cleaned = {k.replace("module.", "").replace("_orig_mod.", ""): v |
| for k, v in state_dict.items()} |
|
|
| self.model = GPT(self.model_config) |
| self.model.load_state_dict(cleaned, strict=True) |
| self.model.to(self.device) |
| self.model.eval() |
| for param in self.model.parameters(): |
| param.requires_grad = False |
| |
| total_params = sum(p.numel() for p in self.model.parameters()) |
| print(f"[DPO] Policy model params: {total_params:,}") |
| print(f"[DPO] Reference model: frozen copy") |
| |
| sft_loss = checkpoint.get("val_loss", "unknown") |
| print(f"[DPO] SFT val_loss: {sft_loss}") |
| |
|
|
| |
| def _test_generation(self): |
| """训练后快速测试生成""" |
| print(f"\n{'='*40} Generation Test {'='*40}") |
| |
| test_questions = [ |
| "Who is considered an Appellate Officer according to the student handbook?", |
| "What does the term 'Associate Dean' refer to in the UHCL Student Handbook?", |
| "How is 'Good Standing' defined for students at UHCL?", |
| ] |
| |
| self.model.eval() |
| enc = tiktoken.get_encoding("gpt2") |
| |
| for q in test_questions: |
| prompt_text = "" |
| if self.sft_config.system_prompt: |
| prompt_text += f"### System:\n{self.sft_config.system_prompt}\n\n" |
| prompt_text += f"### User:\n{q}\n\n### Assistant:\n" |
| |
| prompt_ids = enc.encode(prompt_text) |
| prompt_t = torch.tensor([prompt_ids], dtype=torch.long, device=self.device) |
| |
| with torch.no_grad(): |
| with torch.autocast(device_type=self.device_type, dtype=torch.bfloat16): |
| generated = self.model.generate( |
| prompt_t, max_new_tokens=64, |
| temperature=0.0, top_k=50, top_p=0.9 |
| ) |
| |
| output = enc.decode(generated[0].tolist()) |
| |
| if "### Assistant:" in output: |
| answer = output.split("### Assistant:")[-1].strip() |
| |
| if "###" in answer: |
| answer = answer[:answer.index("###")].strip() |
| else: |
| answer = output[len(prompt_text):] |
| |
| print(f"\nQ: {q}") |
| print(f"A: {answer[:300]}") |
| |
| print(f"{'='*80}\n") |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| config = SFTConfig() |
| |
| |
| import sys |
| for arg in sys.argv[1:]: |
| if "=" in arg: |
| key, val = arg.split("=", 1) |
| key = key.lstrip("-") |
| if hasattr(config, key): |
| field_type = type(getattr(config, key)) |
| setattr(config, key, field_type(val)) |
| print(f" Override: {key} = {val}") |
| |
| trainer = SFTTrainer(config) |
| trainer._test_generation() |