guanghulab/zhuyuan-agent/training_runner.py

287 lines
12 KiB
Python
Raw Permalink Normal View History

# HLDP原生格式训练执行器
# HLDP://zhuyuan-agent/training-runner
#
# 核心验证Notion原生页面格式HLDP标记直接训练不转JSONL。
import os
import json
import time
import sys
from typing import Optional, Callable
# 特殊Token定义HLDP结构标记
SPECIAL_TOKENS = [
"[HLDP_PATH]", "[/HLDP_PATH]",
"[PERSONA]", "[/PERSONA]",
"[COGNITIVE_JUMP]", "[/COGNITIVE_JUMP]",
"[CAUSAL_CHAIN]", "[/CAUSAL_CHAIN]",
"[TITLE]", "[/TITLE]",
"[CONTENT]", "[/CONTENT]",
"[QUALITY_HIGH]", "[QUALITY_MEDIUM]",
"[THINKING]", "[/THINKING]",
"[HEADING_1]", "[/HEADING_1]",
"[HEADING_2]", "[/HEADING_2]",
"[HEADING_3]", "[/HEADING_3]",
"[CODE_BLOCK]", "[/CODE_BLOCK]",
"[QUOTE]", "[/QUOTE]",
"[CALLOUT]", "[/CALLOUT]",
"[LIST_ITEM]", "[/LIST_ITEM]",
]
class TrainingRunner:
"""HLDP原生格式训练管道"""
def __init__(self, config: dict, progress_callback: Optional[Callable] = None):
"""
Args:
config: 训练配置来自config.json的training部分
progress_callback: 每步回调接收 (step, loss, total_steps)
"""
self.config = config
self.progress_callback = progress_callback
self.model = None
self.tokenizer = None
self.start_time = None
self.loss_history = []
def prepare(self):
"""准备训练环境注册特殊token加载模型"""
print("[铸渊Agent] 准备HLDP训练环境...")
try:
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
import torch
model_name = self.config.get("model_name", "Qwen/Qwen2.5-3B")
use_4bit = self.config.get("use_4bit", True)
print(f"[铸渊Agent] 加载模型: {model_name}")
# 加载tokenizer并添加HLDP特殊token
self.tokenizer = AutoTokenizer.from_pretrained(
model_name,
trust_remote_code=True,
padding_side="right"
)
# 设置pad_token
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
# 添加HLDP特殊token到tokenizer
num_added = self.tokenizer.add_tokens(SPECIAL_TOKENS)
print(f"[铸渊Agent] 添加了 {num_added} 个HLDP特殊token到tokenizer")
# 加载模型4bit量化以适配3090 24GB
load_kwargs = {
"trust_remote_code": True,
"torch_dtype": torch.float16,
"device_map": "auto",
}
if use_4bit:
try:
from transformers import BitsAndBytesConfig
load_kwargs["quantization_config"] = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)
print("[铸渊Agent] 使用4bit量化加载")
except ImportError:
print("[铸渊Agent] bitsandbytes不可用使用float16")
load_kwargs.pop("quantization_config", None)
self.model = AutoModelForCausalLM.from_pretrained(model_name, **load_kwargs)
# 扩展embedding层以支持新token
if num_added > 0:
self.model.resize_token_embeddings(len(self.tokenizer))
# 准备LoRA
self.model = prepare_model_for_kbit_training(self.model)
lora_config = LoraConfig(
r=self.config.get("lora_r", 16),
lora_alpha=self.config.get("lora_alpha", 32),
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
self.model = get_peft_model(self.model, lora_config)
self.model.print_trainable_parameters()
print("[铸渊Agent] HLDP训练环境准备完成")
return True
except ImportError as e:
print(f"[铸渊Agent] 缺少依赖: {e}")
print("请安装: pip install transformers peft accelerate bitsandbytes torch")
return False
except Exception as e:
print(f"[铸渊Agent] 准备失败: {e}")
return False
def load_hldp_corpus(self, corpus_dir: str) -> list:
"""加载HLDP格式语料不转JSONL保留原生HLDP标记
Returns:
list of str: 每条是一个HLDP标记的完整训练文本
"""
texts = []
if not os.path.exists(corpus_dir):
print(f"[铸渊Agent] 语料目录不存在: {corpus_dir}")
return texts
for root, dirs, files in os.walk(corpus_dir):
for filename in files:
filepath = os.path.join(root, filename)
if filename.endswith(".hldp"):
# HLDP原生格式文件
with open(filepath, "r", encoding="utf-8") as f:
texts.append(f.read())
elif filename.endswith(".md"):
# Markdown文件 → 按HLDP结构包装
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# 包裹HLDP标记
wrapped = f"[HLDP_PATH]{filepath}[/HLDP_PATH]\n[CONTENT]\n{content}\n[/CONTENT]"
texts.append(wrapped)
print(f"[铸渊Agent] 加载了 {len(texts)} 条HLDP语料")
return texts
def train(self, corpus_dir: str, progress_callback: Optional[Callable] = None):
"""执行HLDP原生格式训练"""
if self.model is None:
if not self.prepare():
return {"status": "error", "message": "模型准备失败"}
texts = self.load_hldp_corpus(corpus_dir)
if not texts:
return {"status": "error", "message": "无语料数据"}
# 如果没有transformers Trainer用simulated training输出结构
# 实际训练在3090上运行时才加载完整transformers
try:
from transformers import Trainer, TrainingArguments, DataCollatorForLanguageModeling
import torch
from torch.utils.data import Dataset
max_steps = self.config.get("max_steps", 500)
batch_size = self.config.get("batch_size", 2)
grad_accum = self.config.get("gradient_accumulation", 4)
lr = self.config.get("learning_rate", 2e-4)
output_dir = self.config.get("output_dir", "/data/models/shuangyan-3b-hldp")
max_seq_length = self.config.get("max_seq_length", 2048)
# 准备数据集
class HLDPDataset(Dataset):
def __init__(self, texts, tokenizer, max_length):
self.encodings = tokenizer(
texts, truncation=True, padding="max_length",
max_length=max_length, return_tensors="pt"
)
def __len__(self): return len(self.encodings["input_ids"])
def __getitem__(self, idx):
return {k: v[idx] for k, v in self.encodings.items()}
dataset = HLDPDataset(texts, self.tokenizer, max_seq_length)
data_collator = DataCollatorForLanguageModeling(
tokenizer=self.tokenizer, mlm=False
)
# 训练参数
training_args = TrainingArguments(
output_dir=output_dir,
per_device_train_batch_size=batch_size,
gradient_accumulation_steps=grad_accum,
learning_rate=lr,
warmup_steps=self.config.get("warmup_steps", 100),
max_steps=max_steps,
logging_steps=5,
save_steps=self.config.get("save_steps", 50),
save_total_limit=3,
fp16=True,
report_to=[],
dataloader_pin_memory=False,
)
# 自定义callback用于进度上报
class ProgressCallback:
def __init__(self, runner, total_steps):
self.runner = runner
self.total_steps = total_steps
self.current_step = 0
self.start = time.time()
def on_log(self, args, state, control, logs=None, **kwargs):
if logs and "loss" in logs:
self.current_step = state.global_step
loss = logs["loss"]
self.runner.loss_history.append(loss)
elapsed = time.time() - self.start
# 估算ETA
if self.current_step > 0:
eta = (elapsed / self.current_step) * (self.total_steps - self.current_step)
else:
eta = 0
# 回调上报进度
cb = progress_callback or self.runner.progress_callback
if cb:
cb(self.current_step, loss, self.total_steps, {
"eta_seconds": eta,
"elapsed_seconds": elapsed,
"learning_rate": state.optimizer.param_groups[0]["lr"] if state.optimizer else None,
"loss_history": self.runner.loss_history[-50:],
})
progress_cb = ProgressCallback(self, max_steps)
trainer = Trainer(
model=self.model,
args=training_args,
train_dataset=dataset,
data_collator=data_collator,
callbacks=[progress_cb],
)
print(f"[铸渊Agent] 开始HLDP训练: {max_steps}× {batch_size}batch × {grad_accum}累积")
self.start_time = time.time()
trainer.train()
# 保存模型
trainer.save_model(output_dir)
self.tokenizer.save_pretrained(output_dir)
elapsed = time.time() - self.start_time
print(f"[铸渊Agent] HLDP训练完成耗时: {elapsed:.0f}s, 最终loss: {self.loss_history[-1] if self.loss_history else 'N/A'}")
return {
"status": "done",
"final_loss": self.loss_history[-1] if self.loss_history else None,
"steps_completed": max_steps,
"elapsed_seconds": elapsed,
"output_dir": output_dir,
"message": f"HLDP原生格式训练完成{len(texts)}条语料,{max_steps}",
}
except ImportError as e:
# 如果transformers不可用返回simulated结果用于测试仪表盘
print(f"[铸渊Agent] 训练依赖不可用: {e}")
return {
"status": "error",
"message": f"缺少训练依赖: {e}。请安装: pip install transformers peft accelerate bitsandbytes torch",
}