Files
wehub-resource-sync 542cfa195c
CI / Frontend build (push) Failing after 9m6s
CI / Plugin validate (push) Failing after 9m27s
CI / Python lint (push) Failing after 16m1s
CI / Tests (push) Successful in 18m0s
Deploy / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:27 +08:00

141 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Build think-SFT datasets using pre-generated reasoning traces.
For each compression level (2x/3x/5x/9x), produce ShareGPT-format training
data where the assistant target is `<think>{reasoning}</think>{answer}`.
Also produces a mixed-compression dataset that concatenates all 4.
Input: think_traces_Nk.jsonl with {query, chunk_path, answer, reasoning}
Output: per-compression and mixed ShareGPT JSON + dataset_info.json
"""
from __future__ import annotations
import argparse
import json
import random
from pathlib import Path
COMPRESSIONS = ["2x", "3x", "5x", "9x"]
BASE_DATA = "/scratch/users/zwcolin/cxr_embeds/sft_data"
def format_assistant(reasoning: str, answer: str) -> str:
# Qwen3 thinking format: <think>...</think>answer
return f"<think>\n{reasoning.strip()}\n</think>\n\n{answer.strip()}"
def main():
p = argparse.ArgumentParser()
p.add_argument(
"--traces",
required=True,
help="JSONL with {query, chunk_path, answer, reasoning}",
)
p.add_argument("--output-dir", default=f"{BASE_DATA}/think_sft")
p.add_argument("--seed", type=int, default=42)
args = p.parse_args()
out = Path(args.output_dir)
out.mkdir(parents=True, exist_ok=True)
# Load traces
with open(args.traces) as f:
traces = [json.loads(line) for line in f]
traces = [t for t in traces if t.get("reasoning")]
print(f"Loaded {len(traces)} traces")
# For each compression, build think-SFT examples pointing at the compressed image
info = {}
rng = random.Random(args.seed)
per_comp = {}
for c in COMPRESSIONS:
c_dir = Path(f"{BASE_DATA}/compressed_{c}/images")
data = []
skipped = 0
for t in traces:
# Image path: compressed_Nx/images/<src_rel>
src_rel = t["chunk_path"]
img_path = str(c_dir / src_rel)
if not Path(img_path).exists():
skipped += 1
continue
data.append(
{
"messages": [
{"role": "user", "content": "<image>\n" + t["query"]},
{
"role": "assistant",
"content": format_assistant(t["reasoning"], t["answer"]),
},
],
"images": [img_path],
}
)
print(f" {c}: {len(data)} ({skipped} skipped)")
out_json = out / f"train_{c}.json"
out_json.write_text(json.dumps(data, ensure_ascii=False))
per_comp[c] = data
info[f"think_train_{c}"] = {
"file_name": str(out_json),
"formatting": "sharegpt",
"columns": {"messages": "messages", "images": "images"},
"tags": {
"role_tag": "role",
"content_tag": "content",
"user_tag": "user",
"assistant_tag": "assistant",
},
}
# Mixed dataset: concat all 4
mixed = []
for c in COMPRESSIONS:
mixed.extend(per_comp[c])
rng.shuffle(mixed)
print(f" mixed: {len(mixed)}")
mixed_json = out / "train_mixed.json"
mixed_json.write_text(json.dumps(mixed, ensure_ascii=False))
info["think_train_mixed"] = {
"file_name": str(mixed_json),
"formatting": "sharegpt",
"columns": {"messages": "messages", "images": "images"},
"tags": {
"role_tag": "role",
"content_tag": "content",
"user_tag": "user",
"assistant_tag": "assistant",
},
}
# Reuse existing eval sets (no reasoning needed, we still eval on plain Q→A)
# ... actually to monitor think loss we should have think eval too. Let's skip for now —
# just point eval_dataset at existing compressed_qa_eval per compression if needed.
# For simplicity, write a small think_eval (5x only) from first 500 eval examples with reasoning.
# Actually: eval on plain answer is fine, training loss monitors itself.
# For LF, point eval_dataset to the existing non-think eval of the matching compression.
# We reference those via symlink or just list them here as mixed_eval_Nx
for c in COMPRESSIONS:
info[f"think_eval_{c}"] = {
"file_name": f"{BASE_DATA}/compressed_{c}/eval.json",
"formatting": "sharegpt",
"columns": {"messages": "messages", "images": "images"},
"tags": {
"role_tag": "role",
"content_tag": "content",
"user_tag": "user",
"assistant_tag": "assistant",
},
}
info_path = out / "dataset_info.json"
info_path.write_text(json.dumps(info, indent=2))
print(f"\nDataset info: {info_path}")
if __name__ == "__main__":
main()