gamedevelop's picture
Update chatcode/train.py
ec9ed0b verified
Raw History Blame Contribute Delete
7.88 kB
"""Cloud GPU QLoRA training. Never falls back to training on the user's CPU."""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
from .data import encode_example, normalize_messages, prompt_key
from .runtime import CONFIG
def read_data(folder):
manifest = json.loads((folder / "manifest.json").read_text(encoding="utf-8"))
if manifest["config"] != CONFIG:
raise ValueError("Data manifest does not match the pinned experiment configuration.")
if "article_examples_sha256" in manifest:
current = hashlib.sha256(Path(__file__).with_name("article_examples.json").read_bytes()).hexdigest()
if manifest["article_examples_sha256"] != current:
raise ValueError("Article behavior examples changed after preparation.")
records, seen, groups = {}, set(), {}
for split in ["train", "validation", "test"]:
raw = (folder / f"{split}.jsonl").read_bytes()
if hashlib.sha256(raw).hexdigest() != manifest["sha256"][split]:
raise ValueError(f"Data changed after preparation: {split}")
records[split] = [json.loads(line) for line in raw.decode().splitlines() if line]
if not records[split] or len(records[split]) != manifest["counts"][split]:
raise ValueError(f"Unexpected split size: {split}")
for row in records[split]:
key = prompt_key(normalize_messages(row))
if key != row["prompt_hash"] or key in seen:
raise ValueError("Duplicate or inconsistent prompts across dataset splits.")
seen.add(key)
group = row.get("group_id", key)
if group in groups and groups[group] != split:
raise ValueError("A source packet appears in multiple dataset splits.")
groups[group] = split
return records
def collate(examples, pad_token_id):
import torch
length = max(len(item["input_ids"]) for item in examples)
return {key: torch.tensor([item[key] + [pad] * (length - len(item[key])) for item in examples])
for key, pad in [("input_ids", pad_token_id), ("attention_mask", 0), ("labels", -100)]}
def train(args):
import torch
if not torch.cuda.is_available():
raise RuntimeError("Cloud CUDA GPU required. No training or model download was started.")
from datasets import Dataset
from peft import LoraConfig, TaskType, get_peft_model, prepare_model_for_kbit_training
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from transformers import Trainer, TrainingArguments, set_seed
output = Path(args.output)
if output.exists() and any(output.iterdir()) and not args.resume_from_checkpoint:
raise FileExistsError("Use an empty output directory or explicitly resume a checkpoint.")
records = read_data(Path(args.data))
set_seed(CONFIG["seed"])
tokenizer = AutoTokenizer.from_pretrained(
CONFIG["base_model"], revision=CONFIG["base_revision"], trust_remote_code=False)
tokenizer.padding_side = "right"
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
base = AutoModelForCausalLM.from_pretrained(
CONFIG["base_model"], revision=CONFIG["base_revision"], trust_remote_code=False,
dtype=dtype, device_map={"": torch.cuda.current_device()}, attn_implementation="sdpa",
quantization_config=BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=dtype))
base.config.use_cache = False
base = prepare_model_for_kbit_training(base, use_gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False})
model = get_peft_model(base, LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05,
target_modules="all-linear", bias="none", task_type=TaskType.CAUSAL_LM))
model.peft_config["default"].base_model_name_or_path = CONFIG["base_model"]
model.peft_config["default"].revision = CONFIG["base_revision"]
datasets = {split: Dataset.from_list([
encode_example(tokenizer, row["messages"], CONFIG["max_sequence_length"]) for row in records[split]
]) for split in ["train", "validation"]}
settings = TrainingArguments(
output_dir=str(output), num_train_epochs=1, max_steps=args.max_steps,
per_device_train_batch_size=1, per_device_eval_batch_size=1,
gradient_accumulation_steps=16, learning_rate=5e-5, warmup_ratio=0.05,
lr_scheduler_type="cosine", logging_steps=5, eval_strategy="steps", eval_steps=25,
save_strategy="steps", save_steps=25, save_total_limit=2,
load_best_model_at_end=True, metric_for_best_model="eval_loss",
greater_is_better=False, bf16=dtype == torch.bfloat16, fp16=dtype == torch.float16,
gradient_checkpointing=True, gradient_checkpointing_kwargs={"use_reentrant": False},
optim="adamw_torch", report_to="none", seed=CONFIG["seed"],
dataloader_num_workers=0, remove_unused_columns=False)
trainer = Trainer(model=model, args=settings, processing_class=tokenizer,
train_dataset=datasets["train"], eval_dataset=datasets["validation"],
data_collator=lambda batch: collate(batch, tokenizer.pad_token_id))
baseline = trainer.evaluate()
trained = trainer.train(resume_from_checkpoint=args.resume_from_checkpoint)
final = trainer.evaluate()
destination = output / "adapter"
trainer.save_model(str(destination))
tokenizer.save_pretrained(destination)
shutil.copy2(Path(args.data) / "manifest.json", destination / "data_manifest.json")
report = {"config": CONFIG, "baseline_validation": baseline, "final_validation": final,
"training": trained.metrics, "counts": {k: len(v) for k, v in records.items()},
"status": "candidate_requires_chat_code_and_article_evaluation", "max_steps": args.max_steps}
(destination / "training_report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
(destination / "README.md").write_text(
"---\nlicense: gpl-3.0\nlibrary_name: peft\npipeline_tag: text-generation\n"
f"base_model: {CONFIG['base_model']}\nbase_model_relation: adapter\n"
"datasets:\n- HuggingFaceTB/smoltalk\ntags:\n- lora\n- code\n- experimental\n---\n\n"
"# PocketVision Chat, Code & Writing candidate\n\nText-only QLoRA adapter. "
"This candidate has validation-loss measurements only; no claim of better chat, coding or factual article quality. "
"See training_report.json and data_manifest.json. "
"Training data: Smol-Magpie-Ultra, Smol-rewrite, Smol-summarize (Apache-2.0), authored fictional article behavior examples (GPL-3.0-only), and the Self-OSS-Instruct subset "
"(original: bigcode/self-oss-instruct-sc2-exec-filter-50k, ODC-BY). "
"Original base weights remain Apache-2.0; project code and adapter use GPL-3.0-only.\n",
encoding="utf-8")
for source in ["LICENSE", "LICENSE-BASE-MODEL.txt"]:
shutil.copy2(Path(__file__).resolve().parent.parent / source, destination / source)
print(f"Candidate saved to {destination}. Evaluate answers before publishing a release.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Train text-only Chat & Code on a cloud CUDA GPU")
parser.add_argument("--data", default="chatcode-data")
parser.add_argument("--output", required=True, help="Prefer mounted persistent cloud storage")
parser.add_argument("--max-steps", type=int, default=-1, help="25 for a smoke run; -1 for one epoch")
parser.add_argument("--resume-from-checkpoint")
args = parser.parse_args()
if args.max_steps != -1 and args.max_steps < 25:
parser.error("Use at least 25 steps so validation and checkpoint saving occur.")
train(args)