szxllm commited on
Commit
5963aaa
·
verified ·
1 Parent(s): afd1085

Update grpo.py

Browse files
Files changed (1) hide show
  1. grpo.py +234 -477
grpo.py CHANGED
@@ -1,539 +1,296 @@
1
  import torch
2
- import torch.optim as optim
3
  import torch.nn.functional as F
 
4
  from torch.utils.data import DataLoader, TensorDataset
5
- from typing import Dict, List, Tuple, Optional
6
  from tqdm import tqdm
7
- import numpy as np
8
- import gc
9
  import logging
 
 
10
 
11
- logging.basicConfig(level=logging.INFO)
12
- logger = logging.getLogger(__name__)
13
 
 
14
 
15
- class GRPOTrainer:
16
  def __init__(
17
  self,
18
  actor_model,
19
- reward_model,
20
  ref_model,
21
  tokenizer,
22
  learning_rate: float = 1e-6,
23
- kl_coef: float = 0.04,
24
- group_size: int = 4,
25
  clip_epsilon: float = 0.2,
26
- max_grad_norm: float = 1.0,
27
  grpo_epochs: int = 1,
28
- update_batch_size: int = 4,
29
  use_amp: bool = True,
30
- value_clip: bool = False,
31
- entropy_coef: float = 0.01,
32
- advantage_normalization: str = 'group', # 'group', 'global', 'none'
33
- kl_estimation_method: str = 'forward' # 'forward', 'reverse', 'symmetric'
34
  ):
35
  self.actor = actor_model
36
- self.reward_model = reward_model
37
  self.ref_model = ref_model
38
  self.tokenizer = tokenizer
 
39
 
40
  self.kl_coef = kl_coef
41
  self.group_size = group_size
42
  self.clip_epsilon = clip_epsilon
43
- self.max_grad_norm = max_grad_norm
44
  self.grpo_epochs = grpo_epochs
45
- self.update_batch_size = update_batch_size
46
  self.use_amp = use_amp
47
- self.entropy_coef = entropy_coef
48
- self.advantage_normalization = advantage_normalization
49
- self.kl_estimation_method = kl_estimation_method
50
 
51
- self.device = next(actor_model.parameters()).device
 
 
52
 
53
- # 冻结参考模型和奖励模型
54
- self.ref_model.eval()
55
- self.ref_model.requires_grad_(False)
56
- self.reward_model.eval()
57
- self.reward_model.requires_grad_(False)
58
 
59
- # 优化器配置
60
- self.optimizer = optim.AdamW(
61
- filter(lambda p: p.requires_grad, actor_model.parameters()),
 
 
 
 
62
  lr=learning_rate,
63
- weight_decay=0.01,
64
- betas=(0.9, 0.95),
65
- eps=1e-8
66
  )
67
-
68
- # 混合精度训练
69
- self.scaler = torch.amp.GradScaler('cuda', enabled=self.use_amp)
70
-
71
- self.training_stats = {
72
- 'iterations': 0,
73
- 'total_samples': 0,
74
- 'avg_rewards': [],
75
- 'avg_kl': [],
76
- 'policy_losses': []
77
- }
78
-
79
- logger.info(f"GRPO Trainer initialized:")
80
- logger.info(f" Group Size: {group_size}")
81
- logger.info(f" KL Coef: {kl_coef}")
82
- logger.info(f" Clip Epsilon: {clip_epsilon}")
83
- logger.info(f" Learning Rate: {learning_rate}")
84
- logger.info(f" Update Batch Size: {update_batch_size}")
85
- logger.info(f" Mixed Precision: {use_amp}")
86
- logger.info(f" KL Estimation: {kl_estimation_method}")
87
 
88
- def _compute_kl_divergence(
89
- self,
90
- log_probs: torch.Tensor,
91
- ref_log_probs: torch.Tensor,
92
- mask: torch.Tensor
93
- ) -> torch.Tensor:
94
 
95
- if self.kl_estimation_method == 'forward':
96
- kl = log_probs - ref_log_probs
97
- elif self.kl_estimation_method == 'reverse':
98
- kl = ref_log_probs - log_probs
99
- else:
100
- forward_kl = log_probs - ref_log_probs
101
- reverse_kl = ref_log_probs - log_probs
102
- kl = 0.5 * (forward_kl + reverse_kl)
103
-
104
- kl_penalty = (kl * mask).sum(dim=-1)
105
- return kl_penalty
106
 
107
  @torch.no_grad()
