Download sw_train.py from GhostScientist/semanticwiki-dataset-v3-scripts: direct link, hf CLI and curl.
- Browser
- Download file 5.96 kB
-
https://huggingface.co/GhostScientist/semanticwiki-dataset-v3-scripts/resolve/main/sw_train.py
- Command line
-
hf download hf://GhostScientist/semanticwiki-dataset-v3-scripts/sw_train.py
-
curl -L -o sw_train.py https://huggingface.co/GhostScientist/semanticwiki-dataset-v3-scripts/resolve/main/sw_train.py
5.96 kB
| """SemanticWiki v3 - LoRA SFT of 14B-class students on citation-verified data. | |
| Uses TRL's conversational prompt-completion format: `prompt` = the system+user | |
| messages, `completion` = the assistant wiki page. Loss is computed on the | |
| completion only (TRL default for prompt-completion datasets), so the model is | |
| not trained to regurgitate the code context. | |
| Real run: | |
| python sw_train.py --base_model Qwen/Qwen2.5-Coder-14B-Instruct \ | |
| --hub_model_id GhostScientist/semanticwiki-coder-14b-v3 | |
| Smoke (CPU): add --smoke (tiny subset, 1 step, no push, no trackio). | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| from datasets import load_dataset | |
| from peft import LoraConfig | |
| from transformers import AutoTokenizer, TrainerCallback | |
| from trl import SFTConfig, SFTTrainer | |
| DATASET = "GhostScientist/semanticwiki-data-v3" | |
| MAX_LEN = 24576 # contexts are <=20k tokens; completions add ~1-2k | |
| TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj", | |
| "gate_proj", "up_proj", "down_proj"] | |
| class TrackioCallback(TrainerCallback): | |
| def on_log(self, args, state, control, logs=None, **kwargs): | |
| if logs: | |
| import trackio | |
| trackio.log({k: v for k, v in logs.items() | |
| if isinstance(v, (int, float))}, | |
| step=state.global_step) | |
| def _norm_content(c): | |
| """Arrow may infer message content as Json(decode=True), turning some string | |
| contents into e.g. ["the string"]. Normalize everything back to str.""" | |
| if isinstance(c, str): | |
| return c | |
| if isinstance(c, list): | |
| return "".join(x if isinstance(x, str) else x.get("text", "") for x in c) | |
| if c is None: | |
| return "" | |
| return json.dumps(c) | |
| def to_prompt_completion(ex): | |
| msgs = ex["messages"] | |
| if isinstance(msgs, str): # Arrow can infer the column as string-typed JSON | |
| msgs = json.loads(msgs) | |
| msgs = [{"role": m.get("role", "user"), "content": _norm_content(m.get("content"))} | |
| for m in msgs] | |
| return {"prompt": msgs[:-1], "completion": [msgs[-1]]} | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--base_model", required=True) | |
| ap.add_argument("--hub_model_id", default=None) | |
| ap.add_argument("--dataset", default=DATASET) | |
| ap.add_argument("--epochs", type=float, default=2.0) | |
| ap.add_argument("--lr", type=float, default=1e-4) | |
| ap.add_argument("--max_steps", type=int, default=-1) | |
| ap.add_argument("--max_len", type=int, default=MAX_LEN, | |
| help="override sequence length (GPU smoke uses the real 24576)") | |
| ap.add_argument("--smoke", action="store_true", | |
| help="tiny subset, 1 step, no push, no trackio, short seqs") | |
| args = ap.parse_args() | |
| tok = AutoTokenizer.from_pretrained(args.base_model) | |
| ds = load_dataset(args.dataset, split="train") | |
| assert "messages" in ds.column_names, f"unexpected columns: {ds.column_names}" | |
| max_len = 2048 if args.smoke else args.max_len | |
| pc = ds.map(to_prompt_completion, remove_columns=ds.column_names, num_proc=4) | |
| # fail fast, single process: the chat template must accept the normalized rows | |
| for ex in pc.select(range(min(2, len(pc)))): | |
| tok.apply_chat_template(ex["prompt"] + ex["completion"]) | |
| print("[train] chat-template sanity check passed on 2 rows") | |
| bad = [i for i, ex in enumerate(pc) | |
| if not (isinstance(ex["prompt"], list) and ex["prompt"] | |
| and isinstance(ex["completion"], list) and ex["completion"])] | |
| print(f"[train] rows with malformed prompt/completion: {len(bad)}") | |
| def keep(ex): | |
| ids = tok.apply_chat_template(ex["prompt"] + ex["completion"], tokenize=True) | |
| if isinstance(ids, dict): # some transformers versions return a BatchEncoding | |
| ids = ids["input_ids"] | |
| return len(ids) <= max_len | |
| n0 = len(pc) | |
| pc = pc.filter(keep, num_proc=4) | |
| print(f"[train] dataset: {n0} -> {len(pc)} rows (length <= {max_len})") | |
| assert len(pc) > 0, "length filter removed everything" | |
| if args.smoke: | |
| pc = pc.select(range(min(4, len(pc)))) | |
| space_id = None if args.smoke else os.environ.get("TRACKIO_SPACE_ID") | |
| if space_id: | |
| import trackio | |
| trackio.init(project="semanticwiki-sft-14b", space_id=space_id) | |
| # CPU smoke only: bf16 matmul unsupported here; keep bfloat16 on the GPU path. | |
| dtype = "float32" if args.smoke else "bfloat16" | |
| cfg = SFTConfig( | |
| output_dir="/work/out", | |
| per_device_train_batch_size=1, | |
| gradient_accumulation_steps=16, | |
| num_train_epochs=args.epochs, | |
| max_steps=args.max_steps, | |
| learning_rate=args.lr, | |
| lr_scheduler_type="cosine", | |
| warmup_steps=12, # ~3% of the ~400-step run (warmup_ratio removed in transformers 5.x) | |
| logging_steps=5, | |
| save_strategy="no", | |
| max_length=max_len, | |
| packing=False, | |
| report_to="none", | |
| bf16=not args.smoke, | |
| fp16=False, | |
| model_init_kwargs={"dtype": dtype, "attn_implementation": "sdpa"}, | |
| push_to_hub=bool(args.hub_model_id) and not args.smoke, | |
| hub_model_id=args.hub_model_id, | |
| seed=42, | |
| ) | |
| lora = LoraConfig( | |
| r=64, lora_alpha=128, lora_dropout=0.05, | |
| target_modules=TARGET_MODULES, task_type="CAUSAL_LM", | |
| ) | |
| trainer = SFTTrainer( | |
| model=args.base_model, | |
| args=cfg, | |
| train_dataset=pc, | |
| peft_config=lora, | |
| callbacks=[TrackioCallback()] if space_id else [], | |
| ) | |
| trainer.train() | |
| trainer.save_model("/work/out/final") | |
| print("[train] done; final loss history:", | |
| [round(log.get("loss", -1), 4) for log in trainer.state.log_history | |
| if "loss" in log][-5:]) | |
| if args.hub_model_id and not args.smoke: | |
| trainer.push_to_hub() | |
| print("[train] pushed adapter to " + args.hub_model_id) | |
| if space_id: | |
| import trackio | |
| trackio.finish() | |
| if __name__ == "__main__": | |
| main() |