fusion-llm-demo / train /full_finetune.py
zhan1206
fix(v8): S1 lora vocab sync, M1 test_sbla tuple unpack, N1 dead code removal
36e31c4
Raw
History Blame
13.4 kB
"""
Fusion 模型全参数微调脚本
支持:
- 本地 FusionModel(无需预训练权重)
- 8B 模型:单卡 24GB(开启 ZeRO-3 offload)
- 14B 模型:双卡 24GB 或单卡 48GB
- DeepSpeed ZeRO-3 支持
- 混合精度训练(BF16/FP16)
使用方法:
# 本地模型全参微调
python train/full_finetune.py --local_model --data_path data/example_data.json
# 8B 模型 + DeepSpeed ZeRO-3
deepspeed train/full_finetune.py --local_model --model_size 8B --deepspeed configs/ds_zero3.json --data_path data/example_data.json
作者:zhan1206
项目:Fusion - 六边形开源大模型
许可证:Apache 2.0
"""
import argparse
import torch
import torch.nn as nn
import deepspeed
from transformers import (
get_linear_schedule_with_warmup,
)
from models.tokenizer import get_tokenizer, get_effective_vocab_size
from torch.utils.data import Dataset, DataLoader
import json
import os
import sys
import logging
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from models import FusionModel, FusionConfig
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ============================================================
# 数据格式说明
# ============================================================
"""
训练数据格式(JSON):
[
{
"prompt": "解释量子纠缠",
"response": "量子纠缠是...",
"think_rank": 2
},
...
]
"""
class FusionFullFinetuneDataset(Dataset):
"""
全参数微调数据集
"""
def __init__(
self,
data_path: str,
tokenizer,
max_length: int = 2048,
):
self.tokenizer = tokenizer
self.max_length = max_length
with open(data_path, 'r', encoding='utf-8') as f:
self.data = json.load(f)
logger.info(f"[FusionFullFinetuneDataset] 加载数据集:{len(self.data)} 条样本")
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
item = self.data[idx]
prompt = item["prompt"]
response = item["response"]
think_rank = item.get("think_rank", 0)
if think_rank > 0:
thinking_token = f"<|think_depth_{think_rank}|>"
full_text = f"{thinking_token}\n{prompt}\n{response}"
else:
full_text = f"{prompt}\n{response}"
encoding = self.tokenizer(
full_text,
max_length=self.max_length,
padding="max_length",
truncation=True,
return_tensors="pt",
)
return {
"input_ids": encoding["input_ids"].squeeze(0),
"attention_mask": encoding["attention_mask"].squeeze(0),
"labels": encoding["input_ids"].squeeze(0).clone(),
}
def create_local_model(
model_size: str = "8B",
torch_dtype: torch.dtype = torch.bfloat16,
vocab_size_override: Optional[int] = None,
):
"""
创建本地 FusionModel(无需预训练权重)
"""
model_configs = {
"0.5B": dict(vocab_size=32000, hidden_size=2048, num_hidden_layers=16,
num_attention_heads=16, num_key_value_heads=8, intermediate_size=5504),
"1.5B": dict(vocab_size=32000, hidden_size=3072, num_hidden_layers=24,
num_attention_heads=24, num_key_value_heads=8, intermediate_size=8192),
"8B": dict(vocab_size=100000, hidden_size=4096, num_hidden_layers=32,
num_attention_heads=32, num_key_value_heads=8, intermediate_size=11008),
"14B": dict(vocab_size=100000, hidden_size=5120, num_hidden_layers=40,
num_attention_heads=40, num_key_value_heads=8, intermediate_size=13824),
}
if model_size not in model_configs:
raise ValueError(f"不支持的模型大小:{model_size}")
config_dict = model_configs[model_size]
# S3 fix: override vocab_size to match actual tokenizer
if vocab_size_override is not None:
config_dict['vocab_size'] = vocab_size_override
common_config = dict(
block_size=512,
latent_dim=64,
window_size=2048,
sbla_mode="hybrid",
rms_norm_eps=1e-6,
rope_theta=10000.0,
tie_word_embeddings=False,
enable_thinking_dial=True,
num_thinking_depths=4,
)
config = FusionConfig(**config_dict, **common_config)
logger.info(f"[create_local_model] 创建 Fusion-{model_size}(随机初始化)")
logger.info(f" hidden_size={config.hidden_size}, layers={config.num_hidden_layers}, "
f"heads={config.num_attention_heads}")
model = FusionModel(config)
total_params = sum(p.numel() for p in model.parameters())
logger.info(f"[create_local_model] 参数总量:{total_params / 1e9:.2f}B")
return model, config
def create_tokenizer(tokenizer_type: str = "fusion", vocab_size: int = 32000):
"""
Create tokenizer using the unified tokenizer module.
"""
logger.info(f"[create_tokenizer] Creating tokenizer: type={tokenizer_type}, vocab_size={vocab_size}")
tokenizer = get_tokenizer(tokenizer_type, vocab_size=vocab_size)
return tokenizer
def train(args):
"""
主训练函数
"""
logger.info("=" * 60)
logger.info("[train] 开始全参数微调")
logger.info(f" 模型大小:{args.model_size}")
logger.info(f" 使用 DeepSpeed:{args.deepspeed is not None}")
logger.info(f" 数据路径:{args.data_path}")
logger.info("=" * 60)
# 1. 设备设置
if args.local_rank == -1:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
logger.info(f"[train] 单卡训练,设备:{device}")
else:
torch.cuda.set_device(args.local_rank)
device = torch.device("cuda", args.local_rank)
logger.info(f"[train] 分布式训练,local_rank:{args.local_rank}")
# 2. 加载 tokenizer
vocab_size_map = {"0.5B": 32000, "1.5B": 32000, "8B": 100000, "14B": 100000}
tokenizer = create_tokenizer(vocab_size=vocab_size_map.get(args.model_size, 32000))
# Sync vocab_size to actual tokenizer size to prevent index-out-of-range (S3)
actual_vocab_size = len(tokenizer)
if actual_vocab_size != vocab_size_map.get(args.model_size, 32000):
logger.warning(f"[S3-fix] Vocab size mismatch: config={vocab_size_map.get(args.model_size, 32000)}, tokenizer={actual_vocab_size}. Syncing to tokenizer.")
vocab_size_map[args.model_size] = actual_vocab_size
# 3. 创建模型(本地随机初始化)
model, config = create_local_model(args.model_size, torch_dtype=args.torch_dtype, vocab_size_override=actual_vocab_size)
# 4. 加载数据集
train_dataset = FusionFullFinetuneDataset(
data_path=args.data_path,
tokenizer=tokenizer,
max_length=args.max_length,
)
train_loader = DataLoader(
train_dataset,
batch_size=args.batch_size,
shuffle=True,
num_workers=args.num_workers,
pin_memory=True,
)
# 5. 优化器
optimizer = torch.optim.AdamW(
model.parameters(),
lr=args.learning_rate,
betas=(0.9, 0.95),
weight_decay=args.weight_decay,
)
# 6. 学习率调度器
total_steps = len(train_loader) * args.num_epochs // args.gradient_accumulation_steps
warmup_steps = int(total_steps * args.warmup_ratio)
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=warmup_steps,
num_training_steps=total_steps,
)
# 7. DeepSpeed 初始化
if args.deepspeed:
logger.info(f"[train] 使用 DeepSpeed:{args.deepspeed}")
model_engine, optimizer, _, _ = deepspeed.initialize(
model=model,
optimizer=optimizer,
config=args.deepspeed,
)
else:
model = model.to(device)
model_engine = None
# 8. 训练循环
logger.info("[train] 开始训练循环...")
global_step = 0
for epoch in range(args.num_epochs):
logger.info(f"[train] Epoch {epoch + 1}/{args.num_epochs}")
model.train()
for step, batch in enumerate(train_loader):
input_ids = batch["input_ids"].to(device)
attention_mask = batch["attention_mask"].to(device)
labels = batch["labels"].to(device)
if args.deepspeed:
outputs = model_engine(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels,
)
loss = outputs.loss
model_engine.backward(loss)
model_engine.step()
else:
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels,
)
loss = outputs.loss
loss = loss / args.gradient_accumulation_steps
loss.backward()
if (step + 1) % args.gradient_accumulation_steps == 0:
torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm)
optimizer.step()
scheduler.step()
optimizer.zero_grad()
global_step += 1
if global_step % args.logging_steps == 0:
logger.info(f"Step {global_step} | Loss: {loss.item():.4f} | "
f"LR: {scheduler.get_last_lr()[0]:.2e}")
# 保存检查点
if args.deepspeed:
if model_engine.local_rank == 0:
save_path = os.path.join(args.output_dir, f"epoch_{epoch + 1}")
model_engine.save_checkpoint(save_path)
else:
if args.local_rank in [-1, 0]:
save_path = os.path.join(args.output_dir, f"epoch_{epoch + 1}")
model.save_pretrained(save_path)
tokenizer.save_pretrained(save_path)
config_path = os.path.join(save_path, "fusion_config.json")
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config.to_dict(), f, indent=2)
logger.info(f"[train] Epoch {epoch + 1} 完成,保存到 {args.output_dir}")
logger.info("[train] 全参数微调完成!")
def main():
parser = argparse.ArgumentParser(description="Fusion 模型全参数微调")
# 模型参数
parser.add_argument("--model_size", type=str, default="1.5B",
choices=["0.5B", "1.5B", "8B", "14B"],
help="模型大小")
parser.add_argument("--local_model", action="store_true", default=True,
help="使用本地 FusionModel(默认)")
parser.add_argument("--torch_dtype", type=str, default="bfloat16",
choices=["float32", "float16", "bfloat16"],
help="模型精度")
# 训练参数
parser.add_argument("--data_path", type=str, required=True,
help="训练数据路径(JSON 格式)")
parser.add_argument("--output_dir", type=str, default="./output/fusion-full",
help="输出目录")
parser.add_argument("--num_epochs", type=int, default=3,
help="训练轮数")
parser.add_argument("--batch_size", type=int, default=2,
help="批次大小(根据显存调整)")
parser.add_argument("--gradient_accumulation_steps", type=int, default=16,
help="梯度累积步数")
parser.add_argument("--learning_rate", type=float, default=1e-5,
help="学习率")
parser.add_argument("--weight_decay", type=float, default=0.01,
help="权重衰减")
parser.add_argument("--warmup_ratio", type=float, default=0.03,
help="预热步数比例")
parser.add_argument("--max_grad_norm", type=float, default=1.0,
help="梯度裁剪")
parser.add_argument("--max_length", type=int, default=2048,
help="最大序列长度")
# 硬件参数
parser.add_argument("--num_workers", type=int, default=4,
help="数据加载线程数")
parser.add_argument("--local_rank", type=int, default=-1,
help="用于分布式训练(由 torchrun 自动设置)")
# DeepSpeed
parser.add_argument("--deepspeed", type=str, default=None,
help="DeepSpeed 配置文件路径")
# 日志
parser.add_argument("--logging_steps", type=int, default=10,
help="日志打印间隔")
args = parser.parse_args()
# 设置 torch dtype
dtype_map = {"float32": torch.float32, "float16": torch.float16, "bfloat16": torch.bfloat16}
args.torch_dtype = dtype_map.get(args.torch_dtype, torch.bfloat16)
train(args)
if __name__ == "__main__":
main()