Text Generation
PEFT
Chinese
English
preference-learning
qlora
agent
personalization
association-engine
Instructions to use feiertu/hermes-association-engine with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use feiertu/hermes-association-engine with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
| """QLoRA 训练 — 从训练集到 LoRA 权重.""" | |
| import json | |
| import hashlib | |
| import uuid | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| from datetime import datetime, timezone | |
| from hermes_core.types import HERMES_DATA_DIR, TrainingRun, TrainingStatus | |
| from hermes_core.db import init_db, get_active_records, insert_training_run, update_training_run | |
| MIN_RECORDS_FOR_TRAINING = 10 | |
| def _now() -> str: | |
| return datetime.now(timezone.utc).isoformat() | |
| def _checkpoint_dir(user_id: str, scope_id: str, version: int) -> Path: | |
| return HERMES_DATA_DIR / "users" / user_id / "checkpoints" / scope_id / f"v{version}" | |
| def compute_content_hash(records: list) -> str: | |
| """计算训练集内容哈希,用于去重。""" | |
| data = json.dumps([ | |
| {"scope_label": r.scope_label, | |
| "dimensions": [{"k": d.key, "v": d.value} for d in r.dimensions]} | |
| for r in sorted(records, key=lambda r: r.id) | |
| ], sort_keys=True, ensure_ascii=False) | |
| return hashlib.sha256(data.encode()).hexdigest() | |
| def build_training_dataset(user_id: str, scope_id: str) -> list[dict]: | |
| """从 DB 读取 active 记录,构建 QLoRA 训练样本。 | |
| 每条记录的 scope_label 和 dimensions 组成一条训练样本: | |
| { | |
| "instruction": "你是一个AI助手。根据场景和已知偏好,默认采用以下偏好。", | |
| "input": "场景: 后端API开发", | |
| "output": "已知偏好: [language: TypeScript] [testing: Vitest]" | |
| } | |
| """ | |
| conn = init_db(user_id) | |
| records = get_active_records(conn, scope_id) | |
| conn.close() | |
| samples = [] | |
| for rec in records: | |
| dims_str = " ".join(f"[{d.key}: {d.value}]" for d in rec.dimensions) | |
| sample = { | |
| "instruction": "你是一个AI助手。根据当前的场景标签,默认采用用户的已知偏好。", | |
| "input": f"场景: {rec.scope_label}", | |
| "output": f"已知偏好: {dims_str}", | |
| } | |
| samples.append(sample) | |
| return samples | |
| def run_qlora_train( | |
| user_id: str, | |
| scope_id: str, | |
| base_model: str = "Qwen/Qwen2.5-7B-Instruct", | |
| ) -> str: | |
| """执行 QLoRA 训练。 | |
| Returns: | |
| checkpoint_path 或空字符串(失败时) | |
| """ | |
| conn = init_db(user_id) | |
| records = get_active_records(conn, scope_id) | |
| if len(records) < MIN_RECORDS_FOR_TRAINING: | |
| conn.close() | |
| return "" | |
| # 去重检查 | |
| new_hash = compute_content_hash(records) | |
| # 查最新一次训练 | |
| latest = conn.execute( | |
| "SELECT * FROM training_runs WHERE scope_id=? AND status='done' ORDER BY version DESC LIMIT 1", | |
| (scope_id,) | |
| ).fetchone() | |
| if latest and latest["content_hash"] == new_hash: | |
| conn.close() | |
| return "" | |
| # 确定版本号 | |
| if latest: | |
| version = latest["version"] + 1 | |
| else: | |
| version = 1 | |
| run_id = f"run_{uuid.uuid4().hex[:12]}" | |
| run = TrainingRun( | |
| id=run_id, scope_id=scope_id, version=version, | |
| status=TrainingStatus.pending, content_hash=new_hash, | |
| started_at=_now(), | |
| ) | |
| insert_training_run(conn, run) | |
| conn.close() | |
| # 构建训练数据 | |
| samples = build_training_dataset(user_id, scope_id) | |
| ckpt_dir = _checkpoint_dir(user_id, scope_id, version) | |
| ckpt_dir.mkdir(parents=True, exist_ok=True) | |
| dataset_path = ckpt_dir / "train.jsonl" | |
| with open(dataset_path, "w", encoding="utf-8") as f: | |
| for s in samples: | |
| f.write(json.dumps(s, ensure_ascii=False) + "\n") | |
| # 写训练脚本 | |
| train_script = ckpt_dir / "train.py" | |
| train_script.write_text(""" | |
| import json | |
| import os | |
| import sys | |
| import torch | |
| from transformers import ( | |
| AutoModelForCausalLM, | |
| AutoTokenizer, | |
| BitsAndBytesConfig, | |
| TrainingArguments, | |
| Trainer, | |
| DataCollatorForSeq2Seq, | |
| ) | |
| from peft import ( | |
| LoraConfig, | |
| get_peft_model, | |
| prepare_model_for_kbit_training, | |
| ) | |
| from datasets import Dataset | |
| import warnings | |
| warnings.filterwarnings("ignore") | |
| # ── 加载数据集 ── | |
| dataset_path = sys.argv[1] | |
| checkpoint_dir = sys.argv[2] | |
| samples = [] | |
| with open(dataset_path, "r", encoding="utf-8") as f: | |
| for line in f: | |
| samples.append(json.loads(line)) | |
| dataset = Dataset.from_list(samples) | |
| # ── 量化配置 ── | |
| bnb_config = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_quant_type="nf4", | |
| bnb_4bit_compute_dtype=torch.float16, | |
| bnb_4bit_use_double_quant=True, | |
| ) | |
| # ── 加载模型 ── | |
| base_model = sys.argv[3] if len(sys.argv) > 3 else "Qwen/Qwen2.5-7B-Instruct" | |
| model = AutoModelForCausalLM.from_pretrained( | |
| base_model, | |
| quantization_config=bnb_config, | |
| device_map="auto", | |
| trust_remote_code=True, | |
| ) | |
| tokenizer = AutoTokenizer.from_pretrained(base_model, trust_remote_code=True) | |
| if tokenizer.pad_token is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| model = prepare_model_for_kbit_training(model) | |
| # ── LoRA 配置(偏好学习优化)── | |
| lora_config = LoraConfig( | |
| r=8, | |
| lora_alpha=16, | |
| lora_dropout=0.05, | |
| target_modules=["q_proj", "v_proj"], | |
| task_type="CAUSAL_LM", | |
| ) | |
| model = get_peft_model(model, lora_config) | |
| # ── Tokenize ── | |
| def tokenize(example): | |
| prompt = f"{example['instruction']}\\n\\n输入: {example['input']}\\n输出: " | |
| full = prompt + example["output"] | |
| tokenized = tokenizer(full, truncation=True, max_length=512, padding=False) | |
| tokenized["labels"] = tokenized["input_ids"].copy() | |
| # mask prompt | |
| prompt_len = len(tokenizer(prompt, truncation=True, max_length=512)["input_ids"]) | |
| tokenized["labels"][:prompt_len] = [-100] * prompt_len | |
| return tokenized | |
| tokenized_dataset = dataset.map(tokenize, remove_columns=dataset.column_names) | |
| # ── 训练参数 ── | |
| training_args = TrainingArguments( | |
| output_dir=checkpoint_dir, | |
| per_device_train_batch_size=1, | |
| gradient_accumulation_steps=4, | |
| num_train_epochs=3, | |
| learning_rate=1e-4, | |
| lr_scheduler_type="cosine", | |
| warmup_ratio=0.1, | |
| optim="paged_adamw_8bit", | |
| logging_steps=10, | |
| save_strategy="epoch", | |
| fp16=True, | |
| report_to="none", | |
| ) | |
| trainer = Trainer( | |
| model=model, | |
| args=training_args, | |
| train_dataset=tokenized_dataset, | |
| data_collator=DataCollatorForSeq2Seq(tokenizer, pad_to_multiple_of=8), | |
| ) | |
| trainer.train() | |
| model.save_pretrained(checkpoint_dir) | |
| tokenizer.save_pretrained(checkpoint_dir) | |
| print(f"Training complete. Checkpoint saved to {checkpoint_dir}") | |
| """, encoding="utf-8") | |
| # ── 启动训练子进程 ── | |
| try: | |
| conn = init_db(user_id) | |
| update_training_run(conn, run_id, status=TrainingStatus.training) | |
| conn.close() | |
| result = subprocess.run( | |
| [sys.executable, str(train_script), str(dataset_path), str(ckpt_dir), base_model], | |
| capture_output=True, encoding="utf-8", errors="replace", timeout=7200, # 2h max | |
| ) | |
| conn = init_db(user_id) | |
| if result.returncode == 0: | |
| update_training_run( | |
| conn, run_id, | |
| status=TrainingStatus.done, | |
| checkpoint_path=str(ckpt_dir), | |
| finished_at=_now(), | |
| ) | |
| # 更新 scope | |
| conn.execute( | |
| "UPDATE scopes SET needs_training=0 WHERE id=?", | |
| (scope_id,) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return str(ckpt_dir) | |
| else: | |
| update_training_run( | |
| conn, run_id, | |
| status=TrainingStatus.failed, | |
| error_msg=result.stderr[:500], | |
| finished_at=_now(), | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return "" | |
| except subprocess.TimeoutExpired: | |
| conn = init_db(user_id) | |
| update_training_run( | |
| conn, run_id, | |
| status=TrainingStatus.failed, | |
| error_msg="Training timed out (2h)", | |
| finished_at=_now(), | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return "" | |
| except Exception as e: | |
| conn = init_db(user_id) | |
| update_training_run( | |
| conn, run_id, | |
| status=TrainingStatus.failed, | |
| error_msg=str(e)[:500], | |
| finished_at=_now(), | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return "" | |