feliksier's picture
download
raw
12.5 kB
"""
JustGRPO reproduction script — all three claims in one GPU run.
Claim 1: JustGRPO achieves 89.1% on GSM8K (gen_length=256, steps=256, block_length=32).
Claim 2: Retains parallel decoding (fully-parallel block_length=256 still works).
Claim 3: Arbitrary order (parallel) collapses solution coverage (Pass@k) vs AR order.
Single-GPU (no torchrun). Uses the official JustGRPO utils (generate, grader, parser).
"""
import argparse
import json
import os
import sys
import time
import torch
from datasets import load_dataset
from transformers import AutoModel, AutoTokenizer
from tqdm import tqdm
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from utils.generate import generate
from utils.grader import math_equal
from utils.parser import extract_answer
def extract_answer_gsm8k(answer: str):
"""Extract the final answer from GSM8K format (after ####)."""
return answer.split('####')[-1].strip()
def load_model_tokenizer(ckpt_path, base_path, device):
print(f"[load] tokenizer from {base_path}", flush=True)
tokenizer = AutoTokenizer.from_pretrained(base_path, trust_remote_code=True)
print(f"[load] model from {ckpt_path} (bf16)", flush=True)
model = AutoModel.from_pretrained(
ckpt_path, trust_remote_code=True, torch_dtype=torch.bfloat16
)
model.eval().requires_grad_(False).to(device)
print(f"[load] done. #params={sum(p.numel() for p in model.parameters())/1e9:.2f}B", flush=True)
return model, tokenizer
def build_prompt_ids(tokenizer, question, device):
msgs = [[{"role": "user", "content": question}]]
prompt_text = tokenizer.apply_chat_template(
msgs, add_generation_prompt=True, tokenize=False
)
prompt_ids = tokenizer(prompt_text, return_tensors="pt")["input_ids"].to(device)
return prompt_ids
def grade_gsm8k(gt_answer, response):
"""gt_answer: raw GSM8K 'answer' field (with ####). response: model text."""
return bool(math_equal(extract_answer_gsm8k(gt_answer), extract_answer(response)))
# ---------------- Claim 1 ----------------
def claim1(model, tokenizer, device, args, out_dir):
"""GSM8K accuracy with paper config: block_length=32, gen_length=256, steps=256, temp=0."""
print("\n" + "=" * 70, flush=True)
print("CLAIM 1: GSM8K accuracy (block_length=32, gen=256, steps=256, temp=0)", flush=True)
print("=" * 70, flush=True)
ds = load_dataset("gsm8k", "main", split="test")
# Seeded representative subset (no clustering bias) of size N.
n = min(args.c1_n, len(ds))
rng = torch.Generator().manual_seed(args.seed)
idxs = torch.randperm(len(ds), generator=rng)[:n].tolist()
print(f"[c1] evaluating {n} / {len(ds)} examples", flush=True)
correct, total = 0, 0
t0 = time.time()
rows = []
for k, i in enumerate(tqdm(idxs, desc="c1")):
ex = ds[int(i)]
q, a = ex["question"], ex["answer"]
pid = build_prompt_ids(tokenizer, q, device)
gen = generate(model=model, prompt=pid, steps=args.gen_length,
gen_length=args.gen_length, block_length=32, temperature=0.0)
resp = tokenizer.batch_decode(gen[:, pid.shape[1]:], skip_special_tokens=True)[0]
ok = grade_gsm8k(a, resp)
correct += int(ok); total += 1
rows.append({"idx": int(i), "correct": int(ok),
"gt": extract_answer_gsm8k(a), "pred": extract_answer(resp)[:120]})
if (k + 1) % 50 == 0:
acc = correct / total
el = time.time() - t0
eta = el / (k + 1) * (n - k - 1)
print(f"[c1] {k+1}/{n} acc={acc*100:.2f}% elapsed={el:.0f}s eta={eta:.0f}s", flush=True)
acc = correct / total
el = time.time() - t0
res = {"claim": 1, "task": "gsm8k", "ckpt": args.ckpt_path,
"block_length": 32, "gen_length": args.gen_length, "steps": args.gen_length,
"temperature": 0.0, "n": n, "correct": correct, "total": total,
"accuracy": acc, "paper_reported": 0.891, "elapsed_sec": el,
"sample_results": rows[:20]}
with open(os.path.join(out_dir, "claim1_gsm8k.json"), "w") as f:
json.dump(res, f, indent=2)
print(f"[c1] FINAL acc={acc*100:.2f}% ({correct}/{total}) paper=89.10% elapsed={el:.0f}s", flush=True)
return res
# ---------------- Claim 2 ----------------
def claim2(model, tokenizer, device, args, out_dir):
"""Parallel decoding retained: compare block_length=256 (fully parallel) vs
block_length=1 (pure AR) vs block_length=32 (semi-AR paper default) on same subset."""
print("\n" + "=" * 70, flush=True)
print("CLAIM 2: Parallel decoding retained (block_length sweep, temp=0)", flush=True)
print("=" * 70, flush=True)
ds = load_dataset("gsm8k", "main", split="test")
n = min(args.c2_n, len(ds))
rng = torch.Generator().manual_seed(args.seed)
idxs = torch.randperm(len(ds), generator=rng)[:n].tolist()
print(f"[c2] evaluating {n} examples under 3 block_length settings", flush=True)
results = {}
for bl in [32, 256, 1]: # semi-AR (paper default), fully-parallel, pure-AR
label = {32: "semi_AR_blk32", 256: "fully_parallel_blk256", 1: "pure_AR_blk1"}[bl]
correct, total = 0, 0
t0 = time.time()
for k, i in enumerate(tqdm(idxs, desc=f"c2-{label}")):
ex = ds[int(i)]
q, a = ex["question"], ex["answer"]
pid = build_prompt_ids(tokenizer, q, device)
gen = generate(model=model, prompt=pid, steps=args.gen_length,
gen_length=args.gen_length, block_length=bl, temperature=0.0)
resp = tokenizer.batch_decode(gen[:, pid.shape[1]:], skip_special_tokens=True)[0]
ok = grade_gsm8k(a, resp)
correct += int(ok); total += 1
acc = correct / total
el = time.time() - t0
results[label] = {"block_length": bl, "n": n, "correct": correct,
"total": total, "accuracy": acc, "elapsed_sec": el}
print(f"[c2] {label}: acc={acc*100:.2f}% ({correct}/{total}) elapsed={el:.0f}s", flush=True)
res = {"claim": 2, "task": "gsm8k", "ckpt": args.ckpt_path,
"gen_length": args.gen_length, "steps": args.gen_length,
"n": n, "modes": results,
"interpretation": (
"If fully_parallel_blk256 accuracy is close to semi_AR_blk32 and pure_AR_blk1, "
"the model trained with AR order RETAINS parallel decoding ability at inference.")}
with open(os.path.join(out_dir, "claim2_parallel.json"), "w") as f:
json.dump(res, f, indent=2)
return res
# ---------------- Claim 3 ----------------
def pass_at_k(n, c, k):
"""Unbiased pass@k (Chen et al. 2021). n=samples, c=correct, k=requested."""
import math
if n - c < k:
return 1.0
return 1.0 - math.prod((n - c - j) / (n - j) for j in range(k))
def claim3(model, tokenizer, device, args, out_dir):
"""Solution coverage collapse: Pass@k of AR order (block_length=1) vs
arbitrary order (block_length=256, low_confidence) at temperature>0."""
print("\n" + "=" * 70, flush=True)
print(f"CLAIM 3: Solution coverage — Pass@{args.c3_k} AR vs arbitrary (temp={args.c3_temp})", flush=True)
print("=" * 70, flush=True)
ds = load_dataset("gsm8k", "main", split="test")
n_prob = min(args.c3_problems, len(ds))
rng = torch.Generator().manual_seed(args.seed)
idxs = torch.randperm(len(ds), generator=rng)[:n_prob].tolist()
K = args.c3_k
print(f"[c3] {n_prob} problems x {K} samples x 2 modes (AR blk1, arbitrary blk256)", flush=True)
modes = {"AR_order_blk1": 1, "arbitrary_order_blk256": 256}
summary = {}
for mode, bl in modes.items():
per_problem = [] # list of (n_correct, n_total)
t0 = time.time()
for pi, i in enumerate(tqdm(idxs, desc=f"c3-{mode}")):
ex = ds[int(i)]
q, a = ex["question"], ex["answer"]
pid = build_prompt_ids(tokenizer, q, device)
# generate K samples in one batch (identical prompt -> supported by generate)
gen = generate(model=model, prompt=pid.repeat(K, 1), steps=args.gen_length,
gen_length=args.gen_length, block_length=bl, temperature=args.c3_temp)
resps = tokenizer.batch_decode(gen[:, pid.shape[1]:], skip_special_tokens=True)
n_correct = sum(grade_gsm8k(a, r) for r in resps)
per_problem.append((n_correct, K))
# aggregate pass@k
pk = [pass_at_k(K, c, K) for (c, _) in per_problem] # pass@K = any correct in K
passk = sum(pk) / len(pk)
# also greedy-style acc (sample-1 correctness rate)
acc1 = sum(c / K for c, _ in per_problem) / len(per_problem)
# exact pass@1 over all samples
all_correct = sum(c for c, _ in per_problem)
all_total = sum(t for _, t in per_problem)
sample_acc = all_correct / all_total
el = time.time() - t0
summary[mode] = {"block_length": bl, "n_problems": n_prob, "k": K,
"pass_at_k": passk, "sample_accuracy": sample_acc,
"mean_per_problem_acc": acc1,
"n_correct_samples": all_correct, "n_total_samples": all_total,
"elapsed_sec": el,
"per_problem": [{"n_correct": c, "n_total": t} for c, t in per_problem]}
print(f"[c3] {mode}: pass@{K}={passk*100:.2f}% sample_acc={sample_acc*100:.2f}% elapsed={el:.0f}s", flush=True)
ar_pk = summary["AR_order_blk1"]["pass_at_k"]
arb_pk = summary["arbitrary_order_blk256"]["pass_at_k"]
collapse = arb_pk < ar_pk
res = {"claim": 3, "task": "gsm8k", "ckpt": args.ckpt_path,
"gen_length": args.gen_length, "steps": args.gen_length,
"n_problems": n_prob, "k": K, "temperature": args.c3_temp,
"modes": summary,
"AR_pass_at_k": ar_pk, "arbitrary_pass_at_k": arb_pk,
"arbitrary_collapses_coverage": bool(collapse),
"relative_drop_pct": (ar_pk - arb_pk) / (ar_pk + 1e-9) * 100 if ar_pk > 0 else 0.0,
"interpretation": (
"If arbitrary_order pass@k < AR_order pass@k, arbitrary-order generation "
"collapses solution coverage, supporting the paper's core claim.")}
with open(os.path.join(out_dir, "claim3_coverage.json"), "w") as f:
json.dump(res, f, indent=2)
print(f"[c3] AR pass@{K}={ar_pk*100:.2f}% arbitrary pass@{K}={arb_pk*100:.2f}% "
f"collapse={collapse} drop={res['relative_drop_pct']:.1f}%", flush=True)
return res
def main():
p = argparse.ArgumentParser()
p.add_argument("--ckpt_path", default="/ckpt")
p.add_argument("--base_path", default="/base")
p.add_argument("--output", default="/outputs")
p.add_argument("--gen_length", type=int, default=256)
p.add_argument("--seed", type=int, default=113)
p.add_argument("--c1_n", type=int, default=500)
p.add_argument("--c2_n", type=int, default=100)
p.add_argument("--c3_problems", type=int, default=30)
p.add_argument("--c3_k", type=int, default=4)
p.add_argument("--c3_temp", type=float, default=0.7)
p.add_argument("--claims", default="1,2,3")
args = p.parse_args()
os.makedirs(args.output, exist_ok=True)
device = torch.device("cuda:0")
torch.manual_seed(args.seed)
model, tokenizer = load_model_tokenizer(args.ckpt_path, args.base_path, device)
claims = [int(c) for c in args.claims.split(",")]
all_res = {}
if 1 in claims:
all_res["c1"] = claim1(model, tokenizer, device, args, args.output)
if 2 in claims:
all_res["c2"] = claim2(model, tokenizer, device, args, args.output)
if 3 in claims:
all_res["c3"] = claim3(model, tokenizer, device, args, args.output)
with open(os.path.join(args.output, "summary.json"), "w") as f:
json.dump(all_res, f, indent=2)
print("\n" + "=" * 70, flush=True)
print("ALL CLAIMS COMPLETE — summary:", flush=True)
print(json.dumps({k: {kk: vv for kk, vv in v.items() if kk in
("accuracy", "pass_at_k", "modes", "AR_pass_at_k",
"arbitrary_pass_at_k", "arbitrary_collapses_coverage",
"relative_drop_pct", "paper_reported", "n", "correct", "total")}
for k, v in all_res.items()}, indent=2), flush=True)
print("=" * 70, flush=True)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
12.5 kB
·
Xet hash:
ba91bacafd95083e215b7180757e4cd104c14f43ec049b116587e71091040022

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.