Spaces:
Running
Running
zhan1206 commited on
Commit ·
b0bc454
1
Parent(s): 376d8d7
v2.1.0: fix N25 off-by-one, S2 reward_fn string safety
Browse filesN25: 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
| 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-
|
| 735 |
-
|
|
|
|
| 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"
|