108
- def generate_experience(
109
- self,
110
- prompts_dataloader: DataLoader,
111
- max_gen_len: int,
112
- temperature: float = 1.0,
113
- top_p: float = 0.9
114
- ) -> Dict:
115
-
116
  self.actor.eval()
117
 
118
- all_sequences = []
119
- all_log_probs = []
120
- all_advantages = []
121
- all_prompt_lens = []
122
- all_rewards = []
123
 
124
- logger.info("Generating experience...")
 
 
 
 
 
125
 
126
- for prompts in tqdm(prompts_dataloader, desc="Generating Experience"):
127
- try:
128
- # 处理不同的输入格式
129
- if isinstance(prompts, (list, tuple)):
130
- prompts = prompts[0]
131
-
132
- prompts = prompts.to(self.device)
133
- batch_size = prompts.shape[0]
134
-
135
- # 扩展prompts以生成group_size个样本
136
- prompts_repeated = prompts.repeat_interleave(self.group_size, dim=0)
137
- prompt_len = prompts_repeated.shape[1]
138
-
139
- input_data = {
140
- 'segments': [{
141
- 'type': 'text',
142
- 'data': prompts_repeated,
143
- 'modality_id': 0
144
- }]
145
- }
146
-
147
- # 1. 采样生成
148
- with torch.amp.autocast('cuda', enabled=self.use_amp):
149
- response_ids = self.actor.generate(
150
- input_data,
151
- max_new_tokens=max_gen_len,
152
- do_sample=True,
153
- temperature=temperature,
154
- top_p=top_p,
155
- eos_token_id=self.tokenizer.eos_token_id,
156
- pad_token_id=self.tokenizer.pad_token_id,
157
- use_cache=True
158
- )
159
-
160
- sequences = torch.cat([prompts_repeated, response_ids], dim=1)
161
-
162
- # 检查序列长度
163
- if sequences.shape[1] <= prompt_len:
164
- logger.warning("Generated sequence too short, skipping batch")
165
- continue
166
-
167
- full_input_data = {
168
- 'segments': [{
169
- 'type': 'text',
170
- 'data': sequences,
171
- 'modality_id': 0
172
- }]
173
- }
174
-
175
- # 2. 计算当前策略和参考策略的 LogProbs
176
- with torch.amp.autocast('cuda', enabled=self.use_amp):
177
- actor_out = self.actor(full_input_data)
178
- ref_out = self.ref_model(full_input_data)
179
-
180
- logits = actor_out['logits'][:, :-1, :]
181
- ref_logits = ref_out['logits'][:, :-1, :]
182
- targets = sequences[:, 1:]
183
-
184
- log_probs = F.log_softmax(logits, dim=-1)
185
- ref_log_probs = F.log_softmax(ref_logits, dim=-1)
186
-
187
- # 提取对应token的log概率
188
- per_token_log_probs = torch.gather(
189
- log_probs, -1, targets.unsqueeze(-1)
190
- ).squeeze(-1)
191
- per_token_ref_log_probs = torch.gather(
192
- ref_log_probs, -1, targets.unsqueeze(-1)
193
- ).squeeze(-1)
194
-
195
- # 3. 计算 KL 散度 (只针对response部分)
196
- response_mask = torch.arange(
197
- sequences.size(1) - 1, device=self.device
198
- ) >= (prompt_len - 1)
199
- response_mask = response_mask.unsqueeze(0).expand_as(per_token_log_probs)
200
- response_mask = response_mask.float()
201
-
202
- kl_penalty = self._compute_kl_divergence(
203
- per_token_log_probs,
204
- per_token_ref_log_probs,
205
- response_mask
206
- )
207
-
208
- with torch.amp.autocast('cuda', enabled=self.use_amp):
209
- reward_output = self.reward_model(full_input_data)
210
-
211
- # reward_model返回 (batch_size, seq_len),取最后一个位置的奖励
212
- if reward_output.dim() == 2:
213
- raw_rewards = reward_output[:, -1]
214
- else:
215
- raw_rewards = reward_output.squeeze(-1)
216
-
217
- # 5. 组合总奖励: R_total = R_env - β * KL
218
- total_rewards = raw_rewards - self.kl_coef * kl_penalty
219
-
220
- # 6. 计算组内相对优势
221
- rewards_grouped = total_rewards.view(batch_size, self.group_size)
222
-
223
- if self.advantage_normalization == 'group':
224
- # 组内标准化
225
- mean_grouped = rewards_grouped.mean(dim=1, keepdim=True)
226
- std_grouped = rewards_grouped.std(dim=1, keepdim=True) + 1e-8
227
- advantages = (rewards_grouped - mean_grouped) / std_grouped
228
- elif self.advantage_normalization == 'global':
229
- # 全局标准化
230
- advantages = (rewards_grouped - rewards_grouped.mean()) / (
231
- rewards_grouped.std() + 1e-8
232
- )
233
- else: # 'none'
234
- advantages = rewards_grouped - rewards_grouped.mean(dim=1, keepdim=True)
235
-
236
- advantages = advantages.view(-1)
237
-
238
- # 保存数据
239
- all_sequences.append(sequences.cpu())
240
- all_log_probs.append(per_token_log_probs.detach().cpu())
241
- all_advantages.append(advantages.detach().cpu())
242
- all_prompt_lens.append(
243
- torch.full((sequences.size(0),), prompt_len, dtype=torch.long)
244
- )
245
- all_rewards.append(total_rewards.detach().cpu())
246
-
247
- # 清理中间变量
248
- del logits, ref_logits, actor_out, ref_out
249
- del log_probs, ref_log_probs, reward_output
250
-
251
- except Exception as e:
252
- logger.error(f"Error generating experience for batch: {e}")
253
- import traceback
254
- traceback.print_exc()
255
- continue
256
-
257
- finally:
258
- torch.cuda.empty_cache()
259
 
