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:
2026-07-21 23:47:28 +08:00
parent 60d7ee614a
commit ef8783b7e3
+46 -15
View File
@@ -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]