CampGPT_X / export_hf.py
bmnzyb's picture
Add files using upload-large-folder tool
55da402 verified
Raw
History Blame Contribute Delete
10.8 kB
# export_hf.py
"""
将训练好的模型导出为 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
# =============================================================================
# ========================= README 生成 ========================================
# =============================================================================
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"]
# 清理 key(去掉 DDP/FSDP/compile 可能添加的前缀)
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)
# ==================== 1. 保存模型配置 ====================
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")
# ==================== 2. 保存权重 ====================
# 处理共享权重:wte.weight 和 lm_head.weight 是同一个 tensor
# PyTorch 格式支持共享,直接保存
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)")
# safetensors 不支持共享 tensor,需要去重
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:
# 这个 tensor 和之前的某个 key 共享内存,跳过
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")
# ==================== 3. Tokenizer 信息 ====================
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")
# ==================== 4. 对话模板 ====================
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")
# ==================== 5. 复制源码 ====================
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}")
# ==================== 6. 生成 README.md ====================
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")
# ==================== 7. 上传脚本 ====================
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)