| |
|
|
| """ |
| 将训练好的模型导出为 HuggingFace 兼容格式 |
| 可以上传到 HuggingFace Hub |
| """ |
|
|
| import os |
| import json |
| import shutil |
| from typing import Optional |
|
|
| import torch |
| import tiktoken |
|
|
| from config import GPTConfig, get_model_config |
| from model import GPT |
| from train_dpo import DPOConfig |
|
|
|
|
| |
| |
| |
|
|
| def generate_readme(config_dict, model_config, model_name): |
| """生成 README.md 内容,避免 f-string 嵌套反引号的问题""" |
| total_params = config_dict["total_params"] |
| moe_str = " + MoE" if model_config.use_moe else "" |
|
|
| lines = [ |
| "---", |
| "license: apache-2.0", |
| "language:", |
| " - en", |
| "tags:", |
| " - text-generation", |
| " - education", |
| " - student-handbook", |
| " - campus-qa", |
| " - custom-architecture", |
| "pipeline_tag: text-generation", |
| "---", |
| "", |
| f"# {model_name}", |
| "", |
| "A compact GPT model trained for university student handbook Q&A.", |
| "", |
| "## Model Details", |
| "", |
| "| Property | Value |", |
| "|----------|-------|", |
| f"| Parameters | {total_params:,} ({total_params/1e6:.1f}M) |", |
| f"| Architecture | Transformer (GQA + RoPE + SwiGLU{moe_str}) |", |
| f"| Layers | {model_config.n_layer} |", |
| f"| Heads | {model_config.n_head} (KV: {model_config.n_kv_head}) |", |
| f"| Embedding | {model_config.n_embd} |", |
| f"| Context Length | {model_config.block_size} |", |
| "| Tokenizer | tiktoken (GPT-2, 50257 vocab) |", |
| "| Training | Pretrain -> SFT -> DPO |", |
| "", |
| "## Training Pipeline", |
| "", |
| "1. **Pretrain**: 10B tokens from FineWeb-Edu", |
| "2. **SFT**: Fine-tuned on student handbook Q&A pairs", |
| "3. **DPO**: Preference optimization with chosen/rejected pairs", |
| "", |
| "## Usage", |
| "", |
| "```python", |
| "from serve import CampGPTServer", |
| "", |
| 'server = CampGPTServer("campgpt-student-handbook")', |
| 'response = server.chat("What are the requirements for a scholarship?")', |
| "print(response)", |
| "```", |
| "", |
| "## Chat Format", |
| "", |
| "```text", |
| "### System:", |
| "You are a helpful university assistant...", |
| "", |
| "### User:", |
| "What are the scholarship requirements?", |
| "", |
| "### Assistant:", |
| "Based on the student handbook...", |
| "```", |
| "", |
| "## Limitations", |
| "", |
| "- Small model with limited capacity", |
| "- Knowledge limited to the specific student handbook used for training", |
| "- May hallucinate details not in the training data", |
| ] |
|
|
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
|
|
| def generate_upload_script(model_name, output_dir): |
| """生成上传到 HuggingFace Hub 的 bash 脚本""" |
| lines = [ |
| "#!/bin/bash", |
| "# Upload to HuggingFace Hub", |
| "# pip install huggingface_hub", |
| "", |
| "python -c \"", |
| "from huggingface_hub import HfApi, create_repo", |
| "", |
| "api = HfApi()", |
| f"repo_id = 'YOUR_USERNAME/{model_name}'", |
| "", |
| "create_repo(repo_id, exist_ok=True, repo_type='model')", |
| "", |
| "api.upload_folder(", |
| f" folder_path='{output_dir}',", |
| " repo_id=repo_id,", |
| " repo_type='model',", |
| ")", |
| "print(f'Uploaded to https://huggingface.co/{repo_id}')", |
| "\"", |
| ] |
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
|
|
| def export_to_hf( |
| checkpoint_path: str = "dpo_output/dpo_best.pt", |
| output_dir: str = "campgpt-student-handbook", |
| model_name: str = "CampGPT-Student-Handbook", |
| ): |
| """ |
| 导出模型为 HuggingFace 兼容格式 |
| |
| 输出结构: |
| output_dir/ |
| ├── config.json # 模型配置 |
| ├── model.safetensors # 权重 (safetensors 格式) |
| ├── pytorch_model.bin # 权重 (PyTorch 格式, 备用) |
| ├── tokenizer.json # Tokenizer 信息 |
| ├── chat_template.json # 对话模板 |
| ├── README.md # 模型卡片 |
| ├── upload.sh # 上传脚本 |
| ├── model.py # 模型定义 (方便复现) |
| └── config.py # 配置定义 |
| """ |
|
|
| print(f"[Export] Loading checkpoint: {checkpoint_path}") |
| checkpoint = torch.load(checkpoint_path, map_location="cpu") |
|
|
| model_config = checkpoint["config"] |
| chat_template = checkpoint.get("chat_template", {}) |
| state_dict = checkpoint["model"] |
|
|
| |
| cleaned = {} |
| for k, v in state_dict.items(): |
| k = k.replace("module.", "").replace("_orig_mod.", "") |
| cleaned[k] = v |
|
|
| os.makedirs(output_dir, exist_ok=True) |
|
|
| |
| config_dict = { |
| "model_type": "campgpt", |
| "architectures": ["CampGPT"], |
| "vocab_size": model_config.vocab_size, |
| "n_embd": model_config.n_embd, |
| "n_head": model_config.n_head, |
| "n_kv_head": model_config.n_kv_head, |
| "n_layer": model_config.n_layer, |
| "block_size": model_config.block_size, |
| "norm_eps": model_config.norm_eps, |
| "multiple_of": model_config.multiple_of, |
| "use_moe": model_config.use_moe, |
| "n_experts": getattr(model_config, "n_experts", 0), |
| "n_experts_per_tok": getattr(model_config, "n_experts_per_tok", 0), |
| "n_shared_experts": getattr(model_config, "n_shared_experts", 0), |
| "total_params": sum(p.numel() for p in cleaned.values()), |
| "training_stages": ["pretrain_10B", "sft", "dpo"], |
| "val_loss": checkpoint.get("val_loss", None), |
| } |
|
|
| with open(os.path.join(output_dir, "config.json"), "w") as f: |
| json.dump(config_dict, f, indent=2) |
| print(f" Saved config.json") |
|
|
|
|
|
|
| |
| |
| |
| torch.save(cleaned, os.path.join(output_dir, "pytorch_model.bin")) |
| size_mb = sum(v.numel() * v.element_size() for v in cleaned.values()) / 1e6 |
| print(f" Saved pytorch_model.bin ({size_mb:.1f} MB)") |
|
|
| |
| try: |
| from safetensors.torch import save_file |
|
|
| |
| safetensors_dict = {} |
| seen_data_ptrs = {} |
| shared_keys = {} |
|
|
| for k, v in cleaned.items(): |
| data_ptr = v.data_ptr() |
| if data_ptr in seen_data_ptrs: |
| |
| shared_keys[k] = seen_data_ptrs[data_ptr] |
| print(f" [safetensors] Skip shared: {k} -> {seen_data_ptrs[data_ptr]}") |
| else: |
| seen_data_ptrs[data_ptr] = k |
| safetensors_dict[k] = v |
|
|
| save_file(safetensors_dict, os.path.join(output_dir, "model.safetensors")) |
| print(f" Saved model.safetensors") |
|
|
| |
| if shared_keys: |
| with open(os.path.join(output_dir, "shared_weights.json"), "w") as f: |
| json.dump(shared_keys, f, indent=2) |
| print(f" Saved shared_weights.json: {shared_keys}") |
|
|
| except ImportError: |
| print(f" [Skip] safetensors not installed, skipping") |
|
|
|
|
|
|
|
|
|
|
|
|
| |
| |
| tokenizer_info = { |
| "type": "tiktoken", |
| "encoding": "gpt2", |
| "vocab_size": 50257, |
| "special_tokens": { |
| "pad_token": "<|endoftext|>", |
| "eos_token": "<|endoftext|>", |
| }, |
| } |
| with open(os.path.join(output_dir, "tokenizer.json"), "w") as f: |
| json.dump(tokenizer_info, f, indent=2) |
| print(f" Saved tokenizer.json") |
|
|
| |
| chat_info = { |
| "system_prompt": chat_template.get("system_prompt", ""), |
| "user_prefix": chat_template.get("user_prefix", "\n\n### User:\n"), |
| "assistant_prefix": chat_template.get("assistant_prefix", "\n\n### Assistant:\n"), |
| "turn_end": chat_template.get("turn_end", "\n\n"), |
| "template": "### System:\n{system}\n\n### User:\n{user}\n\n### Assistant:\n{assistant}\n\n", |
| } |
| with open(os.path.join(output_dir, "chat_template.json"), "w") as f: |
| json.dump(chat_info, f, ensure_ascii=False, indent=2) |
| print(f" Saved chat_template.json") |
|
|
| |
| for src_file in ["model.py", "config.py"]: |
| if os.path.exists(src_file): |
| shutil.copy(src_file, os.path.join(output_dir, src_file)) |
| print(f" Copied {src_file}") |
|
|
| |
| readme = generate_readme(config_dict, model_config, model_name) |
| with open(os.path.join(output_dir, "README.md"), "w") as f: |
| f.write(readme) |
| print(f" Saved README.md") |
|
|
| |
| upload_script = generate_upload_script(model_name, output_dir) |
| with open(os.path.join(output_dir, "upload.sh"), "w") as f: |
| f.write(upload_script) |
| print(f" Saved upload.sh") |
|
|
| print(f"\n[Export] Done! Files saved to {output_dir}/") |
| print(f" To upload: edit upload.sh with your HF username, then run it") |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| import sys |
|
|
| ckpt = sys.argv[1] if len(sys.argv) > 1 else "dpo_output/dpo_best.pt" |
| out = sys.argv[2] if len(sys.argv) > 2 else "campgpt-student-handbook" |
|
|
| export_to_hf(checkpoint_path=ckpt, output_dir=out) |