fix: separate attn_mask and loss_mask in get_logprobs, compose causal masking in strategy
- add loss_mask parameter to get_logprobs to decouple attention from loss masking - DPO/GRPO strategies compose key-padding + causal mask before model forward - prevents prompt tokens from being masked out of attention and missing causal masking
This commit is contained in:
+46
-15
@@ -1,7 +1,7 @@
|
|||||||
"""Training strategy implementations with factory pattern."""
|
"""Training strategy implementations with factory pattern."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Callable, Dict, Union
|
from typing import Callable, Dict, Optional, Union
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
@@ -29,9 +29,10 @@ def move_to_device(batch: Dict[str, Tensor], device: str) -> Dict[str, Tensor]:
|
|||||||
|
|
||||||
|
|
||||||
def get_logprobs(
|
def get_logprobs(
|
||||||
model: Union[nn.Module, Callable[..., Dict[str, Tensor]]],
|
model: nn.Module,
|
||||||
input_ids: Tensor,
|
input_ids: Tensor,
|
||||||
mask: Tensor,
|
attn_mask: Tensor,
|
||||||
|
loss_mask: Tensor,
|
||||||
reduction: str,
|
reduction: str,
|
||||||
) -> Tensor:
|
) -> Tensor:
|
||||||
"""Compute token-wise log probabilities from model outputs.
|
"""Compute token-wise log probabilities from model outputs.
|
||||||
@@ -39,7 +40,8 @@ def get_logprobs(
|
|||||||
Args:
|
Args:
|
||||||
model: The language model
|
model: The language model
|
||||||
input_ids: Input token IDs of shape [batch_size, seq_len]
|
input_ids: Input token IDs of shape [batch_size, seq_len]
|
||||||
mask: Attention mask of shape [batch_size, seq_len]
|
attn_mask: Attention mask passed to the model (may include causal).
|
||||||
|
loss_mask: Per-token mask for loss reduction.
|
||||||
reduction: How to reduce over sequence dimension ("mean", "sum", "none")
|
reduction: How to reduce over sequence dimension ("mean", "sum", "none")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -52,9 +54,12 @@ def get_logprobs(
|
|||||||
)
|
)
|
||||||
|
|
||||||
shifted_input_ids = input_ids[:, 1:]
|
shifted_input_ids = input_ids[:, 1:]
|
||||||
shifted_mask = mask[:, 1:]
|
shifted_loss_mask = loss_mask[:, 1:]
|
||||||
|
|
||||||
logits = model(input_ids[:, :-1], mask[:, :-1])["logits"]
|
logits = model(
|
||||||
|
input_ids[:, :-1],
|
||||||
|
attn_mask[:, :, :-1, :-1] if attn_mask.dim() == 4 else attn_mask[:, :-1],
|
||||||
|
)["logits"]
|
||||||
log_probs = torch.log_softmax(logits.float(), dim=-1)
|
log_probs = torch.log_softmax(logits.float(), dim=-1)
|
||||||
|
|
||||||
token_logprobs = torch.gather(
|
token_logprobs = torch.gather(
|
||||||
@@ -62,13 +67,13 @@ def get_logprobs(
|
|||||||
).squeeze(-1)
|
).squeeze(-1)
|
||||||
|
|
||||||
if reduction == "mean":
|
if reduction == "mean":
|
||||||
return (token_logprobs * shifted_mask).sum(dim=-1) / shifted_mask.sum(
|
return (token_logprobs * shifted_loss_mask).sum(dim=-1) / shifted_loss_mask.sum(
|
||||||
dim=-1
|
dim=-1
|
||||||
).clamp(min=1.0)
|
).clamp(min=1.0)
|
||||||
elif reduction == "sum":
|
elif reduction == "sum":
|
||||||
return (token_logprobs * shifted_mask).sum(dim=-1)
|
return (token_logprobs * shifted_loss_mask).sum(dim=-1)
|
||||||
else:
|
else:
|
||||||
return token_logprobs * shifted_mask
|
return token_logprobs * shifted_loss_mask
|
||||||
|
|
||||||
|
|
||||||
def make_doc_boundary_mask(position_ids: Tensor) -> Tensor:
|
def make_doc_boundary_mask(position_ids: Tensor) -> Tensor:
|
||||||
@@ -289,13 +294,31 @@ class DPOStrategy(BaseStrategy):
|
|||||||
chosen_mask, rejected_mask = batch["chosen_mask"], batch["rejected_mask"]
|
chosen_mask, rejected_mask = batch["chosen_mask"], batch["rejected_mask"]
|
||||||
|
|
||||||
concat_ids = torch.cat([chosen_ids, rejected_ids], dim=0)
|
concat_ids = torch.cat([chosen_ids, rejected_ids], dim=0)
|
||||||
concat_mask = torch.cat([chosen_mask, rejected_mask], dim=0)
|
concat_loss_mask = torch.cat([chosen_mask, rejected_mask], dim=0)
|
||||||
|
|
||||||
log_pi = get_logprobs(self.model, concat_ids, concat_mask, self.reduction)
|
# Build full attention mask: key-padding + causal
|
||||||
|
key_pad = concat_ids.bool()[:, None, None, :] # [B*2, 1, 1, S]
|
||||||
|
S = key_pad.shape[-1]
|
||||||
|
causal = torch.tril(
|
||||||
|
torch.ones(S, S, dtype=torch.bool, device=concat_ids.device)
|
||||||
|
)[None, None, :, :] # [1, 1, S, S]
|
||||||
|
full_mask = key_pad & causal # [B*2, 1, S, S] — composed
|
||||||
|
|
||||||
|
log_pi = get_logprobs(
|
||||||
|
self.model,
|
||||||
|
concat_ids,
|
||||||
|
full_mask,
|
||||||
|
concat_loss_mask,
|
||||||
|
self.reduction,
|
||||||
|
)
|
||||||
|
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
log_ref = get_logprobs(
|
log_ref = get_logprobs(
|
||||||
self.ref_model, concat_ids, concat_mask, self.reduction
|
self.ref_model,
|
||||||
|
concat_ids,
|
||||||
|
full_mask,
|
||||||
|
concat_loss_mask,
|
||||||
|
self.reduction,
|
||||||
)
|
)
|
||||||
|
|
||||||
log_pi_chosen = log_pi[: chosen_ids.shape[0]]
|
log_pi_chosen = log_pi[: chosen_ids.shape[0]]
|
||||||
@@ -399,18 +422,26 @@ class GRPOStrategy(BaseStrategy):
|
|||||||
[torch.zeros_like(prompt_expanded, dtype=torch.bool), masks_flat], dim=-1
|
[torch.zeros_like(prompt_expanded, dtype=torch.bool), masks_flat], dim=-1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Build full attention mask: key-padding + causal
|
||||||
|
key_pad = full_sequences.bool()[:, None, None, :]
|
||||||
|
S = key_pad.shape[-1]
|
||||||
|
causal = torch.tril(
|
||||||
|
torch.ones(S, S, dtype=torch.bool, device=full_sequences.device)
|
||||||
|
)[None, None, :, :]
|
||||||
|
attn_mask = key_pad & causal
|
||||||
|
|
||||||
# get_logprobs returns [B*G, S-1] (S = prompt_len + response_len).
|
# get_logprobs returns [B*G, S-1] (S = prompt_len + response_len).
|
||||||
# Response token logprobs occupy the last ``response_len`` positions
|
# Response token logprobs occupy the last ``response_len`` positions
|
||||||
# (the first response token is predicted from the last prompt token).
|
# (the first response token is predicted from the last prompt token).
|
||||||
token_log_probs_policy = get_logprobs(
|
token_log_probs_policy = get_logprobs(
|
||||||
self.model, full_sequences, full_masks, "none"
|
self.model, full_sequences, attn_mask, full_masks, "none"
|
||||||
)[:, prompt_len - 1 :]
|
)[:, prompt_len - 1 :]
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
token_log_probs_old = get_logprobs(
|
token_log_probs_old = get_logprobs(
|
||||||
self.old_model, full_sequences, full_masks, "none"
|
self.old_model, full_sequences, attn_mask, full_masks, "none"
|
||||||
)[:, prompt_len - 1 :]
|
)[:, prompt_len - 1 :]
|
||||||
token_log_probs_ref = get_logprobs(
|
token_log_probs_ref = get_logprobs(
|
||||||
self.ref_model, full_sequences, full_masks, "none"
|
self.ref_model, full_sequences, attn_mask, full_masks, "none"
|
||||||
)[:, prompt_len - 1 :]
|
)[:, prompt_len - 1 :]
|
||||||
|
|
||||||
# Reshape to [B, G, response_len]
|
# Reshape to [B, G, response_len]
|
||||||
|
|||||||
Reference in New Issue
Block a user