v5: remove 4-bit quant, variable-length traces, submit oversampling, GRPO exploration fixes
Browse files- training/train_grpo.py +31 -25
- training/train_sft.py +85 -69
training/train_grpo.py
CHANGED
|
@@ -272,36 +272,36 @@ def compute_group_advantages(
|
|
| 272 |
return [(r - mean) / std for r in rewards]
|
| 273 |
|
| 274 |
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
case = getattr(env, "_case", None)
|
| 277 |
if case is None:
|
| 278 |
env.reset(task_id=task_id.value)
|
| 279 |
case = getattr(env, "_case", None)
|
| 280 |
assert case is not None
|
| 281 |
gt = case.ground_truth
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
"inspect_invoice_line_items",
|
| 286 |
-
"inspect_vendor_profile",
|
| 287 |
-
"compare_quantity",
|
| 288 |
-
"compare_price",
|
| 289 |
-
"compare_totals",
|
| 290 |
-
"check_for_duplicate_invoice",
|
| 291 |
-
"inspect_policy_rules",
|
| 292 |
-
*gt.acceptable_evidence,
|
| 293 |
-
]))
|
| 294 |
explanation = "Key findings: " + "; ".join(gt.key_findings[:3])
|
| 295 |
return [
|
| 296 |
-
|
| 297 |
-
{"action_type": "inspect_goods_receipt_note"},
|
| 298 |
-
{"action_type": "inspect_invoice_line_items"},
|
| 299 |
-
{"action_type": "inspect_vendor_profile"},
|
| 300 |
-
{"action_type": "compare_quantity"},
|
| 301 |
-
{"action_type": "compare_price"},
|
| 302 |
-
{"action_type": "compare_totals"},
|
| 303 |
-
{"action_type": "check_for_duplicate_invoice"},
|
| 304 |
-
{"action_type": "inspect_policy_rules"},
|
| 305 |
{
|
| 306 |
"action_type": "submit_final_resolution",
|
| 307 |
"final_decision": gt.correct_decision.value,
|
|
@@ -329,13 +329,16 @@ def run_format_warmup(
|
|
| 329 |
for group in optimizer.param_groups:
|
| 330 |
group["lr"] = cfg.format_warmup_lr
|
| 331 |
|
|
|
|
|
|
|
| 332 |
policy.train()
|
| 333 |
n_pairs = 0
|
| 334 |
total_loss = 0.0
|
| 335 |
for task_id in tasks[: cfg.format_warmup_tasks]:
|
|
|
|
| 336 |
obs = env.reset(task_id=task_id.value)
|
| 337 |
messages: list[dict] = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 338 |
-
for action_dict in _format_warmup_actions(env, task_id):
|
| 339 |
user_msg = build_observation_prompt(obs, is_first=(len(messages) == 1))
|
| 340 |
messages.append({"role": "user", "content": user_msg})
|
| 341 |
try:
|
|
@@ -469,8 +472,8 @@ def train(cfg: TrainConfig) -> None:
|
|
| 469 |
base.config.pad_token_id = tokenizer.pad_token_id
|
| 470 |
base.config.use_cache = False
|
| 471 |
if cfg.gradient_checkpointing:
|
| 472 |
-
|
| 473 |
-
|
| 474 |
base.gradient_checkpointing_enable()
|
| 475 |
|
| 476 |
lora_cfg = LoraConfig(
|
|
@@ -910,6 +913,7 @@ def _parse_args() -> TrainConfig:
|
|
| 910 |
p.add_argument("--format-warmup-tasks", type=int, default=None)
|
| 911 |
p.add_argument("--no-save-format-warmup", action="store_true")
|
| 912 |
p.add_argument("--format-warmup-model-id", default=None)
|
|
|
|
| 913 |
args = p.parse_args()
|
| 914 |
|
| 915 |
cfg = TrainConfig()
|
|
@@ -941,6 +945,8 @@ def _parse_args() -> TrainConfig:
|
|
| 941 |
cfg.save_format_warmup_checkpoint = False
|
| 942 |
if args.format_warmup_model_id:
|
| 943 |
cfg.format_warmup_model_id = args.format_warmup_model_id
|
|
|
|
|
|
|
| 944 |
if args.no_push:
|
| 945 |
cfg.push_to_hub = False
|
| 946 |
if args.no_4bit:
|
|
|
|
| 272 |
return [(r - mean) / std for r in rewards]
|
| 273 |
|
| 274 |
|
| 275 |
+
_ALL_INVESTIGATION_ACTIONS = [
|
| 276 |
+
{"action_type": "inspect_purchase_order"},
|
| 277 |
+
{"action_type": "inspect_goods_receipt_note"},
|
| 278 |
+
{"action_type": "inspect_invoice_line_items"},
|
| 279 |
+
{"action_type": "inspect_vendor_profile"},
|
| 280 |
+
{"action_type": "compare_quantity"},
|
| 281 |
+
{"action_type": "compare_price"},
|
| 282 |
+
{"action_type": "compare_totals"},
|
| 283 |
+
{"action_type": "check_for_duplicate_invoice"},
|
| 284 |
+
{"action_type": "inspect_policy_rules"},
|
| 285 |
+
]
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def _format_warmup_actions(
|
| 289 |
+
env: InvoiceGuardEnvironment,
|
| 290 |
+
task_id: TaskID,
|
| 291 |
+
max_investigation_steps: int = 9,
|
| 292 |
+
) -> list[dict]:
|
| 293 |
case = getattr(env, "_case", None)
|
| 294 |
if case is None:
|
| 295 |
env.reset(task_id=task_id.value)
|
| 296 |
case = getattr(env, "_case", None)
|
| 297 |
assert case is not None
|
| 298 |
gt = case.ground_truth
|
| 299 |
+
investigation = _ALL_INVESTIGATION_ACTIONS[:max_investigation_steps]
|
| 300 |
+
used_names = [a["action_type"] for a in investigation]
|
| 301 |
+
evidence = list(dict.fromkeys([*used_names, *gt.acceptable_evidence]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
explanation = "Key findings: " + "; ".join(gt.key_findings[:3])
|
| 303 |
return [
|
| 304 |
+
*investigation,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
{
|
| 306 |
"action_type": "submit_final_resolution",
|
| 307 |
"final_decision": gt.correct_decision.value,
|
|
|
|
| 329 |
for group in optimizer.param_groups:
|
| 330 |
group["lr"] = cfg.format_warmup_lr
|
| 331 |
|
| 332 |
+
warmup_trace_lengths = [3, 5, 7]
|
| 333 |
+
|
| 334 |
policy.train()
|
| 335 |
n_pairs = 0
|
| 336 |
total_loss = 0.0
|
| 337 |
for task_id in tasks[: cfg.format_warmup_tasks]:
|
| 338 |
+
n_inv = warmup_trace_lengths[n_pairs % len(warmup_trace_lengths)]
|
| 339 |
obs = env.reset(task_id=task_id.value)
|
| 340 |
messages: list[dict] = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 341 |
+
for action_dict in _format_warmup_actions(env, task_id, max_investigation_steps=n_inv):
|
| 342 |
user_msg = build_observation_prompt(obs, is_first=(len(messages) == 1))
|
| 343 |
messages.append({"role": "user", "content": user_msg})
|
| 344 |
try:
|
|
|
|
| 472 |
base.config.pad_token_id = tokenizer.pad_token_id
|
| 473 |
base.config.use_cache = False
|
| 474 |
if cfg.gradient_checkpointing:
|
| 475 |
+
if cfg.use_4bit:
|
| 476 |
+
base = prepare_model_for_kbit_training(base, use_gradient_checkpointing=True)
|
| 477 |
base.gradient_checkpointing_enable()
|
| 478 |
|
| 479 |
lora_cfg = LoraConfig(
|
|
|
|
| 913 |
p.add_argument("--format-warmup-tasks", type=int, default=None)
|
| 914 |
p.add_argument("--no-save-format-warmup", action="store_true")
|
| 915 |
p.add_argument("--format-warmup-model-id", default=None)
|
| 916 |
+
p.add_argument("--sample-temperature", type=float, default=None)
|
| 917 |
args = p.parse_args()
|
| 918 |
|
| 919 |
cfg = TrainConfig()
|
|
|
|
| 945 |
cfg.save_format_warmup_checkpoint = False
|
| 946 |
if args.format_warmup_model_id:
|
| 947 |
cfg.format_warmup_model_id = args.format_warmup_model_id
|
| 948 |
+
if args.sample_temperature is not None:
|
| 949 |
+
cfg.sample_temperature = args.sample_temperature
|
| 950 |
if args.no_push:
|
| 951 |
cfg.push_to_hub = False
|
| 952 |
if args.no_4bit:
|
training/train_sft.py
CHANGED
|
@@ -128,35 +128,35 @@ def split_tasks(cfg: SftConfig) -> tuple[list[TaskID], list[TaskID]]:
|
|
| 128 |
return train_tasks, eval_tasks
|
| 129 |
|
| 130 |
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
case = getattr(env, "_case", None)
|
| 133 |
if case is None:
|
| 134 |
env.reset(task_id=task_id.value)
|
| 135 |
case = getattr(env, "_case", None)
|
| 136 |
assert case is not None
|
| 137 |
gt = case.ground_truth
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
"inspect_invoice_line_items",
|
| 142 |
-
"inspect_vendor_profile",
|
| 143 |
-
"compare_quantity",
|
| 144 |
-
"compare_price",
|
| 145 |
-
"compare_totals",
|
| 146 |
-
"check_for_duplicate_invoice",
|
| 147 |
-
"inspect_policy_rules",
|
| 148 |
-
*gt.acceptable_evidence,
|
| 149 |
-
]))
|
| 150 |
return [
|
| 151 |
-
|
| 152 |
-
{"action_type": "inspect_goods_receipt_note"},
|
| 153 |
-
{"action_type": "inspect_invoice_line_items"},
|
| 154 |
-
{"action_type": "inspect_vendor_profile"},
|
| 155 |
-
{"action_type": "compare_quantity"},
|
| 156 |
-
{"action_type": "compare_price"},
|
| 157 |
-
{"action_type": "compare_totals"},
|
| 158 |
-
{"action_type": "check_for_duplicate_invoice"},
|
| 159 |
-
{"action_type": "inspect_policy_rules"},
|
| 160 |
{
|
| 161 |
"action_type": "submit_final_resolution",
|
| 162 |
"final_decision": gt.correct_decision.value,
|
|
@@ -168,6 +168,10 @@ def _expert_actions(env: InvoiceGuardEnvironment, task_id: TaskID) -> list[dict]
|
|
| 168 |
]
|
| 169 |
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
def build_sft_examples(
|
| 172 |
tokenizer,
|
| 173 |
env: InvoiceGuardEnvironment,
|
|
@@ -176,53 +180,60 @@ def build_sft_examples(
|
|
| 176 |
) -> list[dict]:
|
| 177 |
examples: list[dict] = []
|
| 178 |
for task_id in tasks:
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
| 222 |
return examples
|
| 223 |
|
| 224 |
|
| 225 |
-
def completion_loss(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
input_ids = torch.cat([prompt_ids, completion_ids], dim=0).unsqueeze(0).to(device)
|
| 227 |
attention_mask = torch.ones_like(input_ids)
|
| 228 |
out = model(input_ids=input_ids, attention_mask=attention_mask, use_cache=False)
|
|
@@ -231,7 +242,7 @@ def completion_loss(model, prompt_ids: torch.Tensor, completion_ids: torch.Tenso
|
|
| 231 |
logprobs = F.log_softmax(logits.float(), dim=-1)
|
| 232 |
token_lp = logprobs.gather(-1, targets.unsqueeze(-1)).squeeze(-1)
|
| 233 |
comp_len = completion_ids.shape[0]
|
| 234 |
-
return -token_lp[-comp_len:].mean()
|
| 235 |
|
| 236 |
|
| 237 |
def main() -> None:
|
|
@@ -279,7 +290,8 @@ def main() -> None:
|
|
| 279 |
base.config.pad_token_id = tokenizer.pad_token_id
|
| 280 |
base.config.use_cache = False
|
| 281 |
if cfg.gradient_checkpointing:
|
| 282 |
-
|
|
|
|
| 283 |
base.gradient_checkpointing_enable()
|
| 284 |
|
| 285 |
lora_cfg = LoraConfig(
|
|
@@ -365,7 +377,8 @@ def main() -> None:
|
|
| 365 |
total_loss = 0.0
|
| 366 |
model.train()
|
| 367 |
for i, ex in enumerate(examples, 1):
|
| 368 |
-
|
|
|
|
| 369 |
loss.backward()
|
| 370 |
total_loss += float(loss.detach().item())
|
| 371 |
torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], cfg.grad_clip)
|
|
@@ -420,6 +433,7 @@ def _parse_args() -> SftConfig:
|
|
| 420 |
p.add_argument("--max-new-tokens", type=int, default=None)
|
| 421 |
p.add_argument("--max-prompt-tokens", type=int, default=None)
|
| 422 |
p.add_argument("--no-push", action="store_true")
|
|
|
|
| 423 |
args = p.parse_args()
|
| 424 |
|
| 425 |
cfg = SftConfig()
|
|
@@ -443,6 +457,8 @@ def _parse_args() -> SftConfig:
|
|
| 443 |
cfg.max_prompt_tokens = args.max_prompt_tokens
|
| 444 |
if args.no_push:
|
| 445 |
cfg.push_to_hub = False
|
|
|
|
|
|
|
| 446 |
return cfg
|
| 447 |
|
| 448 |
|
|
|
|
| 128 |
return train_tasks, eval_tasks
|
| 129 |
|
| 130 |
|
| 131 |
+
_ALL_INVESTIGATION_ACTIONS = [
|
| 132 |
+
{"action_type": "inspect_purchase_order"},
|
| 133 |
+
{"action_type": "inspect_goods_receipt_note"},
|
| 134 |
+
{"action_type": "inspect_invoice_line_items"},
|
| 135 |
+
{"action_type": "inspect_vendor_profile"},
|
| 136 |
+
{"action_type": "compare_quantity"},
|
| 137 |
+
{"action_type": "compare_price"},
|
| 138 |
+
{"action_type": "compare_totals"},
|
| 139 |
+
{"action_type": "check_for_duplicate_invoice"},
|
| 140 |
+
{"action_type": "inspect_policy_rules"},
|
| 141 |
+
]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _expert_actions(
|
| 145 |
+
env: InvoiceGuardEnvironment,
|
| 146 |
+
task_id: TaskID,
|
| 147 |
+
max_investigation_steps: int = 9,
|
| 148 |
+
) -> list[dict]:
|
| 149 |
case = getattr(env, "_case", None)
|
| 150 |
if case is None:
|
| 151 |
env.reset(task_id=task_id.value)
|
| 152 |
case = getattr(env, "_case", None)
|
| 153 |
assert case is not None
|
| 154 |
gt = case.ground_truth
|
| 155 |
+
investigation = _ALL_INVESTIGATION_ACTIONS[:max_investigation_steps]
|
| 156 |
+
used_names = [a["action_type"] for a in investigation]
|
| 157 |
+
evidence = list(dict.fromkeys([*used_names, *gt.acceptable_evidence]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
return [
|
| 159 |
+
*investigation,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
{
|
| 161 |
"action_type": "submit_final_resolution",
|
| 162 |
"final_decision": gt.correct_decision.value,
|
|
|
|
| 168 |
]
|
| 169 |
|
| 170 |
|
| 171 |
+
TRACE_LENGTHS = [3, 5, 7, 9]
|
| 172 |
+
SUBMIT_LOSS_WEIGHT = 5.0
|
| 173 |
+
|
| 174 |
+
|
| 175 |
def build_sft_examples(
|
| 176 |
tokenizer,
|
| 177 |
env: InvoiceGuardEnvironment,
|
|
|
|
| 180 |
) -> list[dict]:
|
| 181 |
examples: list[dict] = []
|
| 182 |
for task_id in tasks:
|
| 183 |
+
for n_inv in TRACE_LENGTHS:
|
| 184 |
+
obs = env.reset(task_id=task_id.value)
|
| 185 |
+
messages: list[dict] = [{"role": "system", "content": SYSTEM_PROMPT}]
|
| 186 |
+
for action_dict in _expert_actions(env, task_id, max_investigation_steps=n_inv):
|
| 187 |
+
user_msg = build_observation_prompt(obs, is_first=(len(messages) == 1))
|
| 188 |
+
messages.append({"role": "user", "content": user_msg})
|
| 189 |
+
try:
|
| 190 |
+
prompt_text = tokenizer.apply_chat_template(
|
| 191 |
+
messages, tokenize=False, add_generation_prompt=True,
|
| 192 |
+
enable_thinking=False,
|
| 193 |
+
)
|
| 194 |
+
except TypeError:
|
| 195 |
+
prompt_text = tokenizer.apply_chat_template(
|
| 196 |
+
messages, tokenize=False, add_generation_prompt=True,
|
| 197 |
+
)
|
| 198 |
+
completion_text = json.dumps(action_dict, ensure_ascii=False)
|
| 199 |
+
prompt_ids = tokenizer(
|
| 200 |
+
prompt_text,
|
| 201 |
+
return_tensors="pt",
|
| 202 |
+
add_special_tokens=False,
|
| 203 |
+
truncation=True,
|
| 204 |
+
max_length=max_prompt_tokens,
|
| 205 |
+
).input_ids[0]
|
| 206 |
+
comp_enc = tokenizer(
|
| 207 |
+
completion_text,
|
| 208 |
+
return_tensors="pt",
|
| 209 |
+
add_special_tokens=False,
|
| 210 |
+
).input_ids[0]
|
| 211 |
+
eos_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
|
| 212 |
+
if eos_id is not None and eos_id != tokenizer.unk_token_id:
|
| 213 |
+
completion_ids = torch.cat([comp_enc, torch.tensor([eos_id])])
|
| 214 |
+
else:
|
| 215 |
+
completion_ids = comp_enc
|
| 216 |
+
examples.append({
|
| 217 |
+
"task_id": task_id.value,
|
| 218 |
+
"action_type": action_dict["action_type"],
|
| 219 |
+
"prompt_ids": prompt_ids,
|
| 220 |
+
"completion_ids": completion_ids,
|
| 221 |
+
"completion_text": completion_text,
|
| 222 |
+
})
|
| 223 |
+
messages.append({"role": "assistant", "content": completion_text})
|
| 224 |
+
obs = env.step(build_action(action_dict))
|
| 225 |
+
if obs.done:
|
| 226 |
+
break
|
| 227 |
return examples
|
| 228 |
|
| 229 |
|
| 230 |
+
def completion_loss(
|
| 231 |
+
model,
|
| 232 |
+
prompt_ids: torch.Tensor,
|
| 233 |
+
completion_ids: torch.Tensor,
|
| 234 |
+
device: torch.device,
|
| 235 |
+
weight: float = 1.0,
|
| 236 |
+
) -> torch.Tensor:
|
| 237 |
input_ids = torch.cat([prompt_ids, completion_ids], dim=0).unsqueeze(0).to(device)
|
| 238 |
attention_mask = torch.ones_like(input_ids)
|
| 239 |
out = model(input_ids=input_ids, attention_mask=attention_mask, use_cache=False)
|
|
|
|
| 242 |
logprobs = F.log_softmax(logits.float(), dim=-1)
|
| 243 |
token_lp = logprobs.gather(-1, targets.unsqueeze(-1)).squeeze(-1)
|
| 244 |
comp_len = completion_ids.shape[0]
|
| 245 |
+
return -token_lp[-comp_len:].mean() * weight
|
| 246 |
|
| 247 |
|
| 248 |
def main() -> None:
|
|
|
|
| 290 |
base.config.pad_token_id = tokenizer.pad_token_id
|
| 291 |
base.config.use_cache = False
|
| 292 |
if cfg.gradient_checkpointing:
|
| 293 |
+
if cfg.use_4bit:
|
| 294 |
+
base = prepare_model_for_kbit_training(base, use_gradient_checkpointing=True)
|
| 295 |
base.gradient_checkpointing_enable()
|
| 296 |
|
| 297 |
lora_cfg = LoraConfig(
|
|
|
|
| 377 |
total_loss = 0.0
|
| 378 |
model.train()
|
| 379 |
for i, ex in enumerate(examples, 1):
|
| 380 |
+
w = SUBMIT_LOSS_WEIGHT if ex["action_type"] == "submit_final_resolution" else 1.0
|
| 381 |
+
loss = completion_loss(model, ex["prompt_ids"], ex["completion_ids"], device, weight=w)
|
| 382 |
loss.backward()
|
| 383 |
total_loss += float(loss.detach().item())
|
| 384 |
torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], cfg.grad_clip)
|
|
|
|
| 433 |
p.add_argument("--max-new-tokens", type=int, default=None)
|
| 434 |
p.add_argument("--max-prompt-tokens", type=int, default=None)
|
| 435 |
p.add_argument("--no-push", action="store_true")
|
| 436 |
+
p.add_argument("--no-4bit", action="store_true")
|
| 437 |
args = p.parse_args()
|
| 438 |
|
| 439 |
cfg = SftConfig()
|
|
|
|
| 457 |
cfg.max_prompt_tokens = args.max_prompt_tokens
|
| 458 |
if args.no_push:
|
| 459 |
cfg.push_to_hub = False
|
| 460 |
+
if args.no_4bit:
|
| 461 |
+
cfg.use_4bit = False
|
| 462 |
return cfg
|
| 463 |
|
| 464 |
|