File size: 11,287 Bytes
77f690c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""rest_em.py — ReST-EM expert iteration (Singh et al. 2024, arXiv:2312.06585), the published method
whose RESULT is HELD-OUT TRANSFER — the exact thing SFT/GRPO/DPO failed at here.

THE load-bearing difference vs everything we tried: each Improve round trains a FRESH LoRA on the FROZEN
BASE — NEVER stacked on the prior adapter. Stacking (our cumulative ratchet) is what degraded held-out
(holdout_split_probe: train-on-distribution overfits + hurts untrained). ReST-EM trains-from-base each
round, so the policy improves only because the DATA improves (the generator gets better → more/diverse
verified-correct solutions), not because weights compound into a memorized mode.

Loop (ReST-EM E-step / M-step):
  GENERATE (E): sample K diverse (high-temp) per TRAIN problem with the CURRENT-BEST generator; verifier
                (real exec) filters to CORRECT-only; dedup near-identical (diversity > volume per Singh);
                cap per problem; ACCUMULATE into a growing pool.
  IMPROVE  (M): SFT a FRESH LoRA on the FROZEN BASE over the whole filtered pool (+ small base-distribution
                replay to preserve pass@k diversity / avoid mode-collapse).
  RATCHET     : eval greedy@1 on the SEALED held-out (deterministic ruler). Keep best adapter as next
                generator. Compounding = held-out greedy@1 rises round-over-round.