260
- if not all_sequences:
261
- raise RuntimeError("No valid sequences generated")
 
262
 
263
- # 合并所有数据
264
- experience = {
265
- 'sequences': torch.cat(all_sequences, dim=0),
266
- 'log_probs': torch.cat(all_log_probs, dim=0),
267
- 'advantages': torch.cat(all_advantages, dim=0),
268
- 'prompt_lengths': torch.cat(all_prompt_lens, dim=0),
269
- 'rewards': torch.cat(all_rewards, dim=0)
270
  }
271
 
272
- # 统计信息
273
- logger.info(f"Generated {len(experience['sequences'])} sequences")
274
- logger.info(f"Avg Reward: {experience['rewards'].mean().item():.4f}")
275
- logger.info(f"Reward Std: {experience['rewards'].std().item():.4f}")
276
- logger.info(f"Avg Advantage: {experience['advantages'].mean().item():.4f}")
 
 
 
 
 
 
 
 
 
 
 
277
 
278
- return experience
 
 
 
 
 
279
 
280
- def grpo_step(
281
- self,
282
- dataset: TensorDataset
283
- ) -> Dict[str, float]:
284
- self.actor.train()
 
 
285
 
286
- dataloader = DataLoader(
287
- dataset,
288
- batch_size=self.update_batch_size,
289
- shuffle=True,
290
- drop_last=False
291
- )
 
292
 
293
- epoch_stats = {
294
- 'total_loss': 0.0,
295
- 'policy_loss': 0.0,
296
- 'entropy': 0.0,
297
- 'approx_kl': 0.0,
298
- 'clip_fraction': 0.0,
299
- 'steps': 0
300
- }
301
 
