SavK1 commited on
Commit
1b68f93
·
2 Parent(s): 703b6685bcbe7f

Merge remote-tracking branch 'hf/main' into train/v1

Browse files
training/pm_ops_trainer.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PMOpsGRPOTrainer — GRPOTrainer subclass with direct reward injection.
2
+
3
+ Problem being solved
4
+ --------------------
5
+ TRL 1.2.0's GRPOTrainer._calculate_rewards() calls reward_funcs with
6
+ **reward_kwargs built from per-example keys in the input batch. This works
7
+ correctly when reward_funcs produce rewards from text completions alone.
8
+
9
+ It breaks for multi-turn env rollouts: our rollout_func computes rewards
10
+ inside the episode loop (where the env state is available), stores them as
11
+ a 'reward' key in the rollout output dict, and expects reward_func to read
12
+ them back via kwargs. TRL silently drops these keys when building
13
+ reward_kwargs from the batch, so reward_func always receives an empty dict
14
+ and returns 0.0 for every episode.
15
+
16
+ Fix
17
+ ---
18
+ Override _calculate_rewards to check for a pre-computed 'reward' key in the
19
+ input batch. If present, return it directly as a [batch_size, 1] tensor —
20
+ no reward_funcs called, no kwargs needed. If absent, fall back to the
21
+ standard TRL behaviour so the class works as a drop-in replacement.
22
+
23
+ Usage
24
+ -----
25
+ from training.pm_ops_trainer import PMOpsGRPOTrainer
26
+
27
+ trainer = PMOpsGRPOTrainer(
28
+ model=model,
29
+ processing_class=tokenizer,
30
+ reward_funcs=reward_func, # kept as-is; only used as fallback
31
+ train_dataset=dataset,
32
+ args=grpo_config,
33
+ rollout_func=rollout_func, # must put 'reward' key in output dict
34
+ )
35
+
36
+ rollout_func contract
37
+ ---------------------
38
+ rollout_func(prompts, trainer=None) must return a dict containing at minimum:
39
+ {
40
+ "prompt_ids": list[list[int]], # one per episode
41
+ "completion_ids": list[list[int]],
42
+ "logprobs": list[list[float]],
43
+ "reward": list[float], # one combined float per episode
44
+ }
45
+
46
+ The 'reward' value is the weighted combination of all sub-signals and is
47
+ injected directly as the GRPO advantage signal.
48
+ """
49
+
50
+ import torch
51
+ from trl import GRPOTrainer
52
+
53
+
54
+ class PMOpsGRPOTrainer(GRPOTrainer):
55
+ """Drop-in GRPOTrainer replacement with direct reward injection.
56
+
57
+ Overrides _calculate_rewards to read pre-computed rewards from the
58
+ rollout batch instead of calling reward_funcs with broken **kwargs.
59
+ Falls back to standard GRPOTrainer behaviour if 'reward' key is absent.
60
+ """
61
+
62
+ # Key written by rollout_func and read by _calculate_rewards
63
+ ROLLOUT_REWARD_KEY = "reward"
64
+
65
+ def _calculate_rewards(
66
+ self,
67
+ inputs,
68
+ prompts,
69
+ completions,
70
+ completion_ids_list,
71
+ ):
72
+ """Override: inject pre-computed rewards when available.
73
+
74
+ Args:
75
+ inputs: list[dict] — one dict per example in the batch.
76
+ rollout_func output keys land here per example.
77
+ prompts: list[str] — prompt texts (unused in this path)
78
+ completions: list[str] — completion texts (unused)
79
+ completion_ids_list: list[list[int]] — token IDs (unused)
80
+
81
+ Returns:
82
+ Tensor of shape [batch_size, 1] when pre-computed rewards are found.
83
+ Falls back to super()._calculate_rewards(...) otherwise.
84
+ """
85
+ # Fast-path: pre-computed reward key present in every example
86
+ if inputs and all(
87
+ self.ROLLOUT_REWARD_KEY in ex for ex in inputs
88
+ ):
89
+ device = self.accelerator.device
90
+ rewards = torch.tensor(
91
+ [float(ex[self.ROLLOUT_REWARD_KEY]) for ex in inputs],
92
+ dtype=torch.float32,
93
+ device=device,
94
+ ).unsqueeze(1) # [batch_size, 1]
95
+
96
+ # Log the mean reward so it shows up in training curves
97
+ mean_r = rewards.mean().item()
98
+ self.log({"reward/injected_mean": mean_r})
99
+
100
+ return rewards
101
+
102
+ # Fallback: standard TRL reward_funcs path
103
+ # Triggered when reward key is absent (e.g. non-rollout evaluation)
104
+ return super()._calculate_rewards(
105
+ inputs, prompts, completions, completion_ids_list
106
+ )
training/rollout.py CHANGED
@@ -16,11 +16,81 @@ import json
16
  import re
17
  from typing import Any
18
 
19
- from trl.experimental.openenv import generate_rollout_completions
 
20
 
21
  from training.dataset import parse_seed_from_prompt
22
  from training.prompts import SYSTEM_PROMPT, format_observation
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  MAX_STEPS = 40
25
  # ~3000 tokens at 4 chars/token; leaves room for completion tokens
26
  MAX_PROMPT_CHARS = 12_000
@@ -209,14 +279,12 @@ def rollout_once(
209
  enable_thinking=False,
210
  )
211
 
212
- rollout_out = generate_rollout_completions(trainer, [prompt_text])[0]
213
  prompt_ids.extend(rollout_out["prompt_ids"])
214
  completion_ids.extend(rollout_out["completion_ids"])
215
  logprobs.extend(rollout_out["logprobs"])
216
 
217
- completion_text = rollout_out.get("text") or tokenizer.decode(
218
- rollout_out["completion_ids"], skip_special_tokens=True
219
- )
220
 
221
  # Parse action; fall back gracefully on parse failure
222
  parsed = extract_json_action(completion_text)
@@ -287,18 +355,34 @@ def rollout_once(
287
  no_wrong_channels = 1.0
288
 
289
  read_runbook_reward = 1.0 if read_runbook_done else 0.0
290
- combined = (
291
- final_score * 0.45
292
- + no_wrong_channels * 0.15
293
- + valid_json_ratio * 0.15
294
- + read_runbook_reward * 0.15
295
- + efficiency * 0.10
296
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  print(
298
  f"[rollout] steps={step} final={final_score:.3f} "
299
  f"json={valid_json_ratio:.2f} runbook={read_runbook_reward:.0f} "
300
  f"no_wrong={no_wrong_channels:.2f} eff={efficiency:.2f} "
301
- f"→ combined={combined:.3f}"
302
  )
303
  return {
304
  "prompt_ids": prompt_ids,
 
16
  import re
17
  from typing import Any
18
 
19
+ import torch
20
+ import torch.nn.functional as F
21
 
22
  from training.dataset import parse_seed_from_prompt
23
  from training.prompts import SYSTEM_PROMPT, format_observation
24
 
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # HF model.generate() — replaces generate_rollout_completions (vLLM-only)
28
+ # ---------------------------------------------------------------------------
29
+
30
+ def _get_model_for_generation(trainer):
31
+ """Unwrap the model safely regardless of accelerate/PEFT/DDP wrapping.
32
+
33
+ Priority:
34
+ 1. accelerator.unwrap_model — handles DDP + PEFT + DeepSpeed
35
+ 2. trainer.model.module — plain DDP wrapping
36
+ 3. trainer.model — unwrapped (local or single-GPU)
37
+ """
38
+ if hasattr(trainer, "accelerator"):
39
+ return trainer.accelerator.unwrap_model(trainer.model)
40
+ if hasattr(trainer.model, "module"):
41
+ return trainer.model.module
42
+ return trainer.model
43
+
44
+
45
+ def _generate_no_vllm(trainer, prompt_text: str, tokenizer, max_new_tokens: int = 512) -> dict:
46
+ """Generate one completion using HF model.generate() without vLLM.
47
+
48
+ Returns the same dict shape as generate_rollout_completions so the rest
49
+ of rollout_once is unchanged:
50
+ prompt_ids: list[int]
51
+ completion_ids: list[int]
52
+ logprobs: list[float] (per-token log-prob under current policy)
53
+ text: str
54
+ """
55
+ model = _get_model_for_generation(trainer)
56
+
57
+ # Device: prefer accelerator.device, fall back to first param device
58
+ if hasattr(trainer, "accelerator"):
59
+ device = trainer.accelerator.device
60
+ else:
61
+ device = next(model.parameters()).device
62
+
63
+ enc = tokenizer(prompt_text, return_tensors="pt").to(device)
64
+ prompt_len = enc["input_ids"].shape[1]
65
+
66
+ with torch.no_grad():
67
+ out = model.generate(
68
+ **enc,
69
+ max_new_tokens=max_new_tokens,
70
+ do_sample=True,
71
+ temperature=0.7,
72
+ pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id,
73
+ output_scores=True,
74
+ return_dict_in_generate=True,
75
+ )
76
+
77
+ prompt_ids = enc["input_ids"][0].tolist()
78
+ completion_ids = out.sequences[0][prompt_len:].tolist()
79
+
80
+ # Per-token log-probs from output.scores (one score tensor per new token)
81
+ logprobs = [
82
+ F.log_softmax(score[0], dim=-1)[tok_id].item()
83
+ for score, tok_id in zip(out.scores, completion_ids)
84
+ ]
85
+
86
+ text = tokenizer.decode(completion_ids, skip_special_tokens=True)
87
+ return {
88
+ "prompt_ids": prompt_ids,
89
+ "completion_ids": completion_ids,
90
+ "logprobs": logprobs,
91
+ "text": text,
92
+ }
93
+
94
  MAX_STEPS = 40
95
  # ~3000 tokens at 4 chars/token; leaves room for completion tokens
96
  MAX_PROMPT_CHARS = 12_000
 
279
  enable_thinking=False,
280
  )
281
 
282
+ rollout_out = _generate_no_vllm(trainer, prompt_text, tokenizer)
283
  prompt_ids.extend(rollout_out["prompt_ids"])
284
  completion_ids.extend(rollout_out["completion_ids"])
285
  logprobs.extend(rollout_out["logprobs"])
286
 
287
+ completion_text = rollout_out["text"]
 
 
288
 
289
  # Parse action; fall back gracefully on parse failure
290
  parsed = extract_json_action(completion_text)
 
355
  no_wrong_channels = 1.0
356
 
357
  read_runbook_reward = 1.0 if read_runbook_done else 0.0
358
+
359
+ # --- Reward gating ---
360
+ # If model never output valid JSON, it never actually tried anything.
361
+ # Strip all process rewards and apply a harsh penalty.
362
+ # Process rewards (runbook, no_wrong, efficiency) only matter if model acted.
363
+ if valid_action_count == 0:
364
+ combined = -1.0
365
+ elif final_score == 0.0:
366
+ # Model tried (valid JSON) but task failed — small process credit, no final bonus
367
+ combined = (
368
+ valid_json_ratio * 0.15
369
+ + read_runbook_reward * 0.10
370
+ - 0.30 # hard penalty for zero task completion
371
+ )
372
+ else:
373
+ combined = (
374
+ final_score * 0.45
375
+ + no_wrong_channels * 0.15
376
+ + valid_json_ratio * 0.15
377
+ + read_runbook_reward * 0.15
378
+ + efficiency * 0.10
379
+ )
380
+
381
  print(
382
  f"[rollout] steps={step} final={final_score:.3f} "
383
  f"json={valid_json_ratio:.2f} runbook={read_runbook_reward:.0f} "
384
  f"no_wrong={no_wrong_channels:.2f} eff={efficiency:.2f} "
385
+ f"valid_acts={valid_action_count} → combined={combined:.3f}"
386
  )
387
  return {
388
  "prompt_ids": prompt_ids,
training/train_v2.ipynb CHANGED
@@ -21,7 +21,9 @@
21
  {
22
  "cell_type": "markdown",
23
  "metadata": {},
24
- "source": ["## 0. Install Dependencies"]
 
 
25
  },
26
  {
27
  "cell_type": "code",
@@ -47,7 +49,9 @@
47
  {
48
  "cell_type": "markdown",
49
  "metadata": {},
50
- "source": ["## 1. Version Check + GPU Detect"]
 
 
51
  },
52
  {
53
  "cell_type": "code",
@@ -81,7 +85,9 @@
81
  {
82
  "cell_type": "markdown",
83
  "metadata": {},
84
- "source": ["## 2. Clone PM-Ops Repo"]
 
 
85
  },
86
  {
87
  "cell_type": "code",
@@ -111,7 +117,9 @@
111
  {
112
  "cell_type": "markdown",
113
  "metadata": {},
114
- "source": ["## 3. HuggingFace Login"]
 
 
115
  },
116
  {
117
  "cell_type": "code",
@@ -161,7 +169,9 @@
161
  {
162
  "cell_type": "markdown",
163
  "metadata": {},
164
- "source": ["## 5. Verify Environment"]
 
 
165
  },
166
  {
167
  "cell_type": "code",
@@ -239,7 +249,9 @@
239
  {
240
  "cell_type": "markdown",
241
  "metadata": {},
242
- "source": ["## 7. Generate Training Dataset"]
 
 
243
  },
244
  {
245
  "cell_type": "code",
@@ -375,7 +387,9 @@
375
  {
376
  "cell_type": "markdown",
377
  "metadata": {},
378
- "source": ["## 10. Configure GRPO Training"]
 
 
379
  },
380
  {
381
  "cell_type": "code",
@@ -423,7 +437,9 @@
423
  {
424
  "cell_type": "markdown",
425
  "metadata": {},
426
- "source": ["## 11. Create Trainer"]
 
 
427
  },
428
  {
429
  "cell_type": "code",
@@ -431,17 +447,17 @@
431
  "metadata": {},
432
  "outputs": [],
433
  "source": [
434
- "from trl import GRPOTrainer\n",
435
  "\n",
436
- "trainer = GRPOTrainer(\n",
437
  " model=model,\n",
438
  " processing_class=tokenizer,\n",
439
- " reward_funcs=reward_func,\n",
440
  " train_dataset=dataset,\n",
441
  " args=grpo_config,\n",
442
  " rollout_func=rollout_func,\n",
443
  ")\n",
444
- "print('GRPOTrainer ready')"
445
  ]
446
  },
447
  {
@@ -485,7 +501,9 @@
485
  {
486
  "cell_type": "markdown",
487
  "metadata": {},
488
- "source": ["## 13. Save + Push"]
 
 
489
  },
490
  {
491
  "cell_type": "code",
@@ -509,7 +527,9 @@
509
  {
510
  "cell_type": "markdown",
511
  "metadata": {},
512
- "source": ["## 14. Merge LoRA → bf16 (Optional — for full-weight inference)"]
 
 
513
  },
514
  {
515
  "cell_type": "code",
@@ -533,7 +553,9 @@
533
  {
534
  "cell_type": "markdown",
535
  "metadata": {},
536
- "source": ["## 15. Evaluate: Baseline vs Trained"]
 
 
537
  },
538
  {
539
  "cell_type": "code",
@@ -621,7 +643,9 @@
621
  {
622
  "cell_type": "markdown",
623
  "metadata": {},
624
- "source": ["## 16. Plot"]
 
 
625
  },
626
  {
627
  "cell_type": "code",
@@ -660,7 +684,9 @@
660
  {
661
  "cell_type": "markdown",
662
  "metadata": {},
663
- "source": ["## 17. Teardown"]
 
 
664
  },
665
  {
666
  "cell_type": "code",
@@ -674,9 +700,16 @@
674
  }
675
  ],
676
  "metadata": {
677
- "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
678
- "language_info": {"name": "python", "version": "3.12.0"}
 
 
 
 
 
 
 
679
  },
680
  "nbformat": 4,
681
  "nbformat_minor": 4
682
- }
 
21
  {
22
  "cell_type": "markdown",
23
  "metadata": {},
24
+ "source": [
25
+ "## 0. Install Dependencies"
26
+ ]
27
  },
28
  {
29
  "cell_type": "code",
 
49
  {
50
  "cell_type": "markdown",
51
  "metadata": {},
52
+ "source": [
53
+ "## 1. Version Check + GPU Detect"
54
+ ]
55
  },
56
  {
57
  "cell_type": "code",
 
85
  {
86
  "cell_type": "markdown",
87
  "metadata": {},
88
+ "source": [
89
+ "## 2. Clone PM-Ops Repo"
90
+ ]
91
  },
92
  {
93
  "cell_type": "code",
 
117
  {
118
  "cell_type": "markdown",
119
  "metadata": {},
120
+ "source": [
121
+ "## 3. HuggingFace Login"
122
+ ]
123
  },
124
  {
125
  "cell_type": "code",
 
169
  {
170
  "cell_type": "markdown",
171
  "metadata": {},
172
+ "source": [
173
+ "## 5. Verify Environment"
174
+ ]
175
  },
176
  {
177
  "cell_type": "code",
 
249
  {
250
  "cell_type": "markdown",
251
  "metadata": {},
252
+ "source": [
253
+ "## 7. Generate Training Dataset"
254
+ ]
255
  },
256
  {
257
  "cell_type": "code",
 
387
  {
388
  "cell_type": "markdown",
389
  "metadata": {},
390
+ "source": [
391
+ "## 10. Configure GRPO Training"
392
+ ]
393
  },
394
  {
395
  "cell_type": "code",
 
437
  {
438
  "cell_type": "markdown",
439
  "metadata": {},
440
+ "source": [
441
+ "## 11. Create Trainer"
442
+ ]
443
  },
444
  {
445
  "cell_type": "code",
 
447
  "metadata": {},
448
  "outputs": [],
449
  "source": [
450
+ "from training.pm_ops_trainer import PMOpsGRPOTrainer\n",
451
  "\n",
452
+ "trainer = PMOpsGRPOTrainer(\n",
453
  " model=model,\n",
454
  " processing_class=tokenizer,\n",
455
+ " reward_funcs=reward_func, # fallback only — injected rewards take priority\n",
456
  " train_dataset=dataset,\n",
457
  " args=grpo_config,\n",
458
  " rollout_func=rollout_func,\n",
459
  ")\n",
460
+ "print(\"PMOpsGRPOTrainer ready — direct reward injection active\")"
461
  ]
462
  },
463
  {
 
501
  {
502
  "cell_type": "markdown",
503
  "metadata": {},
504
+ "source": [
505
+ "## 13. Save + Push"
506
+ ]
507
  },
508
  {
509
  "cell_type": "code",
 
527
  {
528
  "cell_type": "markdown",
529
  "metadata": {},
530
+ "source": [
531
+ "## 14. Merge LoRA → bf16 (Optional — for full-weight inference)"
532
+ ]
533
  },
534
  {
535
  "cell_type": "code",
 
553
  {
554
  "cell_type": "markdown",
555
  "metadata": {},
556
+ "source": [
557
+ "## 15. Evaluate: Baseline vs Trained"
558
+ ]
559
  },
560
  {
561
  "cell_type": "code",
 
643
  {
644
  "cell_type": "markdown",
645
  "metadata": {},
646
+ "source": [
647
+ "## 16. Plot"
648
+ ]
649
  },
650
  {
651
  "cell_type": "code",
 
684
  {
685
  "cell_type": "markdown",
686
  "metadata": {},
687
+ "source": [
688
+ "## 17. Teardown"
689
+ ]
690
  },
691
  {
692
  "cell_type": "code",
 
700
  }
701
  ],
702
  "metadata": {
703
+ "kernelspec": {
704
+ "display_name": "Python 3",
705
+ "language": "python",
706
+ "name": "python3"
707
+ },
708
+ "language_info": {
709
+ "name": "python",
710
+ "version": "3.12.0"
711
+ }
712
  },
713
  "nbformat": 4,
714
  "nbformat_minor": 4
715
+ }