guanghulab/zhuyuan-agent/training_runner.py
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

287 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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",
}