feiertu commited on
Commit
df61362
·
verified ·
1 Parent(s): 15879c4

Upload hermes_core/trainer.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. hermes_core/trainer.py +280 -0
hermes_core/trainer.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """QLoRA 训练 — 从训练集到 LoRA 权重."""
2
+
3
+ import json
4
+ import hashlib
5
+ import uuid
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from datetime import datetime, timezone
10
+ from hermes_core.types import HERMES_DATA_DIR, TrainingRun, TrainingStatus
11
+ from hermes_core.db import init_db, get_active_records, insert_training_run, update_training_run
12
+
13
+
14
+ MIN_RECORDS_FOR_TRAINING = 10
15
+
16
+
17
+ def _now() -> str:
18
+ return datetime.now(timezone.utc).isoformat()
19
+
20
+
21
+ def _checkpoint_dir(user_id: str, scope_id: str, version: int) -> Path:
22
+ return HERMES_DATA_DIR / "users" / user_id / "checkpoints" / scope_id / f"v{version}"
23
+
24
+
25
+ def compute_content_hash(records: list) -> str:
26
+ """计算训练集内容哈希,用于去重。"""
27
+ data = json.dumps([
28
+ {"scope_label": r.scope_label,
29
+ "dimensions": [{"k": d.key, "v": d.value} for d in r.dimensions]}
30
+ for r in sorted(records, key=lambda r: r.id)
31
+ ], sort_keys=True, ensure_ascii=False)
32
+ return hashlib.sha256(data.encode()).hexdigest()
33
+
34
+
35
+ def build_training_dataset(user_id: str, scope_id: str) -> list[dict]:
36
+ """从 DB 读取 active 记录,构建 QLoRA 训练样本。
37
+
38
+ 每条记录的 scope_label 和 dimensions 组成一条训练样本:
39
+ {
40
+ "instruction": "你是一个AI助手。根据场景和已知偏好,默认采用以下偏好。",
41
+ "input": "场景: 后端API开发",
42
+ "output": "已知偏好: [language: TypeScript] [testing: Vitest]"
43
+ }
44
+ """
45
+ conn = init_db(user_id)
46
+ records = get_active_records(conn, scope_id)
47
+ conn.close()
48
+
49
+ samples = []
50
+ for rec in records:
51
+ dims_str = " ".join(f"[{d.key}: {d.value}]" for d in rec.dimensions)
52
+ sample = {
53
+ "instruction": "你是一个AI助手。根据当前的场景标签,默认采用用户的已知偏好。",
54
+ "input": f"场景: {rec.scope_label}",
55
+ "output": f"已知偏好: {dims_str}",
56
+ }
57
+ samples.append(sample)
58
+ return samples
59
+
60
+
61
+ def run_qlora_train(
62
+ user_id: str,
63
+ scope_id: str,
64
+ base_model: str = "Qwen/Qwen2.5-7B-Instruct",
65
+ ) -> str:
66
+ """执行 QLoRA 训练。
67
+
68
+ Returns:
69
+ checkpoint_path 或空字符串(失败时)
70
+ """
71
+ conn = init_db(user_id)
72
+ records = get_active_records(conn, scope_id)
73
+
74
+ if len(records) < MIN_RECORDS_FOR_TRAINING:
75
+ conn.close()
76
+ return ""
77
+
78
+ # 去重检查
79
+ new_hash = compute_content_hash(records)
80
+ # 查最新一次训练
81
+ latest = conn.execute(
82
+ "SELECT * FROM training_runs WHERE scope_id=? AND status='done' ORDER BY version DESC LIMIT 1",
83
+ (scope_id,)
84
+ ).fetchone()
85
+ if latest and latest["content_hash"] == new_hash:
86
+ conn.close()
87
+ return ""
88
+
89
+ # 确定版本号
90
+ if latest:
91
+ version = latest["version"] + 1
92
+ else:
93
+ version = 1
94
+
95
+ run_id = f"run_{uuid.uuid4().hex[:12]}"
96
+ run = TrainingRun(
97
+ id=run_id, scope_id=scope_id, version=version,
98
+ status=TrainingStatus.pending, content_hash=new_hash,
99
+ started_at=_now(),
100
+ )
101
+ insert_training_run(conn, run)
102
+ conn.close()
103
+
104
+ # 构建训练数据
105
+ samples = build_training_dataset(user_id, scope_id)
106
+ ckpt_dir = _checkpoint_dir(user_id, scope_id, version)
107
+ ckpt_dir.mkdir(parents=True, exist_ok=True)
108
+
109
+ dataset_path = ckpt_dir / "train.jsonl"
110
+ with open(dataset_path, "w", encoding="utf-8") as f:
111
+ for s in samples:
112
+ f.write(json.dumps(s, ensure_ascii=False) + "\n")
113
+
114
+ # 写训练脚本
115
+ train_script = ckpt_dir / "train.py"
116
+ train_script.write_text("""
117
+ import json
118
+ import os
119
+ import sys
120
+ import torch
121
+ from transformers import (
122
+ AutoModelForCausalLM,
123
+ AutoTokenizer,
124
+ BitsAndBytesConfig,
125
+ TrainingArguments,
126
+ Trainer,
127
+ DataCollatorForSeq2Seq,
128
+ )
129
+ from peft import (
130
+ LoraConfig,
131
+ get_peft_model,
132
+ prepare_model_for_kbit_training,
133
+ )
134
+ from datasets import Dataset
135
+ import warnings
136
+ warnings.filterwarnings("ignore")
137
+
138
+ # ── 加载数据集 ──
139
+ dataset_path = sys.argv[1]
140
+ checkpoint_dir = sys.argv[2]
141
+ samples = []
142
+ with open(dataset_path, "r", encoding="utf-8") as f:
143
+ for line in f:
144
+ samples.append(json.loads(line))
145
+
146
+ dataset = Dataset.from_list(samples)
147
+
148
+ # ── 量化配置 ──
149
+ bnb_config = BitsAndBytesConfig(
150
+ load_in_4bit=True,
151
+ bnb_4bit_quant_type="nf4",
152
+ bnb_4bit_compute_dtype=torch.float16,
153
+ bnb_4bit_use_double_quant=True,
154
+ )
155
+
156
+ # ── 加载模型 ──
157
+ base_model = sys.argv[3] if len(sys.argv) > 3 else "Qwen/Qwen2.5-7B-Instruct"
158
+ model = AutoModelForCausalLM.from_pretrained(
159
+ base_model,
160
+ quantization_config=bnb_config,
161
+ device_map="auto",
162
+ trust_remote_code=True,
163
+ )
164
+ tokenizer = AutoTokenizer.from_pretrained(base_model, trust_remote_code=True)
165
+ if tokenizer.pad_token is None:
166
+ tokenizer.pad_token = tokenizer.eos_token
167
+
168
+ model = prepare_model_for_kbit_training(model)
169
+
170
+ # ── LoRA 配置(偏好学习优化)──
171
+ lora_config = LoraConfig(
172
+ r=8,
173
+ lora_alpha=16,
174
+ lora_dropout=0.05,
175
+ target_modules=["q_proj", "v_proj"],
176
+ task_type="CAUSAL_LM",
177
+ )
178
+ model = get_peft_model(model, lora_config)
179
+
180
+ # ── Tokenize ──
181
+ def tokenize(example):
182
+ prompt = f"{example['instruction']}\\n\\n输入: {example['input']}\\n输出: "
183
+ full = prompt + example["output"]
184
+ tokenized = tokenizer(full, truncation=True, max_length=512, padding=False)
185
+ tokenized["labels"] = tokenized["input_ids"].copy()
186
+ # mask prompt
187
+ prompt_len = len(tokenizer(prompt, truncation=True, max_length=512)["input_ids"])
188
+ tokenized["labels"][:prompt_len] = [-100] * prompt_len
189
+ return tokenized
190
+
191
+ tokenized_dataset = dataset.map(tokenize, remove_columns=dataset.column_names)
192
+
193
+ # ── 训练参数 ──
194
+ training_args = TrainingArguments(
195
+ output_dir=checkpoint_dir,
196
+ per_device_train_batch_size=1,
197
+ gradient_accumulation_steps=4,
198
+ num_train_epochs=3,
199
+ learning_rate=1e-4,
200
+ lr_scheduler_type="cosine",
201
+ warmup_ratio=0.1,
202
+ optim="paged_adamw_8bit",
203
+ logging_steps=10,
204
+ save_strategy="epoch",
205
+ fp16=True,
206
+ report_to="none",
207
+ )
208
+
209
+ trainer = Trainer(
210
+ model=model,
211
+ args=training_args,
212
+ train_dataset=tokenized_dataset,
213
+ data_collator=DataCollatorForSeq2Seq(tokenizer, pad_to_multiple_of=8),
214
+ )
215
+
216
+ trainer.train()
217
+ model.save_pretrained(checkpoint_dir)
218
+ tokenizer.save_pretrained(checkpoint_dir)
219
+ print(f"Training complete. Checkpoint saved to {checkpoint_dir}")
220
+ """, encoding="utf-8")
221
+
222
+ # ── 启动训练子进程 ──
223
+ try:
224
+ conn = init_db(user_id)
225
+ update_training_run(conn, run_id, status=TrainingStatus.training)
226
+ conn.close()
227
+
228
+ result = subprocess.run(
229
+ [sys.executable, str(train_script), str(dataset_path), str(ckpt_dir), base_model],
230
+ capture_output=True, encoding="utf-8", errors="replace", timeout=7200, # 2h max
231
+ )
232
+
233
+ conn = init_db(user_id)
234
+ if result.returncode == 0:
235
+ update_training_run(
236
+ conn, run_id,
237
+ status=TrainingStatus.done,
238
+ checkpoint_path=str(ckpt_dir),
239
+ finished_at=_now(),
240
+ )
241
+ # 更新 scope
242
+ conn.execute(
243
+ "UPDATE scopes SET needs_training=0 WHERE id=?",
244
+ (scope_id,)
245
+ )
246
+ conn.commit()
247
+ conn.close()
248
+ return str(ckpt_dir)
249
+ else:
250
+ update_training_run(
251
+ conn, run_id,
252
+ status=TrainingStatus.failed,
253
+ error_msg=result.stderr[:500],
254
+ finished_at=_now(),
255
+ )
256
+ conn.commit()
257
+ conn.close()
258
+ return ""
259
+ except subprocess.TimeoutExpired:
260
+ conn = init_db(user_id)
261
+ update_training_run(
262
+ conn, run_id,
263
+ status=TrainingStatus.failed,
264
+ error_msg="Training timed out (2h)",
265
+ finished_at=_now(),
266
+ )
267
+ conn.commit()
268
+ conn.close()
269
+ return ""
270
+ except Exception as e:
271
+ conn = init_db(user_id)
272
+ update_training_run(
273
+ conn, run_id,
274
+ status=TrainingStatus.failed,
275
+ error_msg=str(e)[:500],
276
+ finished_at=_now(),
277
+ )
278
+ conn.commit()
279
+ conn.close()
280
+ return ""