junwatu's picture
download
raw
32.2 kB
#!/usr/bin/env python3
"""
TarGATE reproduction — Target-Aware Data Selection via Token-Attenuation Gates.
Faithful reimplementation of the core method from ICML 2026 paper #3063
(OpenReview: xaqSrbGpPN):
1. Token-level Information Retention Ratio (IRR) gates attenuate FFN outputs.
2. Joint optimization: encourage high IRR on reference (target) data and
low IRR on candidate/noise data (quality_classification_loss).
3. Score candidates by instance-level mean IRR; select top-k%.
4. Compare vs Random / Loss-based selection (noisy recovery + optional SFT).
5. Measure selection wall-clock and cross-model score transfer.
Official anonymous code: https://anonymous.4open.science/r/TarGATE-4008
"""
from __future__ import annotations
import argparse
import json
import os
import random
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
from tqdm import tqdm
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
@dataclass
class ReproConfig:
selector_model: str = "Qwen/Qwen2.5-0.5B-Instruct"
transfer_model: str = "Qwen/Qwen2.5-1.5B-Instruct"
seed: int = 42
max_length: int = 256
# Noisy scenario sizes (scaled vs paper: 8k faker + ~7k gsm8k train)
n_gsm8k_candidate: int = 800
n_gsm8k_reference: int = 80
n_noise: int = 800
n_eval: int = 200
selection_pct: float = 0.10 # top-10% (paper often uses 5%)
warmup_epochs: int = 1
warmup_lr: float = 3e-4
quality_loss_weight: float = 1.0
quality_loss_balance: float = 0.2
batch_size: int = 4
grad_accum: int = 4
sft_epochs: int = 1
sft_lr: float = 2e-5
sft_max_steps: int = 80
device: str = "cuda" if torch.cuda.is_available() else "cpu"
output_dir: str = "outputs"
run_sft: bool = True
run_transfer: bool = True
dtype: str = "bf16"
def set_seed(seed: int) -> None:
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
# ---------------------------------------------------------------------------
# Data
# ---------------------------------------------------------------------------
def make_noise_samples(n: int, seed: int = 42) -> List[Dict]:
"""Synthetic noise approximating Faker-style irrelevant instruction data."""
rng = random.Random(seed)
topics = [
"recipe for purple soup",
"inventory of imaginary insects",
"weather on Neptune last Tuesday",
"biography of a fictional cat mayor",
"instructions to fold invisible origami",
"list of unused color names",
"diary of a sentient toaster",
"rules for underwater chess",
]
out = []
for i in range(n):
topic = topics[i % len(topics)]
filler = " ".join(rng.choice(["lorem", "ipsum", "dolor", "sit", "amet", "noise"]) for _ in range(rng.randint(20, 60)))
out.append(
{
"dataset": "noise",
"id": f"noise_{i:06d}",
"is_target": 0,
"messages": [
{"role": "user", "content": f"Please write about {topic}. {filler}"},
{"role": "assistant", "content": f"Sure. Here is content about {topic}. {filler}"},
],
}
)
return out
def load_gsm8k(split: str = "train") -> List[Dict]:
from datasets import load_dataset
ds = load_dataset("openai/gsm8k", "main", split=split)
rows = []
for i, ex in enumerate(ds):
rows.append(
{
"dataset": "gsm8k",
"id": f"gsm8k_{split}_{i:06d}",
"is_target": 1,
"messages": [
{"role": "user", "content": ex["question"]},
{"role": "assistant", "content": ex["answer"]},
],
}
)
return rows
def split_gsm8k(
n_cand: int, n_ref: int, n_eval: int, seed: int
) -> Tuple[List[Dict], List[Dict], List[Dict]]:
rows = load_gsm8k("train")
rng = random.Random(seed)
rng.shuffle(rows)
ref = rows[:n_ref]
cand = rows[n_ref : n_ref + n_cand]
# holdout for quick exact-match eval after SFT (from remaining train as proxy)
rest = rows[n_ref + n_cand :]
eval_rows = rest[:n_eval]
for r in ref:
r["is_target"] = 1
for r in cand:
r["is_target"] = 1
return cand, ref, eval_rows
def messages_to_text(messages: List[Dict]) -> str:
return "\n".join(f"{m['role']}: {m['content']}" for m in messages)
class ChatDataset(Dataset):
def __init__(self, rows: List[Dict], tokenizer, max_length: int, for_lm: bool = True):
self.rows = rows
self.tokenizer = tokenizer
self.max_length = max_length
self.for_lm = for_lm
def __len__(self) -> int:
return len(self.rows)
def __getitem__(self, idx: int) -> Dict:
row = self.rows[idx]
text = messages_to_text(row["messages"])
enc = self.tokenizer(
text,
truncation=True,
max_length=self.max_length,
padding=False,
return_tensors=None,
)
item = {
"input_ids": enc["input_ids"],
"attention_mask": enc["attention_mask"],
"is_target_task": int(row.get("is_target", 0)),
"dataset": row["dataset"],
"id": row["id"],
"messages": row["messages"],
}
if self.for_lm:
item["labels"] = list(enc["input_ids"])
return item
def collate_fn(batch: List[Dict], pad_id: int) -> Dict:
max_len = max(len(b["input_ids"]) for b in batch)
def pad(seq, value):
return seq + [value] * (max_len - len(seq))
input_ids = torch.tensor([pad(b["input_ids"], pad_id) for b in batch], dtype=torch.long)
attention_mask = torch.tensor([pad(b["attention_mask"], 0) for b in batch], dtype=torch.long)
out = {
"input_ids": input_ids,
"attention_mask": attention_mask,
"is_target_task": torch.tensor([b["is_target_task"] for b in batch], dtype=torch.long),
"dataset": [b["dataset"] for b in batch],
"id": [b["id"] for b in batch],
"messages": [b["messages"] for b in batch],
}
if "labels" in batch[0]:
labels = torch.tensor([pad(b["labels"], -100) for b in batch], dtype=torch.long)
out["labels"] = labels
return out
# ---------------------------------------------------------------------------
# TarGATE: inject token-attenuation gates into transformer MLP residual path
# ---------------------------------------------------------------------------
class QualityGate(nn.Module):
def __init__(self, hidden_size: int, init_mean: float = 0.0, init_std: float = 0.02):
super().__init__()
self.gate = nn.Linear(hidden_size, 1, bias=False)
nn.init.normal_(self.gate.weight, mean=init_mean, std=init_std)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
# hidden: [B, T, H] -> good_ratio [B, T, 1]
return torch.sigmoid(self.gate(hidden_states))
class GatedMLP(nn.Module):
"""MLP wrapper: out = original_mlp(x) * sigmoid(gate(x)). Must be nn.Module for HF layers."""
def __init__(self, original_mlp: nn.Module, gate: QualityGate):
super().__init__()
self.original_mlp = original_mlp
self.gate = gate
self.capture = False
self.last_ratio: Optional[torch.Tensor] = None
def forward(self, hidden_states, *args, **kwargs):
# Ensure gate weights match activation dtype (bf16/fp16)
if next(self.gate.parameters()).dtype != hidden_states.dtype:
self.gate.to(dtype=hidden_states.dtype)
good_ratio = self.gate(hidden_states)
mlp_out = self.original_mlp(hidden_states, *args, **kwargs)
if self.capture:
self.last_ratio = good_ratio.detach()
return mlp_out * good_ratio.to(dtype=mlp_out.dtype)
class TarGATEWrapper(nn.Module):
"""
Wrap a HF CausalLM and attenuate each decoder layer's MLP residual update
by a learned token-level IRR gate (as in QualityGateDecoderLayer).
"""
def __init__(self, model: nn.Module, quality_loss_weight: float = 1.0, quality_loss_balance: float = 0.2):
super().__init__()
self.model = model
self.quality_loss_weight = quality_loss_weight
self.quality_loss_balance = quality_loss_balance
self.gates = nn.ModuleList()
self.gated_mlps = nn.ModuleList()
self._handles = []
self._last_ratios: List[torch.Tensor] = []
self._capture = False
layers = self._get_layers()
hidden = model.config.hidden_size
# Match base model parameter dtype/device (e.g. bf16 on GPU)
ref_param = next(model.parameters())
for layer in layers:
g = QualityGate(hidden)
g.to(device=ref_param.device, dtype=ref_param.dtype)
self.gates.append(g)
# Patch MLP residual: residual + mlp(x) * good_ratio
self._patch_layer(layer, g)
# freeze base, train only gates
for p in self.model.parameters():
p.requires_grad = False
for g in self.gates:
for p in g.parameters():
p.requires_grad = True
def _get_layers(self):
if hasattr(self.model, "model") and hasattr(self.model.model, "layers"):
return self.model.model.layers
if hasattr(self.model, "transformer") and hasattr(self.model.transformer, "h"):
return self.model.transformer.h
raise RuntimeError("Unsupported model architecture for TarGATE wrapping")
def _patch_layer(self, layer: nn.Module, gate: QualityGate) -> None:
if not hasattr(layer, "mlp"):
raise RuntimeError("Layer has no mlp module")
original_mlp = layer.mlp
gated = GatedMLP(original_mlp, gate)
self.gated_mlps.append(gated)
# object.__setattr__ avoids Module type checks if needed; GatedMLP is a Module so OK
layer.mlp = gated
layer._targate_gate = gate
def train(self, mode: bool = True):
super().train(mode)
# base stays frozen for BN-like behavior; keep eval for dropout stability
self.model.eval()
for g in self.gates:
g.train(mode)
return self
def eval(self):
super().eval()
self.model.eval()
for g in self.gates:
g.eval()
return self
def quality_loss(
self,
ratios: List[torch.Tensor],
attention_mask: torch.Tensor,
is_target_task: torch.Tensor,
) -> torch.Tensor:
"""Match quality_classification_loss from official QualityGate code.
For candidate samples (is_target=0): push mean IRR down.
For reference samples (is_target=1): push mean IRR up.
"""
if not ratios:
return torch.tensor(0.0, device=attention_mask.device, requires_grad=True)
total = None
n = 0
mask = attention_mask.float()
is_train_t = 1.0 - is_target_task.float()
for good_ratio in ratios:
gr = good_ratio.squeeze(-1) # [B, T]
avg_per = (gr * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1)
loss_train_t = is_train_t * self.quality_loss_balance * avg_per
loss_target_t = (1.0 - is_train_t) * (2.0 - self.quality_loss_balance) * (1.0 - avg_per)
layer_loss = (loss_train_t + loss_target_t).mean()
total = layer_loss if total is None else total + layer_loss
n += 1
return total / max(n, 1)
def forward(self, input_ids, attention_mask=None, labels=None, is_target_task=None, capture_gates=False, **kwargs):
self._last_ratios = []
self._capture = bool(capture_gates or (self.training and is_target_task is not None))
for gm in self.gated_mlps:
gm.capture = self._capture
gm.last_ratio = None
outputs = self.model(input_ids=input_ids, attention_mask=attention_mask, labels=labels, **kwargs)
loss = outputs.loss
qloss = None
if is_target_task is not None:
# Differentiable IRR path via post-attn LN hooks
live_ratios = self._compute_live_ratios(input_ids, attention_mask)
qloss = self.quality_loss_weight * self.quality_loss(live_ratios, attention_mask, is_target_task)
if loss is not None:
loss = loss + qloss
else:
loss = qloss
return outputs, loss, qloss
def _compute_live_ratios(self, input_ids, attention_mask) -> List[torch.Tensor]:
"""Forward hidden states layer-by-layer to get live gate outputs (differentiable)."""
model = self.model
embeds = model.model.embed_tokens(input_ids)
hidden = embeds
# Build position ids
position_ids = attention_mask.long().cumsum(-1) - 1
position_ids.masked_fill_(attention_mask == 0, 1)
# Use model rotary if available via layer forward — simpler: call full model with hooks
ratios = []
# Use hooks on post_attention_layernorm outputs
captured = []
def make_hook(gate):
def hook(module, inp, out):
captured.append((gate, out))
return hook
handles = []
layers = self._get_layers()
for layer, gate in zip(layers, self.gates):
if hasattr(layer, "post_attention_layernorm"):
handles.append(layer.post_attention_layernorm.register_forward_hook(make_hook(gate)))
elif hasattr(layer, "ln_2"):
handles.append(layer.ln_2.register_forward_hook(make_hook(gate)))
with torch.set_grad_enabled(self.training):
_ = model(input_ids=input_ids, attention_mask=attention_mask, use_cache=False)
for h in handles:
h.remove()
for gate, h_states in captured:
ratios.append(gate(h_states))
return ratios
def score_batch(self, input_ids, attention_mask) -> torch.Tensor:
"""Instance-level mean IRR across tokens and layers."""
self.eval()
with torch.no_grad():
ratios = self._compute_live_ratios(input_ids, attention_mask)
if not ratios:
return torch.zeros(input_ids.size(0), device=input_ids.device)
stacked = torch.stack([r.squeeze(-1) for r in ratios], dim=0) # L,B,T
mask = attention_mask.unsqueeze(0).float()
summed = (stacked * mask).sum(dim=(0, 2))
denom = mask.sum(dim=(0, 2)).clamp(min=1) * stacked.size(0) / stacked.size(0)
# denom: sum over layers and tokens
denom = attention_mask.float().sum(dim=1).clamp(min=1) * stacked.size(0)
scores = summed / denom
return scores
def load_base_model(model_name: str, device: str, dtype: str):
from transformers import AutoModelForCausalLM, AutoTokenizer
torch_dtype = torch.bfloat16 if dtype == "bf16" and torch.cuda.is_available() else torch.float32
tok = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
if tok.pad_token is None:
tok.pad_token = tok.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch_dtype,
trust_remote_code=True,
)
model.to(device)
return model, tok
# ---------------------------------------------------------------------------
# Baselines
# ---------------------------------------------------------------------------
@torch.no_grad()
def score_by_nll(model, tokenizer, rows: List[Dict], cfg: ReproConfig) -> List[float]:
"""Lower NLL = higher quality score (we invert)."""
model.eval()
ds = ChatDataset(rows, tokenizer, cfg.max_length, for_lm=True)
loader = DataLoader(
ds,
batch_size=cfg.batch_size,
shuffle=False,
collate_fn=lambda b: collate_fn(b, tokenizer.pad_token_id),
)
scores = []
for batch in loader:
input_ids = batch["input_ids"].to(cfg.device)
attention_mask = batch["attention_mask"].to(cfg.device)
labels = batch["labels"].to(cfg.device)
out = model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)
# per-example CE
logits = out.logits
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
loss_fct = nn.CrossEntropyLoss(reduction="none", ignore_index=-100)
token_loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
token_loss = token_loss.view(shift_labels.size())
mask = (shift_labels != -100).float()
per = (token_loss * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1)
# quality score = -NLL so higher is better (easier / more fluent)
scores.extend((-per).detach().cpu().tolist())
return scores
def score_random(rows: List[Dict], seed: int) -> List[float]:
rng = random.Random(seed)
return [rng.random() for _ in rows]
@torch.no_grad()
def score_targate(wrapper: TarGATEWrapper, tokenizer, rows: List[Dict], cfg: ReproConfig) -> List[float]:
wrapper.eval()
ds = ChatDataset(rows, tokenizer, cfg.max_length, for_lm=False)
loader = DataLoader(
ds,
batch_size=cfg.batch_size,
shuffle=False,
collate_fn=lambda b: collate_fn(b, tokenizer.pad_token_id),
)
scores = []
for batch in loader:
input_ids = batch["input_ids"].to(cfg.device)
attention_mask = batch["attention_mask"].to(cfg.device)
s = wrapper.score_batch(input_ids, attention_mask)
scores.extend(s.detach().cpu().tolist())
return scores
def select_top_pct(rows: List[Dict], scores: List[float], pct: float) -> List[Dict]:
k = max(1, int(np.ceil(pct * len(rows))))
order = np.argsort(-np.array(scores))
selected = []
for idx in order[:k]:
r = dict(rows[int(idx)])
r["score"] = float(scores[int(idx)])
selected.append(r)
return selected
def selection_metrics(selected: List[Dict], candidate: List[Dict]) -> Dict:
n_sel = len(selected)
n_target = sum(1 for r in selected if r.get("dataset") == "gsm8k" or r.get("is_target") == 1)
n_noise = sum(1 for r in selected if r.get("dataset") == "noise")
pure = n_target / max(n_sel, 1)
# recovery: fraction of all gsm8k candidates recovered
n_cand_target = sum(1 for r in candidate if r.get("dataset") == "gsm8k")
recovery = n_target / max(n_cand_target, 1)
return {
"n_selected": n_sel,
"n_target_in_selected": n_target,
"n_noise_in_selected": n_noise,
"precision": pure,
"recovery": recovery,
"candidate_size": len(candidate),
"candidate_target": n_cand_target,
}
# ---------------------------------------------------------------------------
# Training
# ---------------------------------------------------------------------------
def train_gates(
wrapper: TarGATEWrapper,
tokenizer,
candidate: List[Dict],
reference: List[Dict],
cfg: ReproConfig,
) -> Dict:
"""Stage 1 warmup: joint LM + quality loss on ref+cand."""
# subsample candidate for warmup (paper subset_ratio=0.25)
rng = random.Random(cfg.seed)
warmup_cand = list(candidate)
rng.shuffle(warmup_cand)
warmup_cand = warmup_cand[: max(len(reference) * 4, int(0.5 * len(candidate)))]
train_rows = reference + warmup_cand
rng.shuffle(train_rows)
ds = ChatDataset(train_rows, tokenizer, cfg.max_length, for_lm=True)
loader = DataLoader(
ds,
batch_size=cfg.batch_size,
shuffle=True,
collate_fn=lambda b: collate_fn(b, tokenizer.pad_token_id),
)
opt = torch.optim.AdamW([p for p in wrapper.gates.parameters() if p.requires_grad], lr=cfg.warmup_lr)
wrapper.train()
t0 = time.time()
step = 0
losses = []
for epoch in range(cfg.warmup_epochs):
opt.zero_grad(set_to_none=True)
for batch in tqdm(loader, desc=f"warmup_epoch{epoch}"):
input_ids = batch["input_ids"].to(cfg.device)
attention_mask = batch["attention_mask"].to(cfg.device)
labels = batch["labels"].to(cfg.device)
is_target = batch["is_target_task"].to(cfg.device)
_, loss, qloss = wrapper(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels,
is_target_task=is_target,
capture_gates=True,
)
(loss / cfg.grad_accum).backward()
if (step + 1) % cfg.grad_accum == 0:
opt.step()
opt.zero_grad(set_to_none=True)
losses.append(float(loss.detach().cpu()))
step += 1
if step % cfg.grad_accum != 0:
opt.step()
wall = time.time() - t0
return {
"warmup_steps": step,
"warmup_wall_sec": wall,
"warmup_loss_mean": float(np.mean(losses[-50:])) if losses else None,
"n_trainable_gate_params": sum(p.numel() for p in wrapper.gates.parameters()),
}
def quick_sft_and_eval(
model_name: str,
train_rows: List[Dict],
eval_rows: List[Dict],
cfg: ReproConfig,
tag: str,
) -> Dict:
"""Lightweight LoRA-free full SFT on a small model, short steps; eval exact numeric match rate."""
from transformers import AutoModelForCausalLM, AutoTokenizer
torch_dtype = torch.bfloat16 if cfg.dtype == "bf16" and torch.cuda.is_available() else torch.float32
tok = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
if tok.pad_token is None:
tok.pad_token = tok.eos_token
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch_dtype, trust_remote_code=True)
model.to(cfg.device)
# train all params lightly — for 0.5B short steps is ok on GPU
for p in model.parameters():
p.requires_grad = True
opt = torch.optim.AdamW(model.parameters(), lr=cfg.sft_lr)
ds = ChatDataset(train_rows, tok, cfg.max_length, for_lm=True)
loader = DataLoader(
ds,
batch_size=max(1, cfg.batch_size // 2),
shuffle=True,
collate_fn=lambda b: collate_fn(b, tok.pad_token_id),
)
model.train()
t0 = time.time()
step = 0
losses = []
while step < cfg.sft_max_steps:
for batch in loader:
input_ids = batch["input_ids"].to(cfg.device)
attention_mask = batch["attention_mask"].to(cfg.device)
labels = batch["labels"].to(cfg.device)
out = model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)
out.loss.backward()
opt.step()
opt.zero_grad(set_to_none=True)
losses.append(float(out.loss.detach().cpu()))
step += 1
if step >= cfg.sft_max_steps:
break
sft_wall = time.time() - t0
# eval: greedy generate short answers and check final number
model.eval()
correct = 0
total = 0
for row in eval_rows[: cfg.n_eval]:
q = row["messages"][0]["content"]
gold = row["messages"][1]["content"]
gold_num = gold.split("####")[-1].strip() if "####" in gold else gold.strip().split()[-1]
prompt = f"user: {q}\nassistant:"
enc = tok(prompt, return_tensors="pt", truncation=True, max_length=cfg.max_length).to(cfg.device)
with torch.no_grad():
gen = model.generate(
**enc,
max_new_tokens=64,
do_sample=False,
pad_token_id=tok.pad_token_id,
)
text = tok.decode(gen[0][enc["input_ids"].shape[1] :], skip_special_tokens=True)
pred_num = ""
try:
if "####" in text:
tail = text.split("####")[-1].strip()
parts = tail.split()
pred_num = parts[0] if parts else ""
else:
parts = text.strip().split()
pred_num = parts[-1] if parts else ""
except Exception:
pred_num = ""
# normalize
gold_num_n = "".join(c for c in str(gold_num) if c.isdigit() or c in ".-")
pred_num_n = "".join(c for c in str(pred_num) if c.isdigit() or c in ".-")
if gold_num_n and gold_num_n == pred_num_n:
correct += 1
total += 1
acc = correct / max(total, 1)
del model
if torch.cuda.is_available():
torch.cuda.empty_cache()
return {
"tag": tag,
"sft_steps": step,
"sft_wall_sec": sft_wall,
"sft_loss_mean": float(np.mean(losses[-20:])) if losses else None,
"eval_n": total,
"eval_correct": correct,
"eval_acc": acc,
"n_train": len(train_rows),
}
# ---------------------------------------------------------------------------
# Main experiment
# ---------------------------------------------------------------------------
def run(cfg: ReproConfig) -> Dict:
set_seed(cfg.seed)
out_dir = Path(cfg.output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
print("=== Loading GSM8K + noise ===")
cand_gsm, ref, eval_rows = split_gsm8k(cfg.n_gsm8k_candidate, cfg.n_gsm8k_reference, cfg.n_eval, cfg.seed)
noise = make_noise_samples(cfg.n_noise, cfg.seed)
candidate = cand_gsm + noise
random.Random(cfg.seed).shuffle(candidate)
print(f"candidate={len(candidate)} (gsm8k={len(cand_gsm)}, noise={len(noise)}), ref={len(ref)}, eval={len(eval_rows)}")
print(f"=== Loading selector model {cfg.selector_model} ===")
base, tok = load_base_model(cfg.selector_model, cfg.device, cfg.dtype)
wrapper = TarGATEWrapper(base, cfg.quality_loss_weight, cfg.quality_loss_balance)
wrapper.to(cfg.device)
print("=== Stage 1: Gate warmup ===")
warmup_stats = train_gates(wrapper, tok, candidate, ref, cfg)
print(warmup_stats)
print("=== Stage 2: Scoring ===")
t0 = time.time()
scores_tg = score_targate(wrapper, tok, candidate, cfg)
t_tg = time.time() - t0
t0 = time.time()
scores_nll = score_by_nll(wrapper.model, tok, candidate, cfg)
t_nll = time.time() - t0
t0 = time.time()
scores_rnd = score_random(candidate, cfg.seed + 1)
t_rnd = time.time() - t0
methods = {
"TarGATE": scores_tg,
"NegNLL": scores_nll,
"Random": scores_rnd,
}
timings = {
"TarGATE_score_sec": t_tg,
"NegNLL_score_sec": t_nll,
"Random_score_sec": t_rnd,
"TarGATE_warmup_sec": warmup_stats["warmup_wall_sec"],
}
results = {"selection": {}, "timings": timings, "warmup": warmup_stats, "config": asdict(cfg)}
selected_sets = {}
for name, sc in methods.items():
sel = select_top_pct(candidate, sc, cfg.selection_pct)
selected_sets[name] = sel
metrics = selection_metrics(sel, candidate)
results["selection"][name] = metrics
print(f"{name}: precision={metrics['precision']:.3f} recovery={metrics['recovery']:.3f} n={metrics['n_selected']}")
results["efficiency"] = {
"trainable_gate_params": warmup_stats["n_trainable_gate_params"],
"base_model": cfg.selector_model,
"score_time_targate_sec": t_tg,
"score_time_nll_sec": t_nll,
"warmup_time_sec": warmup_stats["warmup_wall_sec"],
"notes": "TarGATE trains only tiny linear gates; scoring is one forward pass per batch.",
}
# Persist after selection so SFT failures do not lose primary metrics
with open(out_dir / "results_selection.json", "w") as f:
json.dump(results, f, indent=2)
with open(out_dir / "selected_targate.jsonl", "w") as f:
for r in selected_sets["TarGATE"]:
f.write(json.dumps(r) + "\n")
with open(out_dir / "selected_random.jsonl", "w") as f:
for r in selected_sets["Random"]:
f.write(json.dumps(r) + "\n")
print(f"Checkpointed selection results to {out_dir}")
if cfg.run_transfer:
print("=== Cross-model transfer (data selected by small model) ===")
results["transfer"] = {
"selector": cfg.selector_model,
"target_model_for_sft": cfg.transfer_model if cfg.run_sft else None,
"selected_by_small_model_precision": results["selection"]["TarGATE"]["precision"],
"claim": "Small selector TarGATE curates high-purity target data usable for larger model SFT",
}
# Optional short SFT comparison on selector model
if cfg.run_sft:
print("=== Stage 3: short SFT comparison ===")
sft_results = {}
for name in ["TarGATE", "Random", "NegNLL"]:
print(f"SFT on {name} selection...")
try:
sft_results[name] = quick_sft_and_eval(
cfg.selector_model, selected_sets[name], eval_rows, cfg, tag=name
)
print(sft_results[name])
except Exception as e:
print(f"SFT {name} failed: {e}")
sft_results[name] = {"tag": name, "error": str(e)}
results["sft"] = sft_results
if cfg.run_transfer:
print("=== Cross-model SFT on transfer model ===")
xfer = {}
for name in ["TarGATE", "Random"]:
print(f"Transfer SFT {cfg.transfer_model} on {name}...")
try:
xfer[name] = quick_sft_and_eval(
cfg.transfer_model, selected_sets[name], eval_rows, cfg, tag=f"xfer_{name}"
)
print(xfer[name])
except Exception as e:
print(f"Transfer SFT {name} failed: {e}")
xfer[name] = {"tag": f"xfer_{name}", "error": str(e)}
results["transfer_sft"] = xfer
out_path = out_dir / "results.json"
with open(out_path, "w") as f:
json.dump(results, f, indent=2)
print(f"Wrote {out_path}")
return results
def parse_args() -> ReproConfig:
p = argparse.ArgumentParser()
p.add_argument("--selector-model", default=ReproConfig.selector_model)
p.add_argument("--transfer-model", default=ReproConfig.transfer_model)
p.add_argument("--output-dir", default="outputs")
p.add_argument("--n-gsm8k-candidate", type=int, default=800)
p.add_argument("--n-gsm8k-reference", type=int, default=80)
p.add_argument("--n-noise", type=int, default=800)
p.add_argument("--n-eval", type=int, default=100)
p.add_argument("--selection-pct", type=float, default=0.10)
p.add_argument("--warmup-epochs", type=int, default=1)
p.add_argument("--batch-size", type=int, default=4)
p.add_argument("--sft-max-steps", type=int, default=80)
p.add_argument("--max-length", type=int, default=256)
p.add_argument("--no-sft", action="store_true")
p.add_argument("--no-transfer", action="store_true")
p.add_argument("--seed", type=int, default=42)
args = p.parse_args()
return ReproConfig(
selector_model=args.selector_model,
transfer_model=args.transfer_model,
output_dir=args.output_dir,
n_gsm8k_candidate=args.n_gsm8k_candidate,
n_gsm8k_reference=args.n_gsm8k_reference,
n_noise=args.n_noise,
n_eval=args.n_eval,
selection_pct=args.selection_pct,
warmup_epochs=args.warmup_epochs,
batch_size=args.batch_size,
sft_max_steps=args.sft_max_steps,
max_length=args.max_length,
run_sft=not args.no_sft,
run_transfer=not args.no_transfer,
seed=args.seed,
)
if __name__ == "__main__":
cfg = parse_args()
print(json.dumps(asdict(cfg), indent=2))
results = run(cfg)
print(json.dumps(results, indent=2, default=str))

Xet Storage Details

Size:
32.2 kB
·
Xet hash:
e58ae2e7d6e7138c172274898119a5354ebb44cf235ca6973121106dc83ed121

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