302
- for batch_data in dataloader:
303
- sequences, old_log_probs, advantages, prompt_lens = batch_data
304
-
305
- sequences = sequences.to(self.device)
306
- old_log_probs = old_log_probs.to(self.device)
307
- advantages = advantages.to(self.device)
308
-
309
- input_data = {
310
- 'segments': [{
311
- 'type': 'text',
312
- 'data': sequences,
313
- 'modality_id': 0
314
- }]
315
- }
316
-
317
- with torch.amp.autocast('cuda', enabled=self.use_amp):
318
- outputs = self.actor(input_data)
319
- logits = outputs['logits'][:, :-1, :]
320
-
321
- # 计算新的log probabilities
322
- targets = sequences[:, 1:]
323
- log_probs_dist = F.log_softmax(logits, dim=-1)
324
- new_log_probs = torch.gather(
325
- log_probs_dist, -1, targets.unsqueeze(-1)
326
- ).squeeze(-1)
327
-
328
- # 构建response mask
329
- mask = torch.zeros_like(new_log_probs)
330
- for i, pl in enumerate(prompt_lens):
331
- mask[i, pl-1:] = 1.0
332
-
333
- # 计算概率比率
334
- ratio = torch.exp(new_log_probs - old_log_probs)
335
-
336
- # 扩展advantages到序列维度
337
- adv_expanded = advantages.unsqueeze(-1).expand_as(new_log_probs)
338
-
339
- # PPO clip损失
340
- surr1 = ratio * adv_expanded
341
- surr2 = torch.clamp(
342
- ratio,
343
- 1.0 - self.clip_epsilon,
344
- 1.0 + self.clip_epsilon
345
- ) * adv_expanded
346
-
347
- # 策略损失
348
- policy_loss = -torch.min(surr1, surr2)
349
- policy_loss = (policy_loss * mask).sum() / (mask.sum() + 1e-8)
350
-
351
- # 熵奖励
352
- probs = F.softmax(logits, dim=-1)
353
- entropy = -(probs * log_probs_dist).sum(dim=-1)
354
- entropy_bonus = (entropy * mask).sum() / (mask.sum() + 1e-8)
355
-
356
- # 总损失
357
- loss = policy_loss - self.entropy_coef * entropy_bonus
358
-
359
- # 统计信息
360
- with torch.no_grad():
361
- log_ratio = new_log_probs - old_log_probs
362
- approx_kl = ((ratio - 1) - log_ratio) * mask
363
- approx_kl = approx_kl.sum() / (mask.sum() + 1e-8)
364
-
365
- clip_fraction = ((ratio > 1 + self.clip_epsilon) |
366
- (ratio < 1 - self.clip_epsilon)).float()
367
- clip_fraction = (clip_fraction * mask).sum() / (mask.sum() + 1e-8)
368
-
369
- self.optimizer.zero_grad()
370
- self.scaler.scale(loss).backward()
371
-
372
- # 梯度裁剪
373
- self.scaler.unscale_(self.optimizer)
374
- grad_norm = torch.nn.utils.clip_grad_norm_(
375
- self.actor.parameters(),
376
- self.max_grad_norm
377
  )
378
 
379
- self.scaler.step(self.optimizer)
380
- self.scaler.update()
381
-
382
- # 累积统计
383
- epoch_stats['total_loss'] += loss.item()
384
- epoch_stats['policy_loss'] += policy_loss.item()
385
- epoch_stats['entropy'] += entropy_bonus.item()
386
- epoch_stats['approx_kl'] += approx_kl.item()
387
- epoch_stats['clip_fraction'] += clip_fraction.item()
388
- epoch_stats['steps'] += 1
389
-
390
- # 计算平均值
391
- for key in epoch_stats:
392
- if key != 'steps':
393
- epoch_stats[key] /= max(epoch_stats['steps'], 1)
394
 
395
- return epoch_stats
396
-
397
- def train(
398
- self,
399
- prompt_dataloader: DataLoader,
400
- num_iterations: int = 1,
401
- max_gen_len: int = 50,
402
- temperature: float = 1.0,
403
- save_every: int = 5,
404
- save_path: str = "checkpoints"
405
- ):
406
-
407
- logger.info(f"\n{'='*80}")
408
- logger.info(f"Starting GRPO Training")
409
- logger.info(f" Iterations: {num_iterations}")
410
- logger.info(f" Max Gen Length: {max_gen_len}")
411
- logger.info(f" Temperature: {temperature}")
412
- logger.info(f"{'='*80}\n")
413
-
414
- for iteration in range(num_iterations):
415
- try:
416
- # 1. 生成经验
417
- experience = self.generate_experience(
418
- prompt_dataloader,
419
- max_gen_len,
420
- temperature
421
- )
422
-
423
- dataset = TensorDataset(
424
- experience['sequences'],
425
- experience['log_probs'],
426
- experience['advantages'],
427
- experience['prompt_lengths']
428
- )
429
-
430
- # 2. 策略优化
431
- logger.info(f"Optimizing policy for {self.grpo_epochs} epochs...")
432
- all_epoch_stats = []
433
-
434
- for epoch in range(self.grpo_epochs):
435
- stats = self.grpo_step(dataset)
436
- all_epoch_stats.append(stats)
437
-
438
- logger.info(
439
- f" Epoch {epoch+1}/{self.grpo_epochs} | "
440
- f"Loss: {stats['total_loss']:.4f} | "
441
- f"KL: {stats['approx_kl']:.4f} | "
442
- f"Clip%: {stats['clip_fraction']*100:.1f}"
443
- )
444
-
445
- # 3. 汇总统计
446
- avg_stats = {
447
- key: np.mean([s[key] for s in all_epoch_stats])
448
- for key in all_epoch_stats[0].keys()
449
- }
450
-
451
- self.training_stats['iterations'] += 1
452
- self.training_stats['total_samples'] += len(experience['sequences'])
453
- self.training_stats['avg_rewards'].append(
454
- experience['rewards'].mean().item()
455
- )
456
- self.training_stats['avg_kl'].append(avg_stats['approx_kl'])
457
- self.training_stats['policy_losses'].append(avg_stats['policy_loss'])
458
-
459
- # 4. 打印进度
460
- logger.info(f"\n{'='*80}")
461
- logger.info(f"Iteration {iteration+1}/{num_iterations} Complete")
462
- logger.info(f" Avg Reward: {experience['rewards'].mean():.4f}")
463
- logger.info(f" Avg Advantage: {experience['advantages'].mean():.4f}")
464
- logger.info(f" Policy Loss: {avg_stats['policy_loss']:.4f}")
465
- logger.info(f" Approx KL: {avg_stats['approx_kl']:.4f}")
466
- logger.info(f" Entropy: {avg_stats['entropy']:.4f}")
467
- logger.info(f" Clip Fraction: {avg_stats['clip_fraction']*100:.1f}%")
468
- logger.info(f"{'='*80}\n")
469
-
470
- # 5. 保存checkpoint
471
- if (iteration + 1) % save_every == 0:
472
- self.save_checkpoint(
473
- f"{save_path}/grpo_iter_{iteration+1}.pt"
474
- )
475
-
476
- # 6. 清理内存
477
- del experience, dataset
478
- gc.collect()
479
- torch.cuda.empty_cache()
480
-
481
- except Exception as e:
482
- logger.error(f"Error in iteration {iteration+1}: {e}")
483
- import traceback
484
- traceback.print_exc()
485
- continue
486
 
