161 lines
5.4 KiB
Python
161 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from common import ROOT, localize_workspace_path
|
|
|
|
DEFAULT_CONFIG = ROOT / "plugins" / "1c" / "training" / "configs" / "qwen3-coder-30b-a3b-lora.yaml"
|
|
|
|
|
|
def fail_missing_dependencies(exc: Exception) -> None:
|
|
raise SystemExit(
|
|
"Missing training dependencies. Install `requirements-training.txt` "
|
|
"or run inside the GPU training container. Original error: "
|
|
f"{type(exc).__name__}: {exc}"
|
|
)
|
|
|
|
|
|
def load_config(path: Path) -> dict:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
data = yaml.safe_load(handle)
|
|
if not isinstance(data, dict):
|
|
raise ValueError(f"{path} must contain a YAML mapping")
|
|
return data
|
|
|
|
|
|
def load_messages(path: Path) -> list[dict]:
|
|
rows = []
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
for line in handle:
|
|
line = line.strip()
|
|
if line:
|
|
rows.append(json.loads(line))
|
|
return rows
|
|
|
|
|
|
def build_training_texts(rows: list[dict], tokenizer) -> list[str]:
|
|
texts = []
|
|
for row in rows:
|
|
messages = row["messages"]
|
|
if hasattr(tokenizer, "apply_chat_template") and tokenizer.chat_template:
|
|
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
|
|
else:
|
|
text = "\n".join(f"{msg['role']}: {msg['content']}" for msg in messages)
|
|
texts.append(text)
|
|
return texts
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Train the 1C LoRA adapter.")
|
|
parser.add_argument("--config", type=Path, default=DEFAULT_CONFIG)
|
|
parser.add_argument("--dry-run", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
config = load_config(args.config)
|
|
base_model_path = str(localize_workspace_path(config["base_model_path"]))
|
|
dataset_path = localize_workspace_path(config["dataset_path"])
|
|
output_dir = str(localize_workspace_path(config["output_dir"]))
|
|
|
|
rows = load_messages(dataset_path)
|
|
if args.dry_run:
|
|
print(f"Config: {args.config}")
|
|
print(f"Base model: {base_model_path}")
|
|
print(f"Dataset: {dataset_path} ({len(rows)} record(s))")
|
|
print(f"Output: {output_dir}")
|
|
return 0
|
|
|
|
try:
|
|
import torch
|
|
from datasets import Dataset
|
|
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
|
from transformers import (
|
|
AutoModelForCausalLM,
|
|
AutoTokenizer,
|
|
BitsAndBytesConfig,
|
|
DataCollatorForLanguageModeling,
|
|
Trainer,
|
|
TrainingArguments,
|
|
)
|
|
except Exception as exc: # pragma: no cover
|
|
fail_missing_dependencies(exc)
|
|
|
|
if not torch.cuda.is_available():
|
|
raise SystemExit("CUDA is required for this training job.")
|
|
|
|
tokenizer = AutoTokenizer.from_pretrained(base_model_path, trust_remote_code=True)
|
|
if tokenizer.pad_token is None:
|
|
tokenizer.pad_token = tokenizer.eos_token
|
|
|
|
qconf = config.get("quantization") or {}
|
|
quantization_config = None
|
|
if qconf.get("load_in_4bit"):
|
|
quantization_config = BitsAndBytesConfig(
|
|
load_in_4bit=True,
|
|
bnb_4bit_quant_type=qconf.get("bnb_4bit_quant_type", "nf4"),
|
|
bnb_4bit_use_double_quant=bool(qconf.get("bnb_4bit_use_double_quant", True)),
|
|
bnb_4bit_compute_dtype=torch.bfloat16,
|
|
)
|
|
|
|
model = AutoModelForCausalLM.from_pretrained(
|
|
base_model_path,
|
|
trust_remote_code=True,
|
|
device_map="auto",
|
|
quantization_config=quantization_config,
|
|
)
|
|
if quantization_config is not None:
|
|
model = prepare_model_for_kbit_training(model)
|
|
|
|
lora = config["lora"]
|
|
peft_config = LoraConfig(
|
|
r=int(lora["r"]),
|
|
lora_alpha=int(lora["lora_alpha"]),
|
|
lora_dropout=float(lora["lora_dropout"]),
|
|
bias=lora.get("bias", "none"),
|
|
task_type="CAUSAL_LM",
|
|
target_modules=lora["target_modules"],
|
|
)
|
|
model = get_peft_model(model, peft_config)
|
|
|
|
texts = build_training_texts(rows, tokenizer)
|
|
dataset = Dataset.from_dict({"text": texts})
|
|
|
|
def tokenize(batch):
|
|
return tokenizer(
|
|
batch["text"],
|
|
truncation=True,
|
|
max_length=int(config["max_seq_length"]),
|
|
padding=False,
|
|
)
|
|
|
|
tokenized = dataset.map(tokenize, batched=True, remove_columns=["text"])
|
|
collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
|
|
train = config["train"]
|
|
train_args = TrainingArguments(
|
|
output_dir=output_dir,
|
|
num_train_epochs=float(train["num_train_epochs"]),
|
|
per_device_train_batch_size=int(train["per_device_train_batch_size"]),
|
|
gradient_accumulation_steps=int(train["gradient_accumulation_steps"]),
|
|
learning_rate=float(train["learning_rate"]),
|
|
warmup_ratio=float(train["warmup_ratio"]),
|
|
logging_steps=int(train["logging_steps"]),
|
|
save_strategy=train.get("save_strategy", "epoch"),
|
|
save_total_limit=int(train.get("save_total_limit", 1)),
|
|
bf16=bool(train.get("bf16", False)),
|
|
fp16=bool(train.get("fp16", False)),
|
|
report_to=[],
|
|
)
|
|
trainer = Trainer(model=model, args=train_args, train_dataset=tokenized, data_collator=collator)
|
|
trainer.train()
|
|
trainer.save_model(output_dir)
|
|
tokenizer.save_pretrained(output_dir)
|
|
print(f"Saved 1C LoRA adapter to {output_dir}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|