Files
fareedkhan-dev--train-llm-f…/scripts/prepare_sft_data.py
T
wehub-resource-sync a8262fc01e
docs / build (push) Waiting to run
docs / deploy (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:10:22 +08:00

128 lines
5.1 KiB
Python

"""
Build the SFT dataset from real public instruction data + GSM8K, render it through the
chat template (masking prompt tokens), pack to fixed-length rows, and write a packed
HDF5 (``tokens`` + ``loss_mask``) plus a held-out dev file.
Datasets (downloaded via HuggingFace ``datasets`` to /ephemeral/hf_cache):
- tatsu-lab/alpaca (general instruction following)
- databricks/databricks-dolly-15k (general instruction following)
- openai/gsm8k (main, train) (math, reformatted into <think>/<answer> so the model
learns the reasoning format the RL verifier rewards)
Example:
PYTHONPATH=. HF_HOME=/ephemeral/hf_cache python scripts/prepare_sft_data.py \
--context_length 1024 --out_dir /ephemeral/data
"""
from __future__ import annotations
import argparse
import os
import re
import h5py
import numpy as np
from src.post_training.chat_template import encode_chat, ANSWER_OPEN, ANSWER_CLOSE, THINK_OPEN, THINK_CLOSE
from src.post_training.sft import pack_examples
os.environ.setdefault("HF_HOME", "/ephemeral/hf_cache")
_CALC_RE = re.compile(r"<<[^>]*>>") # GSM8K calculator annotations
_HASH_RE = re.compile(r"####\s*(.+)\s*$")
def gsm8k_to_messages(question: str, answer: str) -> list[dict]:
"""Reformat a GSM8K (question, answer) into chat messages whose assistant turn uses
the <think>...</think><answer>N</answer> structure."""
answer = _CALC_RE.sub("", answer).strip()
m = _HASH_RE.search(answer)
final = m.group(1).strip() if m else answer
reasoning = _HASH_RE.sub("", answer).strip()
completion = f"{THINK_OPEN}{reasoning}{THINK_CLOSE}{ANSWER_OPEN}{final}{ANSWER_CLOSE}"
return [{"role": "user", "content": question.strip()},
{"role": "assistant", "content": completion}]
def alpaca_to_messages(ex: dict) -> list[dict]:
instr = ex["instruction"].strip()
inp = (ex.get("input") or "").strip()
user = f"{instr}\n\n{inp}" if inp else instr
return [{"role": "user", "content": user}, {"role": "assistant", "content": ex["output"].strip()}]
def dolly_to_messages(ex: dict) -> list[dict]:
instr = ex["instruction"].strip()
ctx = (ex.get("context") or "").strip()
user = f"{instr}\n\n{ctx}" if ctx else instr
return [{"role": "user", "content": user}, {"role": "assistant", "content": ex["response"].strip()}]
def collect_examples(context_length: int, limit_per_set: int | None) -> list[tuple[list[int], list[int]]]:
from datasets import load_dataset
examples: list[tuple[list[int], list[int]]] = []
n_kept = n_skipped = 0
def add(messages):
nonlocal n_kept, n_skipped
ids, mask = encode_chat(messages)
if len(ids) <= context_length and sum(mask) > 0:
examples.append((ids, mask))
n_kept += 1
else:
n_skipped += 1
print("Loading tatsu-lab/alpaca ...")
alpaca = load_dataset("tatsu-lab/alpaca", split="train")
for ex in (alpaca.select(range(min(limit_per_set, len(alpaca)))) if limit_per_set else alpaca):
add(alpaca_to_messages(ex))
print("Loading databricks/databricks-dolly-15k ...")
try:
dolly = load_dataset("databricks/databricks-dolly-15k", split="train")
for ex in (dolly.select(range(min(limit_per_set, len(dolly)))) if limit_per_set else dolly):
add(dolly_to_messages(ex))
except Exception as e: # noqa: BLE001
print(f" (skipping dolly: {e})")
print("Loading openai/gsm8k (main/train) ...")
gsm = load_dataset("openai/gsm8k", "main", split="train")
for ex in (gsm.select(range(min(limit_per_set, len(gsm)))) if limit_per_set else gsm):
add(gsm8k_to_messages(ex["question"], ex["answer"]))
print(f"Collected {n_kept} examples (skipped {n_skipped} that exceeded context_length).")
return examples
def write_packed(examples, context_length: int, out_path: str) -> int:
tokens, masks = pack_examples(examples, context_length)
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
with h5py.File(out_path, "w") as f:
f.create_dataset("tokens", data=tokens)
f.create_dataset("loss_mask", data=masks)
print(f" wrote {tokens.shape[0]} packed rows x {context_length} -> {out_path}")
return tokens.shape[0]
def main():
p = argparse.ArgumentParser()
p.add_argument("--context_length", type=int, default=1024)
p.add_argument("--out_dir", default="/ephemeral/data")
p.add_argument("--dev_frac", type=float, default=0.02)
p.add_argument("--limit_per_set", type=int, default=None, help="cap examples per dataset (debug)")
p.add_argument("--seed", type=int, default=42)
args = p.parse_args()
examples = collect_examples(args.context_length, args.limit_per_set)
rng = np.random.default_rng(args.seed)
rng.shuffle(examples)
n_dev = max(1, int(len(examples) * args.dev_frac))
dev, train = examples[:n_dev], examples[n_dev:]
write_packed(train, args.context_length, os.path.join(args.out_dir, "sft_packed.h5"))
write_packed(dev, args.context_length, os.path.join(args.out_dir, "sft_dev_packed.h5"))
if __name__ == "__main__":
main()