piyush-mk commited on
Commit
1d75102
·
verified ·
1 Parent(s): 4cd2128

v5: remove 4-bit quant, variable-length traces, submit oversampling, GRPO exploration fixes

Browse files
Files changed (2) hide show
  1. training/train_grpo.py +31 -25
  2. 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
- def _format_warmup_actions(env: InvoiceGuardEnvironment, task_id: TaskID) -> list[dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- evidence = list(dict.fromkeys([
283
- "inspect_purchase_order",
284
- "inspect_goods_receipt_note",
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
- {"action_type": "inspect_purchase_order"},
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
- # For LoRA on quantized backbones, this is the standard memory-saving path.
473
- base = prepare_model_for_kbit_training(base, use_gradient_checkpointing=True)
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
- def _expert_actions(env: InvoiceGuardEnvironment, task_id: TaskID) -> list[dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- evidence = list(dict.fromkeys([
139
- "inspect_purchase_order",
140
- "inspect_goods_receipt_note",
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
- {"action_type": "inspect_purchase_order"},
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
- obs = env.reset(task_id=task_id.value)
180
- messages: list[dict] = [{"role": "system", "content": SYSTEM_PROMPT}]
181
- for action_dict in _expert_actions(env, task_id):
182
- user_msg = build_observation_prompt(obs, is_first=(len(messages) == 1))
183
- messages.append({"role": "user", "content": user_msg})
184
- try:
185
- prompt_text = tokenizer.apply_chat_template(
186
- messages, tokenize=False, add_generation_prompt=True,
187
- enable_thinking=False,
188
- )
189
- except TypeError:
190
- prompt_text = tokenizer.apply_chat_template(
191
- messages, tokenize=False, add_generation_prompt=True,
192
- )
193
- completion_text = json.dumps(action_dict, ensure_ascii=False)
194
- prompt_ids = tokenizer(
195
- prompt_text,
196
- return_tensors="pt",
197
- add_special_tokens=False,
198
- truncation=True,
199
- max_length=max_prompt_tokens,
200
- ).input_ids[0]
201
- comp_enc = tokenizer(
202
- completion_text,
203
- return_tensors="pt",
204
- add_special_tokens=False,
205
- ).input_ids[0]
206
- eos_id = tokenizer.convert_tokens_to_ids("<|im_end|>")
207
- if eos_id is not None and eos_id != tokenizer.unk_token_id:
208
- completion_ids = torch.cat([comp_enc, torch.tensor([eos_id])])
209
- else:
210
- completion_ids = comp_enc
211
- examples.append({
212
- "task_id": task_id.value,
213
- "action_type": action_dict["action_type"],
214
- "prompt_ids": prompt_ids,
215
- "completion_ids": completion_ids,
216
- "completion_text": completion_text,
217
- })
218
- messages.append({"role": "assistant", "content": completion_text})
219
- obs = env.step(build_action(action_dict))
220
- if obs.done:
221
- break
 
222
  return examples
223
 
224
 
225
- def completion_loss(model, prompt_ids: torch.Tensor, completion_ids: torch.Tensor, device: torch.device) -> torch.Tensor:
 
 
 
 
 
 
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
- base = prepare_model_for_kbit_training(base, use_gradient_checkpointing=True)
 
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
- loss = completion_loss(model, ex["prompt_ids"], ex["completion_ids"], device)
 
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