zhan1206 commited on
Commit
b0bc454
·
1 Parent(s): 376d8d7

v2.1.0: fix N25 off-by-one, S2 reward_fn string safety

Browse files

N25: mask_start = prompt_len - 2 (first gen token at index prompt_len-2)
Previously masked first gen token incorrectly with prompt_len - 1

S2: Add callable() check before invoking self.reward_fn
Also support string lookup from REWARD_FUNCTIONS for instance reward_fn

Minor: mask uses both -100 (PyTorch ignore) and 0 as pad markers

Tests: 25/25 pass (2 new: N25, reward_fn_string_safety)

models/thinking_dial.py CHANGED
@@ -505,10 +505,11 @@ class GRPOTrainer:
505
  shift_labels = labels[:, 1:].contiguous()
506
  log_probs = F.log_softmax(shift_logits, dim=-1)
507
  per_token_lp = log_probs.gather(2, shift_labels.unsqueeze(2)).squeeze(2) # (B, L)
508
- mask = (shift_labels != 0).float()
 
509
  if per_token:
510
- return per_token_lp * mask # (B, L)
511
- return (per_token_lp * mask).sum(dim=1) # (B,)
512
  return torch.log_softmax(logits[:, -1, :], dim=-1).sum(dim=-1)
513
 
514
  def compute_reward(
@@ -544,8 +545,11 @@ class GRPOTrainer:
544
  return reward_fn(prompt, response)
545
  if reward_fn is not None and reward_fn in self.REWARD_FUNCTIONS:
546
  return self.REWARD_FUNCTIONS[reward_fn](prompt, response)
547
- if self.reward_fn is not None:
 
548
  return self.reward_fn(prompt, response)
 
 
549
 
550
  score = 0.0
551
 
@@ -728,11 +732,12 @@ class GRPOTrainer:
728
 
729
  # Labels: shift right so log_probs[i] = P(token[i+1] | token[...i])
730
  use_labels = generated_ids[:, 1:].clone() # predict next token
731
- # N23 FIX: Get per-token log probs, then mask prompt positions correctly
732
  # generated_ids layout: [prompt_tokens | gen_tokens]
733
  # logits layout: [prompt_logits | gen_logits] (shifted by 1)
734
- # We want log_probs starting from position prompt_len-1 (first gen token prediction)
735
- mask_start = max(prompt_len - 1, 0) # logits at prompt_len-1 predict token at prompt_len
 
736
 
737
  log_probs_per_token = self._normalize_logits_to_log_probs(logits, use_labels, per_token=True) # (B*N, L)
738
  # Zero out prompt positions so GRPO loss only uses generated tokens
 
505
  shift_labels = labels[:, 1:].contiguous()
506
  log_probs = F.log_softmax(shift_logits, dim=-1)
507
  per_token_lp = log_probs.gather(2, shift_labels.unsqueeze(2)).squeeze(2) # (B, L)
508
+ # Use -100 as ignore index (standard PyTorch/HuggingFace convention for masked labels)
509
+ mask = (shift_labels != -100) & (shift_labels != 0)
510
  if per_token:
511
+ return per_token_lp * mask.float() # (B, L)
512
+ return (per_token_lp * mask.float()).sum(dim=1) # (B,)
513
  return torch.log_softmax(logits[:, -1, :], dim=-1).sum(dim=-1)
514
 
515
  def compute_reward(
 
545
  return reward_fn(prompt, response)
546
  if reward_fn is not None and reward_fn in self.REWARD_FUNCTIONS:
547
  return self.REWARD_FUNCTIONS[reward_fn](prompt, response)
548
+ # S2 FIX: Check callable before invoking instance reward_fn (could be string from registry)
549
+ if self.reward_fn is not None and callable(self.reward_fn):
550
  return self.reward_fn(prompt, response)
551
+ if self.reward_fn is not None and isinstance(self.reward_fn, str) and self.reward_fn in self.REWARD_FUNCTIONS:
552
+ return self.REWARD_FUNCTIONS[self.reward_fn](prompt, response)
553
 
554
  score = 0.0
555
 
 
732
 
733
  # Labels: shift right so log_probs[i] = P(token[i+1] | token[...i])
734
  use_labels = generated_ids[:, 1:].clone() # predict next token
735
+ # N23 FIX: Get per token log probs, then mask prompt positions correctly
736
  # generated_ids layout: [prompt_tokens | gen_tokens]
737
  # logits layout: [prompt_logits | gen_logits] (shifted by 1)
738
+ # We want log_probs starting from position prompt_len-2 (first gen token prediction)
739
+ # N25 FIX: off-by-one - first gen token is at index prompt_len-2, not prompt_len-1
740
+ mask_start = max(prompt_len - 2, 0) # logits at prompt_len-2 predict token at prompt_len-1
741
 
742
  log_probs_per_token = self._normalize_logits_to_log_probs(logits, use_labels, per_token=True) # (B*N, L)
743
  # Zero out prompt positions so GRPO loss only uses generated tokens
tests/test_thinking_dial_integration.py CHANGED
@@ -212,3 +212,40 @@ def test_s2_train_step_accepts_thinking_depth(setup):
212
  import inspect
213
  sig = inspect.signature(trainer.train_step)
214
  assert 'thinking_depth' in sig.parameters, "train_step should accept thinking_depth"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  import inspect
213
  sig = inspect.signature(trainer.train_step)
214
  assert 'thinking_depth' in sig.parameters, "train_step should accept thinking_depth"
215
+
216
+
217
+ def test_n25_mask_start_correct():
218
+ """N25: mask_start should be prompt_len - 2 (first gen token at index prompt_len-2)."""
219
+ config = FusionConfig(
220
+ vocab_size=1000, hidden_size=256, num_hidden_layers=2,
221
+ num_attention_heads=4, num_key_value_heads=2, intermediate_size=512,
222
+ block_size=8, latent_dim=16, window_size=64,
223
+ )
224
+ base_model = FusionModel(config)
225
+ trainer = GRPOTrainer(base_model)
226
+
227
+ # Test the mask_start calculation: prompt_len=5 -> mask_start=3
228
+ # This means index 0,1,2 (prompt tokens) are zeroed, index 3+ (gen tokens) are kept
229
+ prompt_len = 5
230
+ mask_start = max(prompt_len - 2, 0)
231
+ assert mask_start == 3, f"Expected mask_start=3, got {mask_start}"
232
+
233
+ # Verify: index 3 is first gen token (not zeroed)
234
+ assert mask_start == 3, "First gen token should NOT be masked out"
235
+
236
+
237
+ def test_reward_fn_string_safety():
238
+ """S2 FIX: reward_fn as string should not crash compute_reward."""
239
+ config = FusionConfig(
240
+ vocab_size=1000, hidden_size=256, num_hidden_layers=2,
241
+ num_attention_heads=4, num_key_value_heads=2, intermediate_size=512,
242
+ block_size=8, latent_dim=16, window_size=64,
243
+ )
244
+ base_model = FusionModel(config)
245
+ # Register a test reward function
246
+ GRPOTrainer.register_reward_fn('test_reward', lambda p, r: 1.0)
247
+
248
+ trainer = GRPOTrainer(base_model, reward_fn='test_reward') # Set as string
249
+ # Should not raise TypeError
250
+ reward = trainer.compute_reward('prompt', 'response')
251
+ assert reward == 1.0, "String reward_fn should be looked up from registry"