Outcome reward only (unit tests). Deterministic eval (VLLM_DETERMINISTIC=1). Qwen2.5-72B base."""
import os, sys, json, subprocess, tempfile, random, hashlib, pathlib, signal
sys.path.insert(0,"/workspace/RSI")
from concurrent.futures import ThreadPoolExecutor
from src.utils.external_benchmarks import _try_load_from_datasets, _extract_code
from src.utils.config import SystemConfig, ModelConfig
from src.utils.vllm_backend import VLLMModelLoader
from src.trainer.custom_lora import CustomLoRATrainer
from src.generator.data_generator import TrainingSample

MODEL=os.environ.get("MODEL","/workspace/RSI/expanded_models/qwen72")
BNB={"load_in_4bit":True,"bnb_4bit_compute_dtype":"bfloat16"}
PREREG="/workspace/RSI/outputs/prereg.json"
OUT=os.environ.get("RESTEM_OUT","/workspace/RSI/outputs/rest_em_slope.jsonl")
ADAPT=os.environ.get("RESTEM_ADAPT","/workspace/RSI/outputs/rest_em_adapters")
POOLFILE=os.environ.get("RESTEM_POOL","/workspace/RSI/outputs/rest_em_pool.json")  # persisted, resume-safe
N_CYCLES=int(os.environ.get("N_CYCLES","12")); N_TRAIN=int(os.environ.get("N_TRAIN","64")); K=int(os.environ.get("K","6"))
TEMP=float(os.environ.get("RESTEM_TEMP","1.0")); MAXTOK=int(os.environ.get("MAXTOK","640"))
GEN_CHUNK=int(os.environ.get("GEN_CHUNK","720")); EVAL_TC=int(os.environ.get("EVAL_TC","40")); EVAL_TO=int(os.environ.get("EVAL_TO","8"))
RANK=int(os.environ.get("RESTEM_RANK","32")); LR=float(os.environ.get("RESTEM_LR","1e-5"))
EPOCHS=int(os.environ.get("RESTEM_EPOCHS","2")); STEPS=int(os.environ.get("RESTEM_STEPS","120"))
MAXPER=int(os.environ.get("MAXPER","3"))  # diverse correct solutions kept per problem (dedup)
POOL_CAP=int(os.environ.get("POOL_CAP","800")); N_REPLAY=int(os.environ.get("N_REPLAY","16"))
_GW=int(os.environ.get("GRADE_WORKERS","48")); _POOL=ThreadPoolExecutor(max_workers=_GW)

def _norm(s): return "\n".join(l.rstrip() for l in str(s).strip().splitlines())
def _thash(it): return hashlib.md5((it.prompt+repr(it.meta.get("inputs"))+repr(it.meta.get("outputs"))).encode()).hexdigest()
def _chash(c): return hashlib.md5(_norm(c).encode()).hexdigest()
def run_stdin(code,inp,t):
    d=tempfile.mkdtemp(); pth=os.path.join(d,"s.py"); open(pth,"w").write(code)
    try:
        p=subprocess.Popen([sys.executable,pth],stdin=subprocess.PIPE,stdout=subprocess.PIPE,
                           stderr=subprocess.DEVNULL,text=True,start_new_session=True)
        try: out,_=p.communicate(input=str(inp),timeout=t); return out
        except subprocess.TimeoutExpired:
            try: os.killpg(os.getpgid(p.pid),signal.SIGKILL)
            except Exception: pass
            try: p.communicate(timeout=2)
            except Exception: pass
            return ""
        except Exception: return ""
    except Exception: return ""
    finally:
        try: os.remove(pth); os.rmdir(d)
        except Exception: pass
def grade(code,item,maxtc=None,to=EVAL_TO):
    ins=item.meta.get("inputs") or []; outs=item.meta.get("outputs") or []
    if not code or not ins: return 0.0
    tc=list(zip(ins,outs))
    if maxtc: step=max(1,len(tc)//maxtc); tc=tc[::step][:maxtc]
    res=list(_POOL.map(lambda io:_norm(run_stdin(code,io[0],to))==_norm(io[1]), tc))
    return sum(res)/max(1,len(res))

def main():
    cfg=SystemConfig(); cfg.model=ModelConfig(model_path=MODEL,dtype="bfloat16",quantization_config=BNB)
    cfg.trainer.use_rslora=False; cfg.trainer.lora_rank=RANK; cfg.trainer.lora_alpha=RANK
    cfg.trainer.learning_rate=LR; cfg.trainer.num_epochs=EPOCHS; cfg.trainer.max_steps_per_cycle=STEPS; cfg.trainer.training_mode="sft"
    for attr,val in (("gradient_accumulation_steps",1),("batch_size",2)):
        try: setattr(cfg.trainer,attr,val)
        except Exception: pass
    loader=VLLMModelLoader(model_path=MODEL,dtype="bfloat16",max_model_len=cfg.model.max_seq_length,
        gpu_memory_utilization=0.80,allow_remote_code=True,quantization_config=BNB,max_lora_rank=128,
        enable_chunked_prefill=False,enable_lora=True,enforce_eager=True)
    loader.load()
    gen_adapter=None  # the CURRENT-BEST generator (None=base for round 1)
    trainer=CustomLoRATrainer(cfg.trainer,loader); os.makedirs(ADAPT,exist_ok=True)
    def gen(ps,temp): return list(loader.generate_batch(ps,max_new_tokens=MAXTOK,temperature=temp,top_p=(1.0 if temp==0 else 0.95)))
    def gen_chunked(ps,temp):
        o=[]
        for c in range(0,len(ps),GEN_CHUNK): o.extend(gen(ps[c:c+GEN_CHUNK],temp))
        return o

    PR=json.load(open(PREREG)); HOLD=set(PR["hard_holdout"]); FRESH=set(PR.get("fresh_probe",[])); EASY=set(PR.get("easy",[]))
    apps=[it for it in (_try_load_from_datasets("apps") or []) if not (it.meta.get("fn_name") or "").strip() and it.meta.get("inputs") and it.meta.get("outputs")]
    bh={_thash(it):it for it in apps}
    hold=[bh[t] for t in HOLD if t in bh]; easy=[bh[t] for t in EASY if t in bh]
    reserved=HOLD|FRESH|EASY; train=[it for it in apps if _thash(it) not in reserved]
    print(f"[restem] apps {len(apps)} | sealed holdout {len(hold)} | train {len(train)} | K={K} N_TRAIN={N_TRAIN} temp={TEMP} (FRESH-FROM-BASE each round)",flush=True)

    # small base-distribution replay anchor (easy verified-correct) to preserve diversity / avoid collapse
    replay=[]
    for it in easy:
        o=gen([it.prompt],0.0)[0]; c=_extract_code(o) or o
        if grade(c,it,EVAL_TC)>=0.999:
            replay.append(TrainingSample(prompt=it.prompt,response="```python\n"+c.strip()+"\n```",problem_id=_thash(it),verified=True,domain="apps"))
    print(f"[restem] base-replay anchor = {len(replay)} easy verified",flush=True)

    def holdout_greedy():
        outs=gen([it.prompt for it in hold],0.0)
        return sum(1 for it,o in zip(hold,outs) if grade(_extract_code(o) or o,it,EVAL_TC)>=0.999)
    base_solved=holdout_greedy(); best=base_solved; best_adapter=None
    print(f"[restem] BASELINE holdout greedy@1 = {base_solved}/{len(hold)} ({base_solved/len(hold):.4f})",flush=True)
    open(OUT,"a").write(json.dumps({"cycle":0,"holdout_solved":base_solved,"total":len(hold)})+"\n")

    pool={}  # problem_id -> list of (code_hash, TrainingSample) ; accumulates verified-correct DIVERSE solns
    if os.path.exists(POOLFILE):
        try:
            for r in json.load(open(POOLFILE)):
                pool.setdefault(r["pid"],[]).append((r["ch"],TrainingSample(prompt=r["prompt"],response=r["response"],problem_id=r["pid"],verified=True,domain="apps")))
            print(f"[restem] resumed pool: {sum(len(v) for v in pool.values())} solns / {len(pool)} problems",flush=True)
        except Exception as e: print(f"[restem] pool reload failed ({e})",flush=True)

    for c in range(1,N_CYCLES+1):
        random.seed(80000+c); batch=random.sample(train,min(N_TRAIN,len(train)))
        if gen_adapter and os.path.exists(gen_adapter): loader.set_lora_adapter(gen_adapter)
        else: loader.set_lora_adapter(None)
        # GENERATE (E-step): K diverse samples per problem with current-best generator
        prompts=[it.prompt for it in batch for _ in range(K)]
        outs=gen_chunked(prompts,TEMP)
        new=0
        for i,it in enumerate(batch):
            pid=_thash(it); have={h for h,_ in pool.get(pid,[])}
            for o in outs[i*K:(i+1)*K]:
                if len([1 for _ in pool.get(pid,[])])>=MAXPER: break
                code=_extract_code(o) or o; ch=_chash(code)
                if ch in have or not code.strip(): continue
                if grade(code,it,EVAL_TC)>=0.999:
                    pool.setdefault(pid,[]).append((ch,TrainingSample(prompt=it.prompt,response="```python\n"+code.strip()+"\n```",problem_id=pid,verified=True,domain="apps")))
                    have.add(ch); new+=1
        # persist pool
        flat=[{"pid":pid,"ch":ch,"prompt":s.prompt,"response":s.response} for pid,lst in pool.items() for ch,s in lst]
        json.dump(flat,open(POOLFILE,"w"))
        train_samples=[s for lst in pool.values() for _,s in lst][:POOL_CAP]
        n_prob=len(pool)
        print(f"[restem] c{c} E-step: +{new} new verified solns -> pool {len(train_samples)} solns / {n_prob} problems",flush=True)
        if len(train_samples)<8:
            print(f"[restem] c{c} pool<8 — skip improve",flush=True); open(OUT,"a").write(json.dumps({"cycle":c,"pool":len(train_samples),"skip":True})+"\n"); continue
        # IMPROVE (M-step): FRESH LoRA from BASE on the filtered pool (+ replay anchor)
        loader.swap_to_hf_for_training(); ckpt=None
        try:
            trainer.inject_lora()  # FRESH from base — the ReST-EM load-bearing choice (never load prior adapter)
            m=trainer.train(train_samples+replay[:N_REPLAY],c); ckpt=trainer.save_lora_weights(pathlib.Path(ADAPT),c)
        finally:
            loader.swap_to_vllm_after_training(adapter_path=str(ckpt) if ckpt else best_adapter)
        if ckpt is None: print(f"[restem] c{c} improve failed",flush=True); continue
        solved=holdout_greedy(); d=solved-base_solved
        rec={"cycle":c,"pool_solns":len(train_samples),"pool_probs":n_prob,"steps":getattr(m,"steps",0),"loss":round(getattr(m,"final_loss",0.0),4),
             "holdout_solved":solved,"total":len(hold),"d_vs_base":d,"vs_best":solved-best}
        open(OUT,"a").write(json.dumps(rec)+"\n"); print(f"[restem] c{c} {json.dumps(rec)}",flush=True)
        # RATCHET: best adapter becomes next generator
        if solved>=best:
            best=solved; best_adapter=str(ckpt); gen_adapter=str(ckpt)
            print(f"[restem] c{c} RATCHET UP best={best}/{len(hold)} ({best/len(hold):.4f}) gen<-c{c}",flush=True)
        else:
            print(f"[restem] c{c} {solved}<{best}; keep generating from best (c data still accumulates)",flush=True)
            if best_adapter: gen_adapter=best_adapter
    print(f"[restem] DONE base={base_solved} -> best={best}/{len(hold)} ({best/len(hold):.4f}) over {N_CYCLES} cycles",flush=True)
    sys.exit(0)

if __name__=="__main__":
    main()