GhostScientist's picture
Normalize Json-decoded contents; BatchEncoding guard in length filter
1015604 verified
Raw History Blame Contribute Delete
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()