487
- logger.info("GRPO Training Complete!")
488
- self.print_training_summary()
489
-
490
- def save_checkpoint(self, path: str):
491
- import os
492
- os.makedirs(os.path.dirname(path), exist_ok=True)
493
 
494
- checkpoint = {
495
- 'actor_state_dict': self.actor.state_dict(),
496
- 'optimizer_state_dict': self.optimizer.state_dict(),
497
- 'scaler_state_dict': self.scaler.state_dict(),
498
- 'training_stats': self.training_stats,
499
- 'config': {
500
- 'kl_coef': self.kl_coef,
501
- 'group_size': self.group_size,
502
- 'clip_epsilon': self.clip_epsilon,
503
- }
504
- }
505
 
506
- torch.save(checkpoint, path)
507
- logger.info(f"Checkpoint saved to {path}")
508
-
509
- def load_checkpoint(self, path: str):
510
- checkpoint = torch.load(path, map_location=self.device)
511
 
512
- self.actor.load_state_dict(checkpoint['actor_state_dict'])
513
- self.optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
514
 
515
- if 'scaler_state_dict' in checkpoint and self.use_amp:
516
- self.scaler.load_state_dict(checkpoint['scaler_state_dict'])
 
 
 
 
517
 
518
- self.training_stats = checkpoint['training_stats']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
 
520
- logger.info(f"Checkpoint loaded from {path}")
 
 
 
 
 
 
 
 
 
 
 
 
521
 
522
- def print_training_summary(self):
523
- logger.info("\n" + "="*80)
524
- logger.info("Training Summary")
525
- logger.info("="*80)
526
- logger.info(f"Total Iterations: {self.training_stats['iterations']}")
527
- logger.info(f"Total Samples: {self.training_stats['total_samples']}")
 
 
 
 
528
 
529
- if self.training_stats['avg_rewards']:
530
- logger.info(
531
- f"Final Avg Reward: "
532
- f"{self.training_stats['avg_rewards'][-1]:.4f}"
533
- )
534
- logger.info(
535
- f"Reward Improvement: "
536
- f"{self.training_stats['avg_rewards'][-1] - self.training_stats['avg_rewards'][0]:.4f}"
537
- )
538
 
539
- logger.info("="*80 + "\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import torch
 
2
  import torch.nn.functional as F
3
+ import torch.distributed as dist
4
  from torch.utils.data import DataLoader, TensorDataset
 
5
  from tqdm import tqdm
 
 
6
  import logging
7
+ import os
8
+ import gc
9
 
10
+ from math_verifier import MathReward
 
11
 
12
+ logger = logging.getLogger(__name__)
13
 
14
+ class GRPOZeroTrainer:
15
  def __init__(
16
  self,
17
  actor_model,
 
18
  ref_model,
19
  tokenizer,
20
  learning_rate: float = 1e-6,
21
+ kl_coef: float = 0.01,
22
+ group_size: int = 4,
23
  clip_epsilon: float = 0.2,
 
24
  grpo_epochs: int = 1,
25
+ max_grad_norm: float = 1.0,
26
  use_amp: bool = True,
27
+ gradient_accumulation_steps: int = 12,
28
+ inner_batch_size: int = 4
 
 
29
  ):
30
  self.actor = actor_model
 
31
  self.ref_model = ref_model
32
  self.tokenizer = tokenizer
33
+ self.math_verifier = MathReward()
34
 
35
  self.kl_coef = kl_coef
36
  self.group_size = group_size
37
  self.clip_epsilon = clip_epsilon
 
38
  self.grpo_epochs = grpo_epochs
 
39
  self.use_amp = use_amp
40
+ self.max_grad_norm = max_grad_norm
 
 
41
 
42
+ self.gradient_accumulation_steps = gradient_accumulation_steps
43
+ self.inner_batch_size = inner_batch_size
44
+ self.experience_buffer = []
45
 
46
+ self.rank = int(os.environ.get("RANK", 0))
 
 
 
 
47
 
48
+ if hasattr(actor_model, 'module'):
49
+ self.device = next(actor_model.module.parameters()).device
50
+ else:
51
+ self.device = next(actor_model.parameters()).device
52
+
53
+ self.optimizer = torch.optim.AdamW(
54
+ self.actor.parameters(),
55
  lr=learning_rate,
56
+ weight_decay=0.01
 
 
57
  )
58
+ self.scaler = torch.amp.GradScaler('cuda', enabled=use_amp)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
+ self.ref_model.eval()
61
+ self.ref_model.requires_grad_(False)
 
 
 
 
62
 
63
+ def _get_unwrapped_model(self, model):
64
+ if hasattr(model, 'module'):
65
+ return model.module
66
+ return model
 
 
 
 
 
 
 
67
 
68
  @torch.no_grad()
69
+ def generate_and_score(self, prompt_batch, max_gen_len=512, temperature=1.0):
70
+ """生成并打分"""
 
 
 
 
 
 
71
  self.actor.eval()
72
 
73
+ # 1. 准备输入
74
+ prompts_text = prompt_batch['prompt']
75
+ ground_truths = prompt_batch['ground_truth']
 
 
76
 
77
+ inputs = self.tokenizer(
78
+ prompts_text,
79
+ return_tensors="pt",
80
+ padding=True,
81
+ padding_side="left"
82
+ ).to(self.device)
83
 
84
+ prompts_ids = inputs['input_ids']
85
+ attention_mask = inputs['attention_mask']
86
+ prompt_len = int(prompts_ids.shape[1])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ # 重复输入以进行 Group 采样
89
+ prompts_ids_repeated = prompts_ids.repeat_interleave(self.group_size, dim=0)
90
+ attention_mask_repeated = attention_mask.repeat_interleave(self.group_size, dim=0)
91
 
92
+ input_data = {
93
+ 'segments': [{'type': 'text', 'data': prompts_ids_repeated, 'modality_id': 0}],
94
+ 'attention_mask': attention_mask_repeated
 
 
 
 
95
  }
96
 
97
+ # 2. 生成
98
+ unwrapped_actor = self._get_unwrapped_model(self.actor)
99
+ with torch.amp.autocast('cuda', enabled=self.use_amp):
100
+ generated_ids = unwrapped_actor.generate(
101
+ input_data,
102
+ max_new_tokens=max_gen_len,
103
+ do_sample=True,
104
+ temperature=temperature,
105
+ top_p=0.95,
106
+ pad_token_id=self.tokenizer.pad_token_id
107
+ )
108
+
109
+ # 3. 处理生成结果
110
+ sequences = torch.cat([prompts_ids_repeated, generated_ids], dim=1)
111
+ only_response_ids = generated_ids
112
+ decoded_responses = self.tokenizer.batch_decode(only_response_ids, skip_special_tokens=True)
113
 
114
+ full_responses_for_reward = []
115
+ for r in decoded_responses:
116
+ if not r.strip().startswith("<think>"):
117
+ full_responses_for_reward.append("<think>\n" + r.strip())
118
+ else:
119
+ full_responses_for_reward.append(r)
120
 
121
+ # 4. 计算规则奖励
122
+ expanded_gts = []
123
+ for gt in ground_truths:
124
+ expanded_gts.extend([gt] * self.group_size)
125
+
126
+ raw_rewards = self.math_verifier.compute_rewards(full_responses_for_reward, expanded_gts)
127
+ rewards_tensor = torch.tensor(raw_rewards, device=self.device, dtype=torch.float32)
128
 
129
+ # 5. 计算 LogProbs (Actor & Ref)
130
+ gen_mask = (generated_ids != self.tokenizer.pad_token_id).long()
131
+ full_attention_mask = torch.cat([attention_mask_repeated, gen_mask], dim=1)
132
+
133
+ batch_size = sequences.size(0)
134
+ seq_len = sequences.size(1)
135
+ position_ids = torch.zeros((batch_size, seq_len), dtype=torch.long, device=self.device)
136
 
137
+ for i in range(batch_size):
138
+ non_pad_positions = (full_attention_mask[i] == 1).nonzero(as_tuple=True)[0]
139
+ if len(non_pad_positions) > 0:
140
+ start_pos = non_pad_positions[0].item()
141
+ valid_len = len(non_pad_positions)
142
+ position_ids[i, start_pos:start_pos + valid_len] = torch.arange(valid_len, device=self.device)
 
 
143
 
144
+ full_input_data = {'segments': [{'type': 'text', 'data': sequences, 'modality_id': 0}]}
145
+
146
+ with torch.amp.autocast('cuda', enabled=self.use_amp):
147
+ actor_out = self.actor(
148
+ full_input_data,
149
+ attention_mask=full_attention_mask,
150
+ position_ids=position_ids
151
+ )
152
+ ref_out = self.ref_model(
153
+ full_input_data,
154
+ attention_mask=full_attention_mask,
155
+ position_ids=position_ids
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  )
157
 
158
+ actor_logits = actor_out['logits'][:, :-1, :]
159
+ ref_logits = ref_out['logits'][:, :-1, :]
160
+ targets = sequences[:, 1:]
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
+ actor_log_probs = F.log_softmax(actor_logits, dim=-1)
163
+ ref_log_probs = F.log_softmax(ref_logits, dim=-1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ per_token_log_probs = torch.gather(actor_log_probs, -1, targets.unsqueeze(-1)).squeeze(-1)
166
+ per_token_ref_log_probs = torch.gather(ref_log_probs, -1, targets.unsqueeze(-1)).squeeze(-1)
 
 
 
 
167
 
168
+ # 6. 计算 KL 惩罚
169
+ mask = torch.arange(sequences.size(1) - 1, device=self.device) >= (prompt_len - 1)
170
+ mask = mask.unsqueeze(0).expand_as(per_token_log_probs).float()
171
+ is_padding = (targets == self.tokenizer.pad_token_id)
172
+ mask = mask * (~is_padding).float()
 
 
 
 
 
 
173
 
174
+ kl_div = per_token_log_probs - per_token_ref_log_probs
175
+ kl_div = torch.clamp(kl_div, min=-10.0, max=10.0)
176
+ kl_safe = torch.where(mask.bool(), kl_div, torch.tensor(0., device=self.device))
177
+ kl_penalty = kl_safe.sum(dim=-1)
 
178
 
179
+ # 7. 计算最终 Advantage
180
+ total_rewards = rewards_tensor - self.kl_coef * kl_penalty
181
 
182
+ # Group Normalization
183
+ total_rewards = total_rewards.view(-1, self.group_size)
184
+ mean_rewards = total_rewards.mean(dim=1, keepdim=True)
185
+ std_rewards = total_rewards.std(dim=1, keepdim=True) + 1e-8
186
+ advantages = (total_rewards - mean_rewards) / std_rewards
187
+ advantages = advantages.view(-1)
188
 
189
+ return {
190
+ 'sequences': sequences.detach().cpu(),
191
+ 'old_log_probs': per_token_log_probs.detach().cpu(),
192
+ 'advantages': advantages.detach().cpu(),
193
+ 'attention_mask': full_attention_mask.cpu(),
194
+ 'position_ids': position_ids.cpu(),
195
+ 'prompt_lengths': torch.full((sequences.size(0),), prompt_len, dtype=torch.long).cpu(),
196
+ 'avg_reward': rewards_tensor.mean().item()
197
+ }
198
+
199
+ def train_step(self, experience):
200
+ self.experience_buffer.append(experience)
201
+ if len(self.experience_buffer) < self.gradient_accumulation_steps:
202
+ return None
203
+
204
+ self.actor.train()
205
+
206
+ max_seq_len = max([e['sequences'].size(1) for e in self.experience_buffer])
207
+ max_lp_len = max([e['old_log_probs'].size(1) for e in self.experience_buffer])
208
+
209
+ def pad_tensor(t, target_len, pad_value):
210
+ return F.pad(t, (0, target_len - t.size(1)), value=pad_value)
211
+
212
+ padded_sequences = []
213
+ padded_old_log_probs = []
214
+ padded_attention_masks = []
215
+ padded_position_ids = []
216
 
217
+ for e in self.experience_buffer:
218
+ padded_sequences.append(pad_tensor(e['sequences'], max_seq_len, self.tokenizer.pad_token_id))
219
+
220
+ padded_old_log_probs.append(pad_tensor(e['old_log_probs'], max_lp_len, 0.0))
221
+ padded_attention_masks.append(pad_tensor(e['attention_mask'], max_seq_len, 0))
222
+ padded_position_ids.append(pad_tensor(e['position_ids'], max_seq_len, 0))
223
+
224
+ cat_sequences = torch.cat(padded_sequences, dim=0)
225
+ cat_old_log_probs = torch.cat(padded_old_log_probs, dim=0)
226
+ cat_advantages = torch.cat([e['advantages'] for e in self.experience_buffer], dim=0)
227
+ cat_prompt_lengths = torch.cat([e['prompt_lengths'] for e in self.experience_buffer], dim=0)
228
+ cat_attention_masks = torch.cat(padded_attention_masks, dim=0)
229
+ cat_position_ids = torch.cat(padded_position_ids, dim=0)
230
 
231
+ self.experience_buffer = []
232
+
233
+ dataset = TensorDataset(
234
+ cat_sequences,
235
+ cat_old_log_probs,
236
+ cat_advantages,
237
+ cat_prompt_lengths,
238
+ cat_attention_masks,
239
+ cat_position_ids
240
+ )
241
 
242
+ dataloader = DataLoader(dataset, batch_size=self.inner_batch_size, shuffle=True)
 
 
 
 
 
 
 
 
243
 
244
+ total_loss = 0
245
+ update_steps = 0
246
+
247
+ for _ in range(self.grpo_epochs):
248
+ for batch in dataloader:
249
+ seqs, old_lp, advs, p_lens, attn_masks, pos_ids = [b.to(self.device) for b in batch]
250
+
251
+ input_data = {'segments': [{'type': 'text', 'data': seqs, 'modality_id': 0}]}
252
+
253
+ with torch.amp.autocast('cuda', enabled=self.use_amp):
254
+ outputs = self.actor(
255
+ input_data,
256
+ attention_mask=attn_masks,
257
+ position_ids=pos_ids
258
+ )
259
+ logits = outputs['logits'][:, :-1, :]
260
+ targets = seqs[:, 1:]
261
+
262
+ new_log_probs = F.log_softmax(logits, dim=-1)
263
+ new_token_log_probs = torch.gather(new_log_probs, -1, targets.unsqueeze(-1)).squeeze(-1)
264
+
265
+ mask = torch.zeros_like(new_token_log_probs)
266
+ for i, pl in enumerate(p_lens):
267
+ pl_val = int(pl.item())
268
+ if pl_val - 1 < mask.size(1):
269
+ mask[i, pl_val-1:] = 1.0
270
+
271
+ is_padding = (targets == self.tokenizer.pad_token_id)
272
+ is_valid_old_lp = (old_lp != 0.0)
273
+ mask = mask * (~is_padding).float() * is_valid_old_lp.float()
274
+
275
+ ratio = torch.exp(new_token_log_probs - old_lp)
276
+ ratio = torch.clamp(ratio, 0.0, 10.0)
277
+
278
+ surr1 = ratio * advs.unsqueeze(-1)
279
+ surr2 = torch.clamp(ratio, 1.0 - self.clip_epsilon, 1.0 + self.clip_epsilon) * advs.unsqueeze(-1)
280
+
281
+ policy_loss = -torch.min(surr1, surr2)
282
+ policy_loss = (policy_loss * mask).sum() / (mask.sum() + 1e-8)
283
+
284
+ loss = policy_loss
285
+
286
+ self.optimizer.zero_grad()
287
+ self.scaler.scale(loss).backward()
288
+ self.scaler.unscale_(self.optimizer)
289
+ torch.nn.utils.clip_grad_norm_(self.actor.parameters(), self.max_grad_norm)
290
+ self.scaler.step(self.optimizer)
291
+ self.scaler.update()
292
+
293
+ total_loss += loss.item()
294
+ update_steps += 1
295
+
296
+ return total_loss / max(update_steps, 1)