DRPO / modeling_sdar.py
Leotsia's picture
Upload DRPO checkpoint-450 (MATH-500 64.4)
f836ab9 verified
Raw
History Blame Contribute Delete
215 kB
# This file is modified based on https://github.com/huggingface/transformers/blob/v4.52.4/src/transformers/models/qwen3/modeling_qwen3.py.
#
# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
# This file was automatically generated from src/transformers/models/qwen3/modular_qwen3.py.
# Do NOT edit this file manually as any edits will be overwritten by the generation of
# the file from the modular. If any change should be done, please apply the change to the
# modular_qwen3.py file directly. One of our CI enforces this.
# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
# coding=utf-8
# Copyright 2025 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Callable, Optional, Tuple, Union, List, TypedDict
import math
import time
import sys
import re
import torch
from torch import nn
from einops import rearrange
from transformers.activations import ACT2FN
from transformers.cache_utils import Cache, DynamicCache, SlidingWindowCache, StaticCache
from transformers.generation import GenerationMixin
from transformers.integrations import use_kernel_forward_from_hub
from transformers.modeling_attn_mask_utils import AttentionMaskConverter
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
from transformers.modeling_layers import GradientCheckpointingLayer
from transformers.modeling_outputs import (
BaseModelOutputWithPast,
CausalLMOutputWithPast,
QuestionAnsweringModelOutput,
SequenceClassifierOutputWithPast,
TokenClassifierOutput,
)
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
from transformers.processing_utils import Unpack
try:
from transformers.utils import LossKwargs
except ImportError:
class LossKwargs(TypedDict, total=False):
pass
from transformers.utils import auto_docstring, can_return_tuple, is_torch_flex_attn_available, logging
# Pre-register submodules to handle hyphenated directory names
import importlib.util as _ilu, os as _os, sys as _sys
_dir = _os.path.dirname(_os.path.abspath(__file__))
for _n in ["configuration_sdar", "fused_linear_diffusion_cross_entropy", "gap_sdar_training"]:
_fqn = f"{__name__.rsplit(chr(46), 1)[0]}.{_n}" if chr(46) in (__name__ or "") else _n
if _fqn not in _sys.modules:
_sp = _ilu.spec_from_file_location(_fqn, _os.path.join(_dir, f"{_n}.py"))
_md = _ilu.module_from_spec(_sp)
_sys.modules[_fqn] = _md
_sp.loader.exec_module(_md)
from .configuration_sdar import SDARConfig
from .fused_linear_diffusion_cross_entropy import FusedLinearDiffusionCrossEntropyLoss
from .gap_sdar_training import (
apply_gap_remask,
build_rollout_scope_mask,
build_rollout_p_mask,
get_num_transfer_tokens,
select_policy_transfer_tokens,
select_teacher_forced_rollout_tokens,
)
try:
from flash_attn.ops.triton.layer_norm import rms_norm_fn as flash_rms_norm
except ImportError:
flash_rms_norm = None
import torch.nn.functional as F
try:
from flash_attn import flash_attn_func, flash_attn_varlen_func
from flash_attn.bert_padding import index_first_axis, pad_input, unpad_input
except:
pass
try:
from liger_kernel.ops.swiglu import LigerSiLUMulFunction # noqa: F401
liger_kernel_is_available = True
except ImportError:
liger_kernel_is_available = False
if is_torch_flex_attn_available():
from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
from transformers.integrations.flex_attention import make_flex_block_causal_mask
logger = logging.get_logger(__name__)
def _gap_env_int(name: str, default: int = 0) -> int:
raw = _os.getenv(name)
if raw is None:
return default
try:
return int(raw.strip())
except Exception:
return default
def _gap_env_flag(name: str, default: bool = False) -> bool:
raw = _os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
def _gap_is_rank0() -> bool:
if not torch.distributed.is_available() or not torch.distributed.is_initialized():
return True
try:
return torch.distributed.get_rank() == 0
except Exception:
return True
def _gap_debug_enabled() -> bool:
return _gap_env_flag("SDAR_GAP_COARSE_MARKERS", False) and _gap_is_rank0()
def _gap_stderr(message: str) -> None:
try:
_os.write(2, (message + "\n").encode("utf-8", errors="replace"))
except Exception:
pass
def modify_padded_position_ids_2d(position_ids: torch.LongTensor) -> torch.LongTensor:
"""
使用完全向量化的 PyTorch 操作修改一个 batch 的 packed position_ids。
这个函数假设输入是一个 2D Tensor,形状为 (batch_size, sequence_length)。
它会独立地处理 batch 中的每一行。
Args:
position_ids: 二维 PyTorch Tensor, shape (batch_size, sequence_length).
Returns:
修改后的 position_ids Tensor, shape (batch_size, sequence_length).
"""
if position_ids.dim() != 2:
raise ValueError(f"Input tensor must be 2D, but got {position_ids.dim()} dimensions.")
batch_size, seq_len = position_ids.shape
device = position_ids.device
col_indices = torch.arange(seq_len, device=device, dtype=position_ids.dtype).expand(batch_size, -1)
mask = (position_ids != 0)
masked_indices = col_indices * mask
last_nonzero_idx = torch.max(masked_indices, dim=1).values
has_nonzero = torch.any(mask, dim=1)
pad_start_idx = torch.where(has_nonzero, last_nonzero_idx + 1, torch.tensor(0, device=device, dtype=position_ids.dtype))
padding_mask = col_indices >= pad_start_idx.unsqueeze(1)
new_pad_values = col_indices - pad_start_idx.unsqueeze(1)
position_ids = torch.where(padding_mask, new_pad_values, position_ids)
return position_ids
def calculate_token_nums(position_ids: torch.Tensor):
"""
使用 PyTorch 高效计算一个批次中每个打包序列的长度。
Args:
position_ids (torch.Tensor): 一个 2D Tensor,形状为 (batch_size, sequence_length)。
例如:tensor([[0,1,2,3,4,0,1,2,3,4,5,0,1,2,3,0,0,0]])
Returns:
list[list[int]]: 一个嵌套列表,包含每个批次项中各个序列的长度。
例如:[[5, 6, 4, 1, 1, 1]]
"""
# 检查输入是否为 2D Tensor
if position_ids.dim() != 2:
raise ValueError(f"输入必须是 2D Tensor,但得到了 {position_ids.dim()}D")
all_lengths = []
# 我们按批次逐行处理。因为每行的序列长度数量不同(ragged),
# 所以 Python 循环在批次维度上是最高效且最清晰的写法。
# 循环内部的操作是完全向量化的。
for pids_row in position_ids:
# 获取当前行的总长度
seq_len = pids_row.shape[0]
# 1. 找到所有值为 0 的元素的索引
# pids_row == 0 会返回一个布尔 Tensor: [True, False, ..., True, ...]
# torch.nonzero 会返回这些 True 值的索引
# .flatten() 将其从 (N, 1) 形状的 Tensor 变为 (N,) 形状
zero_indices = torch.nonzero(pids_row == 0).flatten()
# 2. 将序列的总长度作为一个额外的切分点添加到末尾
# 这对于计算最后一个序列的长度至关重要
# 注意:要确保新创建的 tensor 和原始 tensor 在同一个设备上 (cpu/cuda)
split_points = torch.cat([
zero_indices,
torch.tensor([seq_len], device=pids_row.device, dtype=zero_indices.dtype)
])
# 3. 计算相邻切分点之间的差值,这就是我们想要的长度
# torch.diff([a, b, c, d]) 会返回 [b-a, c-b, d-c]
lengths = torch.diff(split_points)
all_lengths.append(lengths)
return all_lengths
def forward_add_noise_packed(
inputs_ids: torch.Tensor,
num_tokens_list: List[torch.Tensor],
prompt_mask: torch.Tensor,
mask_id: int,
eps: float = 1e-3,
max_tries: int = 10,
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""
为一批打包(packed)序列的 token ID 添加噪声。
此函数保留了为每个逻辑样本(在每个批次项内拼接)生成独立随机噪声率的逻辑。
它会随机将一部分 token 的 ID 替换为 mask_id。
这个过程会避开被 prompt_mask 标记的位置。
Args:
inputs_ids (torch.Tensor):
输入的 token ID 张量,形状为 (bsz, total_tokens)。
num_tokens_list (List[torch.Tensor]):
一个张量列表,长度为 bsz。列表中的每个张量记录了对应批次项中
每个逻辑样本的长度。例如: [tensor([len1, len2]), tensor([len3, len4, len5])].
prompt_mask (torch.Tensor):
布尔型张量,形状为 (bsz, total_tokens),值为 True 的位置表示是 prompt,
不应添加噪声。
mask_id (int):
用于替换的 mask token 的 ID。
eps (float):
微小值,用于防止噪声率 t 恰好为 0,确保 p_mask > 0。
max_tries (int):
为确保至少一个非 prompt token 被 mask,对每个批次项尝试的最大次数。
Returns:
Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
- noisy_input_ids (torch.Tensor):
添加噪声后的 token ID 张量,形状为 (bsz, total_tokens)。
- final_masked_indices (torch.Tensor):
布尔型张量,标记了哪些位置被实际 mask 了,形状为 (bsz, total_tokens)。
- p_masks (torch.Tensor):
一个一维张量,包含了被 mask 的 token 对应的实际噪声率。
"""
# 1. 验证和获取形状
bsz, total_tokens = inputs_ids.shape
device = inputs_ids.device
# 检查输入的一致性
assert len(num_tokens_list) == bsz, f"num_tokens_list 的长度 ({len(num_tokens_list)}) 必须等于 bsz ({bsz})"
assert prompt_mask.shape == (bsz, total_tokens), f"prompt_mask 形状不匹配, 期望 {(bsz, total_tokens)}, 得到 {prompt_mask.shape}"
# 准备结果容器
noisy_ids_list = []
final_masked_indices_list = []
p_masks_per_token_list = []
# 2. 在批次维度上迭代
# 这是处理不同打包结构最直接有效的方法
for i in range(bsz):
# 提取当前批次项的数据
current_ids = inputs_ids[i:i+1] # shape: (1, total_tokens)
current_num_tokens = num_tokens_list[i]
current_prompt_mask = prompt_mask[i:i+1] # shape: (1, total_tokens)
num_samples_in_item = len(current_num_tokens)
# 验证当前批次项的 token 总数是否匹配
assert total_tokens == torch.sum(current_num_tokens), \
f"批次项 {i} 的 num_tokens 之和 ({torch.sum(current_num_tokens)}) 与 total_tokens ({total_tokens}) 不匹配"
eligible_for_masking = ~current_prompt_mask
# 如果没有任何 token 可以被 mask,直接使用原始输入,并设置 p_mask 为 eps
if not eligible_for_masking.any():
noisy_ids_list.append(current_ids)
final_masked_indices_list.append(torch.zeros_like(current_prompt_mask, dtype=torch.bool))
# p_mask_per_token 的形状应为 (1, total_tokens) 以便后续拼接
p_masks_per_token_list.append(torch.full((1, total_tokens), eps, device=device, dtype=torch.float))
continue
# --- 尝试生成 mask,确保至少 mask 一个 token ---
final_masked_indices_item = torch.zeros_like(current_prompt_mask, dtype=torch.bool)
p_mask_per_token = None
for _ in range(max_tries):
# 为每个逻辑样本生成一个独立的噪声率 t
t = torch.rand(num_samples_in_item, device=device)
p_mask_per_sample = (1 - eps) * t + eps
# 将每个样本的噪声率扩展到其所有 token 上
p_mask_per_token_1d = torch.repeat_interleave(p_mask_per_sample, current_num_tokens)
p_mask_per_token = p_mask_per_token_1d.unsqueeze(0) # shape: (1, total_tokens)
# 根据噪声率生成随机 mask
masked_indices = torch.rand_like(p_mask_per_token) < p_mask_per_token
# 应用 prompt mask,确保 prompt 不被 mask
final_masked_indices_item = masked_indices & eligible_for_masking
# 如果成功 mask 了至少一个 token,则跳出尝试循环
if final_masked_indices_item.any():
break
# 如果 max_tries 之后仍然没有 mask 任何 token (极小概率),就强制 mask 一个可 mask 的 token
if not final_masked_indices_item.any():
eligible_indices = torch.nonzero(eligible_for_masking.squeeze(0), as_tuple=True)[0]
if len(eligible_indices) > 0:
# 随机选择一个可 mask 的位置
random_choice = torch.randint(0, len(eligible_indices), (1,)).item()
force_mask_idx = eligible_indices[random_choice]
final_masked_indices_item[0, force_mask_idx] = True
# --- 根据最终的 mask 生成带噪声的 IDs ---
noisy_ids_item = torch.where(
final_masked_indices_item,
mask_id,
current_ids
)
# 保存这个批次项的结果
noisy_ids_list.append(noisy_ids_item)
final_masked_indices_list.append(final_masked_indices_item)
p_masks_per_token_list.append(p_mask_per_token)
# 3. 将列表中的结果堆叠成最终的批处理张量
noisy_input_ids = torch.cat(noisy_ids_list, dim=0)
final_masked_indices = torch.cat(final_masked_indices_list, dim=0)
p_mask_full = torch.cat(p_masks_per_token_list, dim=0)
# 4. 提取被 mask 位置对应的噪声率
p_masks = p_mask_full[final_masked_indices]
return noisy_input_ids, final_masked_indices, p_masks
def block_diff_mask(b, h, q_idx, kv_idx, block_size=None, n=None):
"""
Constructs the specialized block diffusion attention mask for training
composed of three masks:
- **Block Diagonal Mask (M_BD)**: Self-attention within noised blocks
- **Offset Block Causal Mask (M_OBC)**: Cross-attention for conditional context
- **Block Causal Mask (M_BC)**: Attention to update x0
Args:
b, h: Batch and head indices (ignored for mask logic).
q_idx, kv_idx: Query and Key indices.
seq_len: Total sequence length.
block_size: Defines the block structure.
Returns:
A boolean attention mask.
"""
# Indicate whether token belongs to xt or x0
x0_flag_q = q_idx >= n
x0_flag_kv = kv_idx >= n
# Compute block indices
block_q = torch.where(
x0_flag_q == 1, (q_idx - n) // block_size, q_idx // block_size
)
block_kv = torch.where(
x0_flag_kv == 1, (kv_idx - n) // block_size, kv_idx // block_size
)
# **1. Block Diagonal Mask (M_BD) **
block_diagonal = (block_q == block_kv) & (x0_flag_q == x0_flag_kv)
# **2. Offset Block-Causal Mask (M_OBC) **
offset_block_causal = (block_q > block_kv) & (
x0_flag_kv == 1) & (x0_flag_q == 0)
# **3. Block-Causal Mask (M_BC) **
block_causal = (block_q >= block_kv) & (x0_flag_kv == 1) & (x0_flag_q == 1)
# **4. Combine Masks **
return block_diagonal | offset_block_causal | block_causal
def block_attn_mask(num_tokens, block_size, device):
masks = []
for i in range(len(num_tokens)):
cur_masks = []
for num in num_tokens[i]:
# 全部返回 n*n 而非 2n*2n
single_mask = block_diff_mask(
b=None,
h=None,
q_idx=torch.arange(num * 2, device=device)[:, None],
kv_idx=torch.arange(num * 2, device=device)[None, :],
block_size=block_size,
n=num,
)
cur_masks.append(single_mask)
masks.append(torch.block_diag(*cur_masks))
masks = torch.stack(masks, dim=0)
return masks
# @torch.compile(fullgraph=True, mode="max-autotune-no-cudagraphs") # Commented out to prevent Dynamo compile errors with Tensor masks
def fused_flex_attention(query, key, value, attention_mask, **kwargs):
return flex_attention(query, key, value, block_mask=attention_mask, **kwargs)
@use_kernel_forward_from_hub("RMSNorm")
class SDARRMSNorm(nn.Module):
def __init__(self, hidden_size, eps=1e-6):
"""
SDARRMSNorm is equivalent to T5LayerNorm
"""
super().__init__()
self.weight = nn.Parameter(torch.ones(hidden_size))
self.variance_epsilon = eps
def forward(self, hidden_states):
input_dtype = hidden_states.dtype
hidden_states = hidden_states.to(torch.float32)
variance = hidden_states.pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * \
torch.rsqrt(variance + self.variance_epsilon)
hidden_states = hidden_states.to(input_dtype)
if flash_rms_norm is not None:
return flash_rms_norm(
hidden_states, weight=self.weight, bias=None, eps=self.variance_epsilon
)
return self.weight * hidden_states
def extra_repr(self):
return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"
class SDARMLP(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.hidden_size = config.hidden_size
self.intermediate_size = config.intermediate_size
self.gate_proj = nn.Linear(
self.hidden_size, self.intermediate_size, bias=False)
self.up_proj = nn.Linear(
self.hidden_size, self.intermediate_size, bias=False)
self.down_proj = nn.Linear(
self.intermediate_size, self.hidden_size, bias=False)
self.act_fn = ACT2FN[config.hidden_act]
def forward(self, x):
if liger_kernel_is_available:
return self.down_proj(LigerSiLUMulFunction.apply(self.gate_proj(x), self.up_proj(x)))
else:
down_proj = self.down_proj(self.act_fn(
self.gate_proj(x)) * self.up_proj(x))
return down_proj
def rotate_half(x):
"""Rotates half the hidden dims of the input."""
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2:]
return torch.cat((-x2, x1), dim=-1)
def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
"""Applies Rotary Position Embedding to the query and key tensors.
Args:
q (`torch.Tensor`): The query tensor.
k (`torch.Tensor`): The key tensor.
cos (`torch.Tensor`): The cosine part of the rotary embedding.
sin (`torch.Tensor`): The sine part of the rotary embedding.
position_ids (`torch.Tensor`, *optional*):
Deprecated and unused.
unsqueeze_dim (`int`, *optional*, defaults to 1):
The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and
sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note
that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and
k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes
cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have
the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2.
Returns:
`tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding.
"""
cos = cos.unsqueeze(unsqueeze_dim)
sin = sin.unsqueeze(unsqueeze_dim)
q_embed = (q * cos) + (rotate_half(q) * sin)
k_embed = (k * cos) + (rotate_half(k) * sin)
return q_embed, k_embed
def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
"""
This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch,
num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim)
"""
batch, num_key_value_heads, slen, head_dim = hidden_states.shape
if n_rep == 1:
return hidden_states
hidden_states = hidden_states[:, :, None, :, :].expand(
batch, num_key_value_heads, n_rep, slen, head_dim)
return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
def eager_attention_forward(
module: nn.Module,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attention_mask: Optional[torch.Tensor],
scaling: float,
dropout: float = 0.0,
**kwargs,
):
key_states = repeat_kv(key, module.num_key_value_groups)
value_states = repeat_kv(value, module.num_key_value_groups)
attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
if attention_mask is not None:
causal_mask = attention_mask[:, :, :, : key_states.shape[-2]]
attn_weights = attn_weights + causal_mask
attn_weights = nn.functional.softmax(
attn_weights, dim=-1, dtype=torch.float32).to(query.dtype)
attn_weights = nn.functional.dropout(
attn_weights, p=dropout, training=module.training)
attn_output = torch.matmul(attn_weights, value_states)
attn_output = attn_output.transpose(1, 2).contiguous()
return attn_output, attn_weights
class SDARAttention(nn.Module):
"""Multi-headed attention from 'Attention Is All You Need' paper"""
def __init__(self, config: SDARConfig, layer_idx: int):
super().__init__()
self.config = config
self.layer_idx = layer_idx
self.head_dim = getattr(
config, "head_dim", config.hidden_size // config.num_attention_heads)
self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads
self.scaling = self.head_dim**-0.5
self.attention_dropout = config.attention_dropout
self.is_causal = True
self.hidden_size = config.hidden_size
self.num_attention_heads = config.num_attention_heads
self.num_key_value_heads = config.num_key_value_heads
self.q_proj = nn.Linear(
config.hidden_size, config.num_attention_heads * self.head_dim, bias=config.attention_bias
)
self.k_proj = nn.Linear(
config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias
)
self.v_proj = nn.Linear(
config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias
)
self.o_proj = nn.Linear(
config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias
)
# unlike olmo, only on the head dim!
self.q_norm = SDARRMSNorm(self.head_dim, eps=config.rms_norm_eps)
# thus post q_norm does not need reshape
self.k_norm = SDARRMSNorm(self.head_dim, eps=config.rms_norm_eps)
self.sliding_window = config.sliding_window
if not (
self.config.use_sliding_window
and getattr(self.config, "sliding_window", None) is not None
and self.layer_idx >= self.config.max_window_layers
):
self.sliding_window = None
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: Tuple[torch.Tensor, torch.Tensor],
attention_mask: Optional[torch.Tensor],
past_key_value: Optional[Cache] = None,
cache_position: Optional[torch.LongTensor] = None,
**kwargs: Unpack[FlashAttentionKwargs],
) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:
input_shape = hidden_states.shape[:-1]
bsz, q_len = input_shape
hidden_shape = (*input_shape, -1, self.head_dim)
query_states = self.q_norm(self.q_proj(
hidden_states).view(hidden_shape)).transpose(1, 2)
key_states = self.k_norm(self.k_proj(
hidden_states).view(hidden_shape)).transpose(1, 2)
value_states = self.v_proj(hidden_states).view(
hidden_shape).transpose(1, 2)
query_states = query_states.to(value_states.dtype)
key_states = key_states.to(value_states.dtype)
cos, sin = position_embeddings
query_states, key_states = apply_rotary_pos_emb(
query_states, key_states, cos, sin)
query_states = query_states.to(value_states.dtype)
key_states = key_states.to(value_states.dtype)
if past_key_value is not None and kwargs.get("store_kv", False):
# sin and cos are specific to RoPE models; cache_position needed for the static cache
key_states, value_states = past_key_value.update(
key_states, value_states, self.layer_idx)
elif past_key_value is not None and not kwargs.get("store_kv", False) and len(past_key_value) > self.layer_idx:
# only retrive, do not store kv
past_key_states, past_value_states = past_key_value[self.layer_idx]
key_states = torch.cat(
[past_key_states, key_states], dim=-2)
value_states = torch.cat(
[past_value_states, value_states], dim=-2)
if self.training:
if isinstance(attention_mask, torch.Tensor) or attention_mask is None:
attn_output = torch.nn.functional.scaled_dot_product_attention(
query_states, key_states, value_states, attn_mask=attention_mask, dropout_p=0.0, is_causal=(attention_mask is None)
)
attn_weights = None
else:
attn_output, attn_weights = fused_flex_attention(
query=query_states,
key=key_states,
value=value_states,
attention_mask=attention_mask,
enable_gqa=True,
scale=self.scaling,
return_lse=True
)
attn_weights = attn_weights.to(
value_states.dtype) if attn_weights is not None else None
attn_output = rearrange(attn_output, 'b h l d -> b l (h d)')
else:
attention_mask = attention_mask.bool() if attention_mask is not None else None
attn_weights = None
if torch.all(attention_mask): # decoding
query_states = query_states.transpose(1, 2)
key_states = key_states.transpose(1, 2)
value_states = value_states.transpose(1, 2)
attn_output = flash_attn_func(
query_states,
key_states,
value_states,
causal=False,
softmax_scale=self.scaling
)
attn_output = rearrange(attn_output, 'b l h d -> b l (h d)')
else: # prefilling
attn_output = F.scaled_dot_product_attention(
query=query_states,
key=key_states,
value=value_states,
attn_mask=attention_mask,
is_causal=False,
scale=self.scaling,
enable_gqa=True
)
attn_output = rearrange(attn_output, 'b h l d -> b l (h d)')
attn_output = self.o_proj(attn_output)
return attn_output, attn_weights # , attn_weights
class SDARDecoderLayer(GradientCheckpointingLayer):
def __init__(self, config: SDARConfig, layer_idx: int):
super().__init__()
self.hidden_size = config.hidden_size
self.self_attn = SDARAttention(config=config, layer_idx=layer_idx)
self.mlp = SDARMLP(config)
self.input_layernorm = SDARRMSNorm(
config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = SDARRMSNorm(
config.hidden_size, eps=config.rms_norm_eps)
if (
config.sliding_window and config._attn_implementation != "flash_attention_2"
): # diff with Llama is this warning
logger.warning_once(
f"Sliding Window Attention is enabled but not implemented for `{config._attn_implementation}`; "
"unexpected results may be encountered."
)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_value: Optional[Cache] = None,
output_attentions: Optional[bool] = False,
use_cache: Optional[bool] = False,
store_kv: Optional[bool] = False,
cache_position: Optional[torch.LongTensor] = None,
# necessary, but kept here for BC
position_embeddings: Optional[Tuple[torch.Tensor,
torch.Tensor]] = None,
**kwargs: Unpack[FlashAttentionKwargs],
) -> Tuple[torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]]]:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
# Self Attention
hidden_states, self_attn_weights = self.self_attn(
hidden_states=hidden_states,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_value=past_key_value,
output_attentions=output_attentions,
use_cache=use_cache,
store_kv=store_kv,
cache_position=cache_position,
position_embeddings=position_embeddings,
**kwargs,
)
hidden_states = residual + hidden_states
# Fully Connected
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states
outputs = (hidden_states,)
if output_attentions:
outputs += (self_attn_weights,)
return outputs
@auto_docstring
class SDARPreTrainedModel(PreTrainedModel):
config_class = SDARConfig
base_model_prefix = "model"
supports_gradient_checkpointing = True
_no_split_modules = ["SDARDecoderLayer"]
_skip_keys_device_placement = ["past_key_values"]
_supports_flash_attn_2 = True
_supports_sdpa = True
_supports_flex_attn = True
_supports_cache_class = True
_supports_quantized_cache = True
_supports_static_cache = True
_supports_attention_backend = True
def _init_weights(self, module):
std = self.config.initializer_range
if isinstance(module, nn.Linear):
module.weight.data.normal_(mean=0.0, std=std)
if module.bias is not None:
module.bias.data.zero_()
elif isinstance(module, nn.Embedding):
module.weight.data.normal_(mean=0.0, std=std)
if module.padding_idx is not None:
module.weight.data[module.padding_idx].zero_()
elif isinstance(module, SDARRMSNorm):
module.weight.data.fill_(1.0)
class SDARRotaryEmbedding(nn.Module):
def __init__(self, config: SDARConfig, device=None):
super().__init__()
# BC: "rope_type" was originally "type"
if hasattr(config, "rope_scaling") and config.rope_scaling is not None:
self.rope_type = config.rope_scaling.get(
"rope_type", config.rope_scaling.get("type"))
else:
self.rope_type = "default"
self.max_seq_len_cached = config.max_position_embeddings
self.original_max_seq_len = config.max_position_embeddings
self.config = config
self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
inv_freq, self.attention_scaling = self.rope_init_fn(
self.config, device)
self.register_buffer("inv_freq", inv_freq, persistent=False)
self.original_inv_freq = self.inv_freq
@torch.no_grad()
# power user: used with advanced RoPE types (e.g. dynamic rope)
@dynamic_rope_update
def forward(self, x, position_ids):
inv_freq_expanded = self.inv_freq[None, :, None].float().expand(
position_ids.shape[0], -1, 1).to(x.device)
position_ids_expanded = position_ids[:, None, :].float()
device_type = x.device.type if isinstance(
x.device.type, str) and x.device.type != "mps" else "cpu"
with torch.autocast(device_type=device_type, enabled=False): # Force float32
freqs = (inv_freq_expanded.float() @
position_ids_expanded.float()).transpose(1, 2)
emb = torch.cat((freqs, freqs), dim=-1)
cos = emb.cos() * self.attention_scaling
sin = emb.sin() * self.attention_scaling
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
@auto_docstring
class SDARModel(SDARPreTrainedModel):
def __init__(self, config: SDARConfig):
super().__init__(config)
self.padding_idx = config.pad_token_id
self.vocab_size = config.vocab_size
self.embed_tokens = nn.Embedding(
config.vocab_size, config.hidden_size, self.padding_idx)
self.layers = nn.ModuleList(
[SDARDecoderLayer(config, layer_idx)
for layer_idx in range(config.num_hidden_layers)]
)
self.norm = SDARRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.rotary_emb = SDARRotaryEmbedding(config=config)
self.gradient_checkpointing = False
# Initialize weights and apply final processing
self.post_init()
def get_input_embeddings(self):
return self.embed_tokens
def set_input_embeddings(self, value):
self.embed_tokens = value
@can_return_tuple
@auto_docstring
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Cache] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
use_cache: Optional[bool] = None,
store_kv: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
**flash_attn_kwargs: Unpack[FlashAttentionKwargs],
) -> BaseModelOutputWithPast:
r"""
store_kv (`bool`, *optional*):
Whether to keep KV states in the custom SDAR cache path during generation/inference.
"""
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
use_cache = use_cache if use_cache is not None else self.config.use_cache
if (input_ids is None) ^ (inputs_embeds is not None):
raise ValueError(
"You must specify exactly one of input_ids or inputs_embeds")
if self.gradient_checkpointing and self.training and use_cache:
logger.warning_once(
"`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`."
)
use_cache = False
# TODO (joao): remove this exception in v4.56 -- it exists for users that try to pass a legacy cache
if not isinstance(past_key_values, (type(None), Cache)):
raise ValueError(
"The `past_key_values` should be either a `Cache` object or `None`.")
if inputs_embeds is None:
inputs_embeds = self.embed_tokens(input_ids)
if use_cache and past_key_values is None:
past_key_values = DynamicCache()
if cache_position is None:
past_seen_tokens = past_key_values.get_seq_length(
) if past_key_values is not None else 0
cache_position = torch.arange(
past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
)
if position_ids is None:
position_ids = cache_position.unsqueeze(0)
attention_mask = self._update_causal_mask(
attention_mask, inputs_embeds, cache_position, past_key_values, output_attentions
)
hidden_states = inputs_embeds
# create position embeddings to be shared across the decoder layers
position_embeddings = self.rotary_emb(hidden_states, position_ids)
# decoder layers
all_hidden_states = () if output_hidden_states else None
all_self_attns = () if output_attentions else None
for decoder_layer in self.layers[: self.config.num_hidden_layers]:
if output_hidden_states:
all_hidden_states += (hidden_states,)
layer_outputs = decoder_layer(
hidden_states,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_value=past_key_values,
output_attentions=output_attentions,
use_cache=use_cache,
store_kv=store_kv,
cache_position=cache_position,
position_embeddings=position_embeddings,
**flash_attn_kwargs,
)
hidden_states = layer_outputs[0]
if output_attentions:
all_self_attns += (layer_outputs[1],)
hidden_states = self.norm(hidden_states)
# add hidden states from the last decoder layer
if output_hidden_states:
all_hidden_states += (hidden_states,)
return BaseModelOutputWithPast(
last_hidden_state=hidden_states,
past_key_values=past_key_values if use_cache else None,
hidden_states=all_hidden_states,
attentions=all_self_attns,
)
def _update_causal_mask(
self,
attention_mask: Union[torch.Tensor, "BlockMask"],
input_tensor: torch.Tensor,
cache_position: torch.Tensor,
past_key_values: Cache,
output_attentions: bool = False,
):
# Training can pass a precomputed flex-attention BlockMask even when the
# Transformers-selected backend is not "flex_attention". In that case the
# mask should bypass tensor-only causal-mask preparation entirely.
if attention_mask is not None and not isinstance(attention_mask, torch.Tensor):
assert isinstance(attention_mask, BlockMask)
return attention_mask
if self.config._attn_implementation == "flash_attention_2":
if attention_mask is not None and past_key_values is not None:
is_padding_right = attention_mask[:, -
1].sum().item() != input_tensor.size()[0]
if is_padding_right:
raise ValueError(
"You are attempting to perform batched generation with padding_side='right'"
" this may lead to unexpected behaviour for Flash Attention version of Qwen3. Make sure to "
" call `tokenizer.padding_side = 'left'` before tokenizing the input. "
)
if attention_mask is not None and 0.0 in attention_mask:
return attention_mask
return None
if self.config._attn_implementation == "flex_attention":
if isinstance(attention_mask, torch.Tensor):
seq_len_q, seq_len_kv = attention_mask.shape
assert seq_len_q == seq_len_kv, f"got {attention_mask.shape=}"
attention_mask = create_block_mask(
# 2d bool tensor, shape: [2*seqlen, 2*seqlen]
lambda b, h, q_idx, kv_idx: attention_mask[q_idx, kv_idx],
B=None, H=None, Q_LEN=seq_len_q, KV_LEN=seq_len_kv,
)
else:
# Here we pass in flex mask computed externally
assert isinstance(attention_mask, BlockMask)
return attention_mask
# For SDPA, when possible, we will rely on its `is_causal` argument instead of its `attn_mask` argument, in
# order to dispatch on Flash Attention 2. This feature is not compatible with static cache, as SDPA will fail
# to infer the attention mask.
past_seen_tokens = past_key_values.get_seq_length(
) if past_key_values is not None else 0
using_static_cache = isinstance(past_key_values, StaticCache)
using_sliding_window_cache = isinstance(
past_key_values, SlidingWindowCache)
# When output attentions is True, sdpa implementation's forward method calls the eager implementation's forward
if (
self.config._attn_implementation == "sdpa"
and not (using_static_cache or using_sliding_window_cache)
and not output_attentions
):
if AttentionMaskConverter._ignore_causal_mask_sdpa(
attention_mask,
inputs_embeds=input_tensor,
past_key_values_length=past_seen_tokens,
sliding_window=self.config.sliding_window,
is_training=self.training,
):
return None
dtype = input_tensor.dtype
min_dtype = torch.finfo(dtype).min
sequence_length = input_tensor.shape[1]
# SlidingWindowCache or StaticCache
if using_sliding_window_cache or using_static_cache:
target_length = past_key_values.get_max_cache_shape()
# DynamicCache or no cache
else:
target_length = (
attention_mask.shape[-1]
if isinstance(attention_mask, torch.Tensor)
else past_seen_tokens + sequence_length + 1
)
# In case the provided `attention` mask is 2D, we generate a causal mask here (4D).
causal_mask = self._prepare_4d_causal_attention_mask_with_cache_position(
attention_mask,
sequence_length=sequence_length,
target_length=target_length,
dtype=dtype,
cache_position=cache_position,
batch_size=input_tensor.shape[0],
config=self.config,
past_key_values=past_key_values,
)
if (
self.config._attn_implementation == "sdpa"
and attention_mask is not None
and attention_mask.device.type in ["cuda", "xpu", "npu"]
and not output_attentions
):
# Attend to all tokens in fully masked rows in the causal_mask, for example the relevant first rows when
# using left padding. This is required by F.scaled_dot_product_attention memory-efficient attention path.
# Details: https://github.com/pytorch/pytorch/issues/110213
causal_mask = AttentionMaskConverter._unmask_unattended(
causal_mask, min_dtype)
return causal_mask
@staticmethod
def _prepare_4d_causal_attention_mask_with_cache_position(
attention_mask: torch.Tensor,
sequence_length: int,
target_length: int,
dtype: torch.dtype,
cache_position: torch.Tensor,
batch_size: int,
config: SDARConfig,
past_key_values: Cache,
):
"""
Creates a causal 4D mask of shape `(batch_size, 1, query_length, key_value_length)` from a 2D mask of shape
`(batch_size, key_value_length)`, or if the input `attention_mask` is already 4D, do nothing.
Args:
attention_mask (`torch.Tensor`):
A 2D attention mask of shape `(batch_size, key_value_length)` or a 4D attention mask of shape `(batch_size, 1, query_length, key_value_length)`.
sequence_length (`int`):
The sequence length being processed.
target_length (`int`):
The target length: when generating with static cache, the mask should be as long as the static cache, to account for the 0 padding, the part of the cache that is not filled yet.
dtype (`torch.dtype`):
The dtype to use for the 4D attention mask.
cache_position (`torch.Tensor`):
Indices depicting the position of the input sequence tokens in the sequence.
batch_size (`torch.Tensor`):
Batch size.
config (`SDARConfig`):
The model's configuration class
past_key_values (`Cache`):
The cache class that is being used currently to generate
"""
if attention_mask is not None and attention_mask.dim() == 4:
# In this case we assume that the mask comes already in inverted form and requires no inversion or slicing.
causal_mask = attention_mask
else:
min_dtype = torch.finfo(dtype).min
causal_mask = torch.full(
(sequence_length, target_length), fill_value=min_dtype, dtype=dtype, device=cache_position.device
)
diagonal_attend_mask = torch.arange(target_length, device=cache_position.device) > cache_position.reshape(
-1, 1
)
text_config = config.get_text_config()
if getattr(text_config, "use_sliding_window", True) and text_config.sliding_window is not None:
# if we have sliding window, we should not attend to tokens beyond sliding window length, so we mask them out also
# the check is needed to verify is current checkpoint was trained with sliding window or not
if not isinstance(past_key_values, SlidingWindowCache) or sequence_length > target_length:
sliding_attend_mask = torch.arange(target_length, device=cache_position.device) <= (
cache_position.reshape(-1, 1) -
text_config.sliding_window
)
diagonal_attend_mask.bitwise_or_(sliding_attend_mask)
causal_mask *= diagonal_attend_mask
causal_mask = causal_mask[None, None,
:, :].expand(batch_size, 1, -1, -1)
if attention_mask is not None:
causal_mask = causal_mask.clone() # copy to contiguous memory for in-place edit
if attention_mask.shape[-1] > target_length:
attention_mask = attention_mask[:, :target_length]
mask_length = attention_mask.shape[-1]
padding_mask = causal_mask[:, :, :, :mask_length] + attention_mask[:, None, None, :].to(
causal_mask.device
)
padding_mask = padding_mask == 0
causal_mask[:, :, :, :mask_length] = causal_mask[:, :, :, :mask_length].masked_fill(
padding_mask, min_dtype
)
return causal_mask
class KwargsForCausalLM(FlashAttentionKwargs, LossKwargs):
...
class GapRemaskHead(nn.Module):
def __init__(self, hidden_size: int, confidence_feature_dim: int = 4):
super().__init__()
self.up_proj = nn.Linear(hidden_size, hidden_size, bias=True)
self.conf_proj = nn.Linear(confidence_feature_dim, hidden_size, bias=True)
self.down_proj = nn.Linear(hidden_size, 1, bias=True)
self.base_scale = nn.Parameter(torch.tensor(1.0))
self.residual_scale = nn.Parameter(torch.tensor(1.0))
@staticmethod
def _score_to_logit(score: torch.Tensor) -> torch.Tensor:
score = score.clamp(min=1e-4, max=1.0 - 1e-4)
return torch.logit(score)
@staticmethod
def build_confidence_features(lm_logits: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
probs = lm_logits.softmax(dim=-1)
top2 = torch.topk(probs, k=2, dim=-1).values
top1_prob = top2[..., 0]
top2_prob = top2[..., 1]
low_conf = 1.0 - top1_prob
margin = top1_prob - top2_prob
entropy = -(probs * probs.clamp_min(1e-8).log()).sum(dim=-1)
entropy = entropy / math.log(lm_logits.shape[-1])
features = torch.stack((top1_prob, low_conf, margin, entropy), dim=-1)
return features, low_conf
def forward(self, hidden_states: torch.Tensor, lm_logits: Optional[torch.Tensor] = None) -> torch.Tensor:
residual_hidden = self.up_proj(hidden_states)
if lm_logits is not None:
conf_features, low_conf = self.build_confidence_features(lm_logits.float())
residual_hidden = residual_hidden + self.conf_proj(conf_features.to(hidden_states.dtype))
base_logit = self._score_to_logit(low_conf)
else:
base_logit = hidden_states.new_zeros(hidden_states.shape[:-1])
residual_logit = self.down_proj(F.gelu(residual_hidden)).squeeze(-1)
return self.base_scale * base_logit + self.residual_scale * residual_logit
def _load_from_state_dict(
self,
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
):
legacy_key_map = {
f"{prefix}0.weight": f"{prefix}up_proj.weight",
f"{prefix}0.bias": f"{prefix}up_proj.bias",
f"{prefix}2.weight": f"{prefix}down_proj.weight",
f"{prefix}2.bias": f"{prefix}down_proj.bias",
}
for legacy_key, new_key in legacy_key_map.items():
if legacy_key in state_dict and new_key not in state_dict:
state_dict[new_key] = state_dict.pop(legacy_key)
super()._load_from_state_dict(
state_dict,
prefix,
local_metadata,
strict,
missing_keys,
unexpected_keys,
error_msgs,
)
@auto_docstring
class SDARForCausalLM(SDARPreTrainedModel, GenerationMixin):
_tied_weights_keys = ["lm_head.weight"]
_tp_plan = {"lm_head": "colwise_rep"}
_pp_plan = {"lm_head": (["hidden_states"], ["logits"])}
def __init__(self, config):
super().__init__(config)
self.model = SDARModel(config)
self.vocab_size = config.vocab_size
self.lm_head = nn.Linear(
config.hidden_size, config.vocab_size, bias=False)
self.gap_remask_head = GapRemaskHead(config.hidden_size)
self.gap_value_head = nn.Linear(config.hidden_size, 1, bias=False)
self._puma_streaming_state = None
self._puma_streaming_context = {"slot_offset": 0, "buffer_size": None}
self._gap_collect_branch_debug = False
self._last_branch_debug = None
# Initialize weights and apply final processing
self.post_init()
with torch.no_grad():
self.gap_value_head.weight.zero_()
def get_input_embeddings(self):
return self.model.embed_tokens
def set_input_embeddings(self, value):
self.model.embed_tokens = value
def get_output_embeddings(self):
return self.lm_head
def set_output_embeddings(self, new_embeddings):
self.lm_head = new_embeddings
def set_decoder(self, decoder):
self.model = decoder
def get_decoder(self):
return self.model
def _get_gap_reward_tokenizer(self):
tokenizer = getattr(self, "_gap_reward_tokenizer", None)
if tokenizer is None:
tokenizer = getattr(self, "_grpo_tokenizer", None)
return tokenizer
def _get_gap_reference_model(self):
kl_coef = float(getattr(self.config, "gap_grpo_kl_coef", 0.0) or 0.0)
ref_model_path = getattr(self.config, "gap_grpo_ref_model_path", None)
if kl_coef <= 0.0:
return None
if not ref_model_path:
raise ValueError("gap_grpo_kl_coef is positive, but gap_grpo_ref_model_path is not set.")
ref_model = self.__dict__.get("_gap_reference_model", None)
if ref_model is None:
dtype = self.lm_head.weight.dtype
ref_model = self.__class__.from_pretrained(
ref_model_path,
torch_dtype=dtype,
low_cpu_mem_usage=True,
)
ref_model.eval()
ref_model.requires_grad_(False)
self.__dict__["_gap_reference_model"] = ref_model
device = next(self.parameters()).device
ref_device = next(ref_model.parameters()).device
if ref_device != device:
ref_model.to(device)
ref_model.eval()
return ref_model
def _decode_gap_response_tokens(self, token_ids: torch.Tensor) -> str:
tokenizer = self._get_gap_reward_tokenizer()
if tokenizer is None:
return ""
if torch.is_tensor(token_ids):
token_ids = token_ids.detach().to("cpu").tolist()
token_ids = [int(token_id) for token_id in token_ids if int(token_id) >= 0]
try:
return tokenizer.decode(token_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False).strip()
except TypeError:
tokens = tokenizer.convert_ids_to_tokens(token_ids, skip_special_tokens=True)
tokens = [token for token in tokens if isinstance(token, str)]
if not tokens:
return ""
return tokenizer.convert_tokens_to_string(tokens).strip()
def _get_gap_eval_stop_words(self) -> list[str]:
tokenizer = self._get_gap_reward_tokenizer()
stop_words: list[str] = []
raw_stop_words = _os.getenv("SDAR_GAP_GRPO_STOP_WORDS", "")
if raw_stop_words.strip():
stop_words.extend([item for item in raw_stop_words.split("|||") if item])
eos_token_id = getattr(self.config, "eos_token_id", None)
if tokenizer is not None and eos_token_id is not None:
eos_ids = [eos_token_id] if isinstance(eos_token_id, int) else list(eos_token_id)
for token_id in eos_ids:
try:
stop_words.append(tokenizer.decode(int(token_id)))
except Exception:
pass
if tokenizer is not None and getattr(tokenizer, "eos_token", None):
stop_words.append(tokenizer.eos_token)
seen = set()
return [word for word in stop_words if word and not (word in seen or seen.add(word))]
def _get_gap_eval_stop_sequences(self) -> list[list[int]]:
cached = getattr(self, "_gap_eval_stop_sequences", None)
if cached is not None:
return cached
tokenizer = self._get_gap_reward_tokenizer()
sequences: list[list[int]] = []
if tokenizer is not None:
for stop in self._get_gap_eval_stop_words():
try:
sequence = tokenizer.encode(stop, add_special_tokens=False)
except Exception:
sequence = []
if sequence:
sequences.append([int(token_id) for token_id in sequence])
self._gap_eval_stop_sequences = sequences
return sequences
@staticmethod
def _match_gap_eval_stop_sequences(token_ids: torch.LongTensor, stop_sequences: list[list[int]]) -> torch.BoolTensor:
if token_ids.dim() == 1:
token_ids = token_ids.unsqueeze(0)
matches = torch.zeros(token_ids.shape[0], dtype=torch.bool, device=token_ids.device)
if token_ids.numel() == 0 or not stop_sequences:
return matches
for sequence in stop_sequences:
seq_len = len(sequence)
if seq_len <= 0:
continue
if seq_len == 1:
matches |= token_ids.eq(int(sequence[0])).any(dim=1)
continue
if token_ids.shape[1] < seq_len:
continue
sequence_tensor = token_ids.new_tensor(sequence)
matches |= token_ids.unfold(1, seq_len, 1).eq(sequence_tensor).all(dim=-1).any(dim=1)
return matches
def _strip_gap_eval_stop_text(self, text: str) -> str:
text = text or ""
for stop in self._get_gap_eval_stop_words():
text = text.split(stop)[0]
return text
@staticmethod
def _truncate_gap_debug_text(text: str, max_chars: int) -> str:
text = (text or "").replace("\n", "\\n").strip()
if max_chars > 0 and len(text) > max_chars:
return text[: max_chars - 3] + "..."
return text
def _apply_gap_grpo_remask_guards(
self,
candidate_mask: torch.BoolTensor,
target_scope_mask: torch.BoolTensor,
masked_indices: Optional[torch.BoolTensor] = None,
) -> torch.BoolTensor:
prefix_guard_tokens = _gap_env_int("SDAR_GAP_GRPO_REMASK_PREFIX_GUARD_TOKENS", 0)
tail_guard_blocks = _gap_env_int("SDAR_GAP_GRPO_REMASK_TAIL_GUARD_BLOCKS", 0)
exclude_frontier_blocks = _gap_env_int("SDAR_GAP_GRPO_EXCLUDE_FRONTIER_BLOCKS", 0)
middle_window_blocks = _gap_env_int("SDAR_GAP_GRPO_MIDDLE_WINDOW_BLOCKS", 0)
if prefix_guard_tokens <= 0 and tail_guard_blocks <= 0 and exclude_frontier_blocks <= 0 and middle_window_blocks <= 0:
return candidate_mask
guarded_mask = candidate_mask.clone()
block_size = max(1, int(getattr(self.config, "block_size", 1)))
for row_idx in range(guarded_mask.shape[0]):
target_positions = torch.nonzero(target_scope_mask[row_idx], as_tuple=False).flatten()
if target_positions.numel() == 0:
guarded_mask[row_idx] = False
continue
allowed_start = int(target_positions[0].item()) + max(0, prefix_guard_tokens)
allowed_end = int(target_positions[-1].item()) + 1 - max(0, tail_guard_blocks) * block_size
if masked_indices is not None:
active_target_mask = masked_indices[row_idx] & target_scope_mask[row_idx]
active_positions = torch.nonzero(active_target_mask, as_tuple=False).flatten()
if active_positions.numel() == 0:
guarded_mask[row_idx] = False
continue
frontier_block_start = (int(active_positions[0].item()) // block_size) * block_size
rolling_end = frontier_block_start + block_size - max(0, exclude_frontier_blocks) * block_size
allowed_end = min(allowed_end, rolling_end)
if middle_window_blocks > 0:
allowed_start = max(allowed_start, allowed_end - middle_window_blocks * block_size)
elif middle_window_blocks > 0:
allowed_end = min(allowed_end, allowed_start + middle_window_blocks * block_size)
if allowed_end <= allowed_start:
guarded_mask[row_idx] = False
continue
positions = torch.arange(guarded_mask.shape[1], device=guarded_mask.device)
middle_mask = positions.ge(allowed_start) & positions.lt(allowed_end)
guarded_mask[row_idx] &= middle_mask
return guarded_mask
def _maybe_capture_gap_branch_debug(
self,
clean_input_ids: torch.LongTensor,
target_scope_mask: torch.BoolTensor,
shared_state_input_ids: torch.LongTensor,
baseline_terminal: Optional[torch.LongTensor],
baseline_terminal_reward: Optional[torch.FloatTensor],
baseline_reward: Optional[torch.FloatTensor],
sampled_terminal: torch.LongTensor,
target_scope_flat: torch.BoolTensor,
sampled_terminal_reward: torch.FloatTensor,
rewards: torch.FloatTensor,
reward_gain: torch.FloatTensor,
remask_rate: torch.FloatTensor,
baseline_remaining_mask_rate: Optional[torch.FloatTensor],
sampled_remaining_mask_rate: torch.FloatTensor,
sampled_full: torch.BoolTensor,
full_candidate_mask: torch.BoolTensor,
baseline_debug_mask: Optional[torch.BoolTensor] = None,
sampled_debug_mask: Optional[torch.BoolTensor] = None,
baseline_stop_hit: Optional[torch.BoolTensor] = None,
sampled_stop_hit: Optional[torch.BoolTensor] = None,
baseline_length_cap_hit: Optional[torch.BoolTensor] = None,
sampled_length_cap_hit: Optional[torch.BoolTensor] = None,
) -> None:
if not getattr(self, "_gap_collect_branch_debug", False):
return
batch_size = clean_input_ids.shape[0]
num_samples = rewards.shape[0]
max_examples = int(getattr(self.config, "gap_grpo_branch_debug_max_examples", 1))
max_branches = int(getattr(self.config, "gap_grpo_branch_debug_max_branches", 3))
max_chars = int(getattr(self.config, "gap_grpo_branch_debug_max_chars", 160))
example_count = max(1, min(batch_size, max_examples))
branch_count = num_samples if max_branches <= 0 else max(1, min(num_samples, max_branches))
has_baseline = (
baseline_terminal is not None
and baseline_terminal_reward is not None
and baseline_reward is not None
and baseline_remaining_mask_rate is not None
)
baseline_text_mask = baseline_debug_mask if baseline_debug_mask is not None else target_scope_mask
sampled_text_mask = sampled_debug_mask if sampled_debug_mask is not None else target_scope_flat
snapshot = {
"candidate_count_mean": float(full_candidate_mask.to(torch.float32).sum(dim=-1).mean().item()) if full_candidate_mask.numel() > 0 else 0.0,
"baseline_remaining_mask_rate": float(baseline_remaining_mask_rate.mean().item()) if baseline_remaining_mask_rate is not None else None,
"sampled_remaining_mask_rate": float(sampled_remaining_mask_rate.mean().item()),
"sampled_remaining_mask_rate_max": float(sampled_remaining_mask_rate.max().item()) if sampled_remaining_mask_rate.numel() > 0 else 0.0,
"has_baseline": has_baseline,
"examples": [],
}
for row_idx in range(example_count):
answer_positions = torch.nonzero(target_scope_mask[row_idx], as_tuple=False).flatten()
prompt_length = int(answer_positions[0].item()) if answer_positions.numel() > 0 else clean_input_ids.shape[1]
visible_answer_mask = target_scope_mask[row_idx] & shared_state_input_ids[row_idx].ne(self.config.mask_token_id)
visible_answer_positions = torch.nonzero(visible_answer_mask, as_tuple=False).flatten()
if visible_answer_positions.numel() > 0:
shared_prefix_end = int(visible_answer_positions[-1].item()) + 1
shared_prefix_text = self._truncate_gap_debug_text(
self._decode_gap_response_tokens(shared_state_input_ids[row_idx, prompt_length:shared_prefix_end]),
max_chars,
)
else:
shared_prefix_end = prompt_length
shared_prefix_text = ""
block_size = max(1, int(getattr(self.config, "block_size", 1)))
shared_visible_answer_tokens = int(visible_answer_positions.numel())
shared_visible_full_blocks = shared_visible_answer_tokens // block_size
frontier_answer_block = shared_visible_answer_tokens // block_size
shared_answer_tokens = int(answer_positions.numel())
gold_text = self._truncate_gap_debug_text(
self._decode_gap_response_tokens(clean_input_ids[row_idx][target_scope_mask[row_idx]]),
max_chars,
)
if has_baseline:
assert baseline_terminal is not None
baseline_text = self._truncate_gap_debug_text(
self._decode_gap_response_tokens(baseline_terminal[row_idx][baseline_text_mask[row_idx]]),
max_chars,
)
else:
baseline_text = ""
branch_entries = []
branch_texts = []
for branch_idx in range(branch_count):
flat_idx = branch_idx * batch_size + row_idx
branch_text = self._truncate_gap_debug_text(
self._decode_gap_response_tokens(sampled_terminal[flat_idx][sampled_text_mask[flat_idx]]),
max_chars,
)
branch_stop = bool(sampled_stop_hit[branch_idx, row_idx].item()) if sampled_stop_hit is not None else None
branch_cap = bool(sampled_length_cap_hit[branch_idx, row_idx].item()) if sampled_length_cap_hit is not None else None
branch_texts.append(branch_text)
branch_entries.append(
{
"branch_idx": int(branch_idx),
"terminal_reward": float(sampled_terminal_reward[branch_idx, row_idx].item()),
"reward": float(rewards[branch_idx, row_idx].item()),
"gain": float(reward_gain[branch_idx, row_idx].item()),
"remask_rate": float(remask_rate[branch_idx, row_idx].item()),
"remaining_mask_rate": float(sampled_remaining_mask_rate[branch_idx, row_idx].item()),
"remask_tokens": int(sampled_full[branch_idx, row_idx].sum().item()),
"remask_block_span": "",
"remask_answer_offset_span": "",
"stop_hit": branch_stop,
"length_cap_hit": branch_cap,
"text": branch_text,
}
)
branch_answer_positions = torch.nonzero(sampled_text_mask[flat_idx], as_tuple=False).flatten()
remask_positions = torch.nonzero(
sampled_full[branch_idx, row_idx] & sampled_text_mask[flat_idx],
as_tuple=False,
).flatten()
if remask_positions.numel() > 0 and branch_answer_positions.numel() > 0:
answer_offsets = torch.searchsorted(branch_answer_positions, remask_positions)
block_ids = torch.div(answer_offsets, block_size, rounding_mode="floor")
branch_entries[-1]["remask_block_span"] = f"{int(block_ids.min().item())}-{int(block_ids.max().item())}"
branch_entries[-1]["remask_answer_offset_span"] = f"{int(answer_offsets.min().item())}-{int(answer_offsets.max().item())}"
base_stop = bool(baseline_stop_hit[row_idx].item()) if (has_baseline and baseline_stop_hit is not None) else None
base_cap = bool(baseline_length_cap_hit[row_idx].item()) if (has_baseline and baseline_length_cap_hit is not None) else None
snapshot["examples"].append(
{
"example_idx": int(row_idx),
"prompt_length": prompt_length,
"answer_token_count": shared_answer_tokens,
"shared_visible_answer_tokens": shared_visible_answer_tokens,
"shared_visible_full_blocks": shared_visible_full_blocks,
"frontier_answer_block": frontier_answer_block,
"shared_prefix_text": shared_prefix_text,
"gold": gold_text,
"baseline_text": baseline_text,
"baseline_terminal_reward": float(baseline_terminal_reward[row_idx].item()) if has_baseline else None,
"baseline_reward": float(baseline_reward[row_idx].item()) if has_baseline else None,
"baseline_remaining_mask_rate": float(baseline_remaining_mask_rate[row_idx].item()) if has_baseline else None,
"baseline_stop_hit": base_stop,
"baseline_length_cap_hit": base_cap,
"unique_branch_texts": int(len(set(branch_texts))),
"branches": branch_entries,
}
)
self._last_branch_debug = snapshot
self._gap_collect_branch_debug = False
@staticmethod
def _extract_gap_boxed_answer(text: str) -> str:
marker = "\\boxed"
start = text.rfind(marker)
if start == -1:
return ""
brace_start = text.find("{", start)
if brace_start == -1:
return ""
depth = 0
chars = []
for ch in text[brace_start + 1:]:
if ch == "{":
depth += 1
chars.append(ch)
elif ch == "}":
if depth == 0:
return "".join(chars).strip()
depth -= 1
chars.append(ch)
else:
chars.append(ch)
return ""
@staticmethod
def _normalize_gap_answer_text(text: str) -> str:
text = (text or "").strip()
boxed = SDARForCausalLM._extract_gap_boxed_answer(text)
if boxed:
text = boxed
text = text.strip().lower()
if text.endswith("."):
text = text[:-1]
text = text.replace("\\left", "").replace("\\right", "")
text = text.replace("$", "").replace(" ", "")
text = text.replace("\\,", "").replace(",", "")
return text
@staticmethod
def _normalize_gap_latex_answer_text(text: str) -> str:
text = (text or "").strip().lower()
text = text.replace("\\left", "").replace("\\right", "")
text = text.replace("\\dfrac", "\\frac").replace("\\tfrac", "\\frac")
text = text.replace("\\cdot", "*").replace("\\times", "*")
text = text.replace("\\,", "").replace("\\!", "").replace("\\;", "").replace("\\:", "")
text = text.replace("$", "").replace(",", "")
text = re.sub(r"\\(?:mathrm|text)\{([^{}]*)\}", r"\1", text)
text = re.sub(r"\s+", "", text)
if text.startswith("{") and text.endswith("}"):
text = text[1:-1]
return text
@staticmethod
def _gap_latex_answer_has_symbolic_token(text: str) -> bool:
text = SDARForCausalLM._normalize_gap_latex_answer_text(text)
symbolic_patterns = (
"\\pi",
"\\sqrt",
"\\sin",
"\\cos",
"\\tan",
"\\log",
"\\ln",
"\\theta",
"\\alpha",
"\\beta",
"\\gamma",
"\\infty",
)
if any(pattern in text for pattern in symbolic_patterns):
return True
return bool(re.search(r"[a-z]", text.replace("\\frac", "")))
@staticmethod
def _gap_strict_boxed_answer_compatible(pred_text: str, gold_text: str) -> bool:
pred_boxed = SDARForCausalLM._extract_gap_boxed_answer(pred_text)
gold_boxed = SDARForCausalLM._extract_gap_boxed_answer(gold_text)
if not pred_boxed or not gold_boxed:
return False
pred_norm = SDARForCausalLM._normalize_gap_latex_answer_text(pred_boxed)
gold_norm = SDARForCausalLM._normalize_gap_latex_answer_text(gold_boxed)
if not pred_norm or not gold_norm:
return False
if pred_norm == gold_norm:
return True
# OpenCompass' postprocessor can reduce symbolic boxed answers such as
# \pi/12 to the trailing number 12. Do not let that fallback mark a
# symbolic answer correct unless both sides retain the same symbols.
pred_symbolic = SDARForCausalLM._gap_latex_answer_has_symbolic_token(pred_boxed)
gold_symbolic = SDARForCausalLM._gap_latex_answer_has_symbolic_token(gold_boxed)
if pred_symbolic != gold_symbolic:
return False
if "\\pi" in pred_norm or "\\pi" in gold_norm:
return "\\pi" in pred_norm and "\\pi" in gold_norm
return True
def _get_gap_opencompass_math_tools(self):
cached = getattr(self, "_gap_opencompass_math_tools", None)
if cached is not None:
return cached
repo_root = _os.getenv("SDAR_REPO_ROOT", "/work/leotsia0416/projects/SDAR")
oc_root = _os.getenv("SDAR_OPENCOMPASS_ROOT", _os.path.join(repo_root, "evaluation", "opencompass"))
if oc_root and oc_root not in sys.path:
sys.path.insert(0, oc_root)
try:
from opencompass.datasets.math import MATHEvaluator, math_postprocess_sdar
evaluator = MATHEvaluator(version="v2")
cached = (math_postprocess_sdar, evaluator)
except Exception:
cached = None
self._gap_opencompass_math_tools = cached
return cached
def _score_gap_answer_with_opencompass(self, pred_text: str, gold_text: str) -> bool:
tools = self._get_gap_opencompass_math_tools()
pred_text = self._strip_gap_eval_stop_text(pred_text)
gold_text = self._strip_gap_eval_stop_text(gold_text)
if not self._gap_strict_boxed_answer_compatible(pred_text, gold_text):
return False
if tools is not None:
postprocess, evaluator = tools
try:
pred = postprocess(pred_text)
gold = postprocess(gold_text)
return bool(pred and gold and evaluator.is_equiv(pred, gold))
except Exception:
pass
gold_norm = self._normalize_gap_answer_text(gold_text)
pred_norm = self._normalize_gap_answer_text(pred_text)
return bool(gold_norm and gold_norm == pred_norm)
@staticmethod
def _compute_gap_answer_block_ids(answer_mask: torch.BoolTensor, block_size: int) -> tuple[torch.LongTensor, int]:
seq_len = answer_mask.shape[0]
block_ids = torch.full((seq_len,), -1, dtype=torch.long, device=answer_mask.device)
answer_positions = torch.nonzero(answer_mask, as_tuple=False).flatten()
if answer_positions.numel() == 0:
return block_ids, 0
answer_order = torch.arange(answer_positions.numel(), device=answer_mask.device, dtype=torch.long)
block_ids[answer_positions] = torch.div(answer_order, block_size, rounding_mode="floor")
total_blocks = int(block_ids[answer_positions[-1]].item()) + 1
return block_ids, total_blocks
@staticmethod
def _compute_gap_prefix_progress_limits(
labels: torch.LongTensor,
num_tokens,
block_size: int,
rollout_steps: int,
) -> torch.LongTensor:
target_mask = labels.ne(-100)
limits = torch.ones(labels.shape[0], dtype=torch.long, device=labels.device)
for batch_idx, packed_lengths in enumerate(num_tokens):
cursor = 0
max_blocks = 0
for sample_len_tensor in packed_lengths:
sample_len = int(sample_len_tensor.item())
sample_end = cursor + sample_len
answer_tokens = int(target_mask[batch_idx, cursor:sample_end].sum().item())
if answer_tokens > 0:
max_blocks = max(max_blocks, int(math.ceil(answer_tokens / max(1, block_size))))
cursor = sample_end
limits[batch_idx] = max(1, max_blocks * max(1, rollout_steps))
return limits
def _build_gap_prefix_teacher_forced_state(
self,
clean_input_ids: torch.LongTensor,
labels: torch.LongTensor,
num_tokens,
progress_units: torch.LongTensor,
) -> torch.LongTensor:
rollout_steps = max(1, int(getattr(self.config, "gap_rollout_steps", self.config.block_size)))
block_size = int(self.config.block_size)
transfer_schedule = get_num_transfer_tokens(block_size, rollout_steps).to(clean_input_ids.device)
cumulative_transfers = torch.cat(
(
torch.zeros(1, dtype=torch.long, device=clean_input_ids.device),
transfer_schedule.cumsum(dim=0),
),
dim=0,
)
target_mask = labels.ne(-100)
noisy_input_ids = torch.where(
target_mask,
torch.full_like(clean_input_ids, self.config.mask_token_id),
clean_input_ids,
)
for batch_idx, packed_lengths in enumerate(num_tokens):
progress = max(0, int(progress_units[batch_idx].item()))
full_blocks = progress // rollout_steps
frontier_stage = progress % rollout_steps
frontier_visible_tokens = int(cumulative_transfers[min(frontier_stage, rollout_steps)].item())
visible_answer_tokens = full_blocks * block_size + frontier_visible_tokens
cursor = 0
for sample_len_tensor in packed_lengths:
sample_len = int(sample_len_tensor.item())
sample_end = cursor + sample_len
sample_target_mask = target_mask[batch_idx, cursor:sample_end]
answer_positions = torch.nonzero(sample_target_mask, as_tuple=False).flatten()
if answer_positions.numel() > 0 and visible_answer_tokens > 0:
reveal_count = min(int(answer_positions.numel()), visible_answer_tokens)
reveal_positions = answer_positions[:reveal_count]
local_noisy = noisy_input_ids[batch_idx, cursor:sample_end]
local_clean = clean_input_ids[batch_idx, cursor:sample_end]
local_noisy[reveal_positions] = local_clean[reveal_positions]
cursor = sample_end
return noisy_input_ids
def _should_use_gap_prefix_frontier_state(self) -> bool:
return (
getattr(self.config, "gap_rollout_strategy", "low_confidence_dynamic") == "sequential"
and getattr(self.config, "gap_rollout_scope", "all") == "frontier_block"
)
@staticmethod
def _sample_from_logits(
logits: torch.Tensor,
temperature: float,
top_k: int,
top_p: float,
) -> torch.LongTensor:
original_shape = logits.shape[:-1]
vocab_size = logits.shape[-1]
logits = logits.reshape(-1, vocab_size)
if temperature <= 0.0:
return logits.argmax(dim=-1).reshape(*original_shape)
sample_logits = logits / max(temperature, 1e-5)
candidate_indices = None
if top_k > 0 and top_k < sample_logits.shape[-1]:
sample_logits, candidate_indices = torch.topk(sample_logits, k=top_k, dim=-1)
if 0.0 < top_p < 1.0:
sorted_logits, sorted_indices = torch.sort(sample_logits, descending=True, dim=-1)
sorted_probs = sorted_logits.softmax(dim=-1)
cumulative_probs = sorted_probs.cumsum(dim=-1)
sorted_remove = cumulative_probs > top_p
sorted_remove[..., 1:] = sorted_remove[..., :-1].clone()
sorted_remove[..., 0] = False
remove_mask = torch.zeros_like(sorted_remove, dtype=torch.bool)
remove_mask.scatter_(dim=-1, index=sorted_indices, src=sorted_remove)
sample_logits = sample_logits.masked_fill(remove_mask, float("-inf"))
probs = sample_logits.softmax(dim=-1)
sampled_local = torch.multinomial(probs, num_samples=1).squeeze(-1)
if candidate_indices is None:
return sampled_local.reshape(*original_shape)
sampled = candidate_indices.gather(dim=-1, index=sampled_local.unsqueeze(-1)).squeeze(-1)
return sampled.reshape(*original_shape)
@staticmethod
def _sample_from_logits_with_eval_scores(
logits: torch.Tensor,
temperature: float,
top_k: int,
top_p: float,
) -> tuple[torch.LongTensor, torch.FloatTensor]:
original_shape = logits.shape[:-1]
vocab_size = logits.shape[-1]
logits = logits.reshape(-1, vocab_size)
if temperature <= 0.0:
probs = logits.softmax(dim=-1)
token = logits.argmax(dim=-1)
token_prob = probs.gather(-1, token.unsqueeze(-1)).squeeze(-1)
return token.reshape(*original_shape), token_prob.reshape(*original_shape)
sample_logits = logits / max(temperature, 1e-5)
original_probs = sample_logits.softmax(dim=-1)
if top_k > 0 and top_k < sample_logits.shape[-1]:
values, _ = torch.topk(sample_logits, k=top_k, dim=-1)
min_values = values[..., -1, None]
sample_logits = torch.where(
sample_logits < min_values,
torch.full_like(sample_logits, float("-inf")),
sample_logits,
)
if 0.0 < top_p < 1.0:
sorted_logits, sorted_indices = torch.sort(sample_logits, descending=True, dim=-1)
cumulative_probs = sorted_logits.softmax(dim=-1).cumsum(dim=-1)
sorted_remove = cumulative_probs > top_p
sorted_remove[..., 1:] = sorted_remove[..., :-1].clone()
sorted_remove[..., 0] = False
remove_mask = torch.zeros_like(sorted_remove, dtype=torch.bool)
remove_mask.scatter_(dim=-1, index=sorted_indices, src=sorted_remove)
sample_logits = sample_logits.masked_fill(remove_mask, float("-inf"))
probs = sample_logits.softmax(dim=-1)
token = torch.multinomial(probs, num_samples=1).squeeze(-1)
token_prob = original_probs.gather(-1, token.unsqueeze(-1)).squeeze(-1)
return token.reshape(*original_shape), token_prob.reshape(*original_shape)
@staticmethod
def _resolve_gap_valid_length(num_tokens_for_row) -> int:
total = 0
for sample_len_tensor in num_tokens_for_row:
total += int(sample_len_tensor.item())
return total
@staticmethod
def _to_gap_block_mask(attention_mask):
if attention_mask is None or not isinstance(attention_mask, torch.Tensor):
return attention_mask
if attention_mask.dim() == 4:
base_mask = attention_mask[:, 0].to(dtype=torch.bool)
elif attention_mask.dim() == 3:
base_mask = attention_mask.to(dtype=torch.bool)
elif attention_mask.dim() == 2:
base_mask = attention_mask.to(dtype=torch.bool).unsqueeze(0)
else:
raise ValueError(f"Unsupported GAP rollout attention mask rank: {attention_mask.dim()}")
return create_block_mask(
lambda b, h, q_idx, kv_idx: base_mask[b, q_idx, kv_idx],
B=base_mask.size(0),
H=None,
Q_LEN=base_mask.size(1),
KV_LEN=base_mask.size(2),
)
@staticmethod
def _select_gap_eval_window_transfer_tokens(
masked_indices: torch.BoolTensor,
proposal_scores_full: torch.FloatTensor,
block_size: int,
num_transfer_tokens: int,
strategy: str,
confidence_threshold: float,
) -> torch.BoolTensor:
reveal_mask = torch.zeros_like(masked_indices)
if num_transfer_tokens <= 0:
return reveal_mask
batch_size, seq_len = masked_indices.shape
for batch_idx in range(batch_size):
for block_start in range(0, seq_len, block_size):
block_end = min(block_start + block_size, seq_len)
block_mask = masked_indices[batch_idx, block_start:block_end]
if not block_mask.any():
continue
masked_local_indices = torch.nonzero(block_mask, as_tuple=False).flatten()
block_scores = proposal_scores_full[batch_idx, block_start:block_end][masked_local_indices]
chosen = select_teacher_forced_rollout_tokens.__globals__["_select_block_positions"](
block_scores=block_scores,
masked_local_indices=masked_local_indices,
num_transfer_tokens=num_transfer_tokens,
strategy=strategy,
confidence_threshold=confidence_threshold,
)
reveal_mask[batch_idx, block_start:block_end][chosen] = True
return reveal_mask
def _build_gap_eval_rollout_prefix_cache(
self,
x: torch.LongTensor,
attention_mask: torch.Tensor,
position_ids: torch.LongTensor,
prefix_token_end: int,
) -> DynamicCache:
cache = DynamicCache()
if prefix_token_end <= 0:
return cache
cur_x = x[:, :prefix_token_end]
cur_attn_mask = attention_mask[:, :, :prefix_token_end, :prefix_token_end]
cur_position_ids = position_ids[:, :prefix_token_end]
if self.model.training and not _gap_env_flag("SDAR_GAP_GRPO_STRICT_EVAL_DECODE", False):
cur_attn_mask = self._to_gap_block_mask(cur_attn_mask)
self.model(
cur_x,
attention_mask=cur_attn_mask,
position_ids=cur_position_ids,
past_key_values=cache,
use_cache=True,
store_kv=True,
)
return cache
def _commit_gap_eval_rollout_block(
self,
cache: DynamicCache,
x: torch.LongTensor,
attention_mask: torch.Tensor,
position_ids: torch.LongTensor,
block_start: int,
block_end: int,
) -> None:
if block_end <= block_start:
return
cur_x = x[:, block_start:block_end]
cur_attn_mask = attention_mask[:, :, block_start:block_end, :block_end]
cur_position_ids = position_ids[:, block_start:block_end]
if self.model.training and not _gap_env_flag("SDAR_GAP_GRPO_STRICT_EVAL_DECODE", False):
cur_attn_mask = self._to_gap_block_mask(cur_attn_mask)
self.model(
cur_x,
attention_mask=cur_attn_mask,
position_ids=cur_position_ids,
past_key_values=cache,
use_cache=True,
store_kv=True,
)
def _decode_gap_eval_window(
self,
window_inputs: torch.LongTensor,
window_attention_mask: torch.Tensor,
window_position_ids: torch.LongTensor,
prefix_cache: DynamicCache,
mask_id: int,
block_length: int,
denoising_steps: int,
temperature: float,
top_k: int,
top_p: float,
rollout_strategy: str,
confidence_threshold: float,
) -> torch.LongTensor:
rollout_steps = max(1, int(denoising_steps))
transfer_schedule = get_num_transfer_tokens(block_length, rollout_steps).to(window_inputs.device)
current_inputs = window_inputs.clone()
current_stage = 0
strict_eval_decode = _gap_env_flag("SDAR_GAP_GRPO_STRICT_EVAL_DECODE", False)
block_attention_mask = self._to_gap_block_mask(window_attention_mask) if self.model.training and not strict_eval_decode else window_attention_mask
while True:
masked_indices = current_inputs.eq(mask_id)
if not masked_indices.any():
break
outputs = self.model(
current_inputs,
attention_mask=block_attention_mask,
position_ids=window_position_ids,
past_key_values=prefix_cache,
use_cache=True,
store_kv=False,
output_attentions=False,
output_hidden_states=False,
return_dict=True,
)
logits = self.lm_head(outputs.last_hidden_state).float()
logits[..., mask_id] = float("-inf")
proposal_ids, proposal_scores = self._sample_from_logits_with_eval_scores(
logits=logits,
temperature=temperature,
top_k=top_k,
top_p=top_p,
)
proposal_ids = torch.where(masked_indices, proposal_ids, current_inputs)
proposal_scores_full = torch.where(
masked_indices,
proposal_scores,
torch.full_like(proposal_scores, float("-inf")),
)
if current_stage >= rollout_steps:
current_inputs[masked_indices] = proposal_ids[masked_indices]
break
reveal_mask = self._select_gap_eval_window_transfer_tokens(
masked_indices=masked_indices,
proposal_scores_full=proposal_scores_full,
block_size=block_length,
num_transfer_tokens=int(transfer_schedule[current_stage].item()),
strategy=rollout_strategy,
confidence_threshold=confidence_threshold,
)
fill_mask = reveal_mask if reveal_mask.any() else masked_indices
current_inputs[fill_mask] = proposal_ids[fill_mask]
current_stage += 1
return current_inputs
@staticmethod
def _compute_gap_answer_start(labels_row: torch.LongTensor, valid_length: int) -> int:
valid_length = max(0, int(valid_length))
if valid_length <= 0:
return 0
answer_mask = labels_row[:valid_length].ne(-100)
if not answer_mask.any():
return valid_length
return int(torch.nonzero(answer_mask, as_tuple=False)[0].item())
def _should_stop_gap_eval_rollout(
self,
generated_prefix: torch.LongTensor,
) -> bool:
stop_sequences = self._get_gap_eval_stop_sequences()
if not stop_sequences:
return False
return bool(self._match_gap_eval_stop_sequences(generated_prefix, stop_sequences).any().item())
@torch.no_grad()
def _rollout_gap_row_to_terminal_eval_style(
self,
clean_input_ids_row: torch.LongTensor,
noisy_input_ids_row: torch.LongTensor,
labels_row: torch.LongTensor,
position_ids_row: torch.LongTensor,
valid_length: int,
rollout_strategy: str,
rollout_confidence_threshold: float,
sample_temperature: float,
sample_top_k: int,
sample_top_p: float,
) -> torch.LongTensor:
block_length = int(self.config.block_size)
valid_length = max(0, int(valid_length))
if valid_length <= 0:
return noisy_input_ids_row
answer_mask = labels_row[:valid_length].ne(-100)
if not answer_mask.any():
return noisy_input_ids_row
prompt_length = int(torch.nonzero(answer_mask, as_tuple=False)[0].item())
total_length = int(math.ceil(valid_length / max(1, block_length)) * block_length)
x = torch.full((1, total_length), self.config.mask_token_id, dtype=noisy_input_ids_row.dtype, device=noisy_input_ids_row.device)
x[:, :valid_length] = noisy_input_ids_row[:valid_length].unsqueeze(0)
block_mask = torch.tril(torch.ones(total_length // block_length, total_length // block_length, device=x.device), diagonal=0)
attention_mask = (
block_mask.repeat_interleave(block_length, dim=0)
.repeat_interleave(block_length, dim=1)
.unsqueeze(0)
.unsqueeze(1)
)
valid_positions = torch.zeros((1, total_length), dtype=attention_mask.dtype, device=x.device)
valid_positions[:, :valid_length] = 1
attention_mask = attention_mask * valid_positions[:, None, None, :]
attention_mask = attention_mask * valid_positions[:, None, :, None]
rollout_position_ids = torch.zeros((1, total_length), dtype=position_ids_row.dtype, device=x.device)
rollout_position_ids[:, :valid_length] = position_ids_row[:valid_length].unsqueeze(0)
if total_length > valid_length:
start_position = int(position_ids_row[valid_length - 1].item()) + 1 if valid_length > 0 else 0
rollout_position_ids[:, valid_length:] = torch.arange(
start_position,
start_position + (total_length - valid_length),
dtype=position_ids_row.dtype,
device=x.device,
).unsqueeze(0)
num_blocks = total_length // block_length
prefill_blocks = prompt_length // block_length
prefill_length = prefill_blocks * block_length
prefix_cache = self._build_gap_eval_rollout_prefix_cache(
x=x,
attention_mask=attention_mask,
position_ids=rollout_position_ids,
prefix_token_end=prefill_length,
)
window_start_block = prefill_blocks
finished = False
for block_idx in range(prefill_blocks, num_blocks):
if finished:
break
while block_idx - window_start_block >= 1:
commit_start = window_start_block * block_length
commit_end = commit_start + block_length
self._commit_gap_eval_rollout_block(
cache=prefix_cache,
x=x,
attention_mask=attention_mask,
position_ids=rollout_position_ids,
block_start=commit_start,
block_end=commit_end,
)
window_start_block += 1
window_token_start = window_start_block * block_length
window_token_end = (block_idx + 1) * block_length
window_slice = slice(window_token_start, window_token_end)
window_inputs = x[:, window_slice].clone()
window_attention_mask = attention_mask[:, :, window_token_start:window_token_end, :window_token_end]
window_position_ids = rollout_position_ids[:, window_slice]
window_inputs = self._decode_gap_eval_window(
window_inputs=window_inputs,
window_attention_mask=window_attention_mask,
window_position_ids=window_position_ids,
prefix_cache=prefix_cache,
mask_id=self.config.mask_token_id,
block_length=block_length,
denoising_steps=max(1, int(getattr(self.config, "gap_rollout_steps", block_length))),
temperature=sample_temperature,
top_k=sample_top_k,
top_p=sample_top_p,
rollout_strategy=rollout_strategy,
confidence_threshold=rollout_confidence_threshold,
)
x[:, window_slice] = window_inputs
generated_prefix = x[:, prompt_length: min(valid_length, window_token_end)]
if self._should_stop_gap_eval_rollout(generated_prefix[0]):
eos_token_id = getattr(self.config, "eos_token_id", None)
if isinstance(eos_token_id, (list, tuple)):
eos_fill = int(eos_token_id[0])
elif eos_token_id is None:
eos_fill = self.config.mask_token_id
else:
eos_fill = int(eos_token_id)
if window_token_end < valid_length:
x[:, window_token_end:valid_length] = eos_fill
finished = True
terminal_row = noisy_input_ids_row.clone()
terminal_row[:valid_length] = x[0, :valid_length]
return terminal_row
@torch.no_grad()
def _rollout_gap_group_to_terminal_eval_style(
self,
clean_input_ids: torch.LongTensor,
noisy_input_ids: torch.LongTensor,
labels: torch.LongTensor,
position_ids: torch.LongTensor,
valid_lengths: list[int],
prompt_lengths: list[int],
rollout_strategy: str,
rollout_confidence_threshold: float,
sample_temperature: float,
sample_top_k: int,
sample_top_p: float,
) -> torch.LongTensor:
batch_size = noisy_input_ids.shape[0]
block_length = int(self.config.block_size)
heartbeat_interval = _gap_env_int("SDAR_GAP_ROLLOUT_HEARTBEAT_INTERVAL_BLOCKS", 0)
heartbeat_enabled = heartbeat_interval > 0 and _gap_is_rank0()
debug_step = getattr(self, "_gap_debug_global_step", -1)
valid_lengths = [max(0, int(v)) for v in valid_lengths]
prompt_lengths = [max(0, int(p)) for p in prompt_lengths]
total_lengths = [
int(math.ceil(valid_length / max(1, block_length)) * block_length)
for valid_length in valid_lengths
]
max_total_length = max(total_lengths, default=0)
if max_total_length <= 0:
return noisy_input_ids
device = noisy_input_ids.device
dtype = noisy_input_ids.dtype
x = torch.full(
(batch_size, max_total_length),
self.config.mask_token_id,
dtype=dtype,
device=device,
)
for row_idx, valid_length in enumerate(valid_lengths):
if valid_length > 0:
x[row_idx, :valid_length] = noisy_input_ids[row_idx, :valid_length]
num_blocks = max_total_length // block_length
block_mask = torch.tril(torch.ones(num_blocks, num_blocks, device=device), diagonal=0)
attention_mask = (
block_mask.repeat_interleave(block_length, dim=0)
.repeat_interleave(block_length, dim=1)
.unsqueeze(0)
.unsqueeze(1)
.expand(batch_size, -1, -1, -1)
.clone()
)
valid_positions = torch.zeros((batch_size, max_total_length), dtype=attention_mask.dtype, device=device)
for row_idx, valid_length in enumerate(valid_lengths):
if valid_length > 0:
valid_positions[row_idx, :valid_length] = 1
attention_mask = attention_mask * valid_positions[:, None, None, :]
attention_mask = attention_mask * valid_positions[:, None, :, None]
rollout_position_ids = torch.zeros((batch_size, max_total_length), dtype=position_ids.dtype, device=device)
for row_idx, valid_length in enumerate(valid_lengths):
if valid_length <= 0:
continue
rollout_position_ids[row_idx, :valid_length] = position_ids[row_idx, :valid_length]
if max_total_length > valid_length:
start_position = int(position_ids[row_idx, valid_length - 1].item()) + 1
rollout_position_ids[row_idx, valid_length:max_total_length] = torch.arange(
start_position,
start_position + (max_total_length - valid_length),
dtype=position_ids.dtype,
device=device,
)
prefill_blocks = min(prompt_length // block_length for prompt_length in prompt_lengths)
prefill_length = prefill_blocks * block_length
prefix_cache = self._build_gap_eval_rollout_prefix_cache(
x=x,
attention_mask=attention_mask,
position_ids=rollout_position_ids,
prefix_token_end=prefill_length,
)
window_start_block = prefill_blocks
row_num_blocks = torch.tensor(
[max(0, total_length // block_length) for total_length in total_lengths],
dtype=torch.long,
device=device,
)
finished = row_num_blocks.le(prefill_blocks)
for block_idx in range(prefill_blocks, num_blocks):
if bool(finished.all().item()):
break
active_rows = (~finished) & row_num_blocks.gt(block_idx)
if not bool(active_rows.any().item()):
break
if heartbeat_enabled and ((block_idx - prefill_blocks) % heartbeat_interval == 0):
logger.info(
"[GAP rollout heartbeat] step=%s block=%s/%s active_rows=%s finished=%s",
debug_step,
int(block_idx - prefill_blocks),
int(max(num_blocks - prefill_blocks, 0)),
int(active_rows.sum().item()),
int(finished.sum().item()),
)
while block_idx - window_start_block >= 1:
commit_start = window_start_block * block_length
commit_end = commit_start + block_length
self._commit_gap_eval_rollout_block(
cache=prefix_cache,
x=x,
attention_mask=attention_mask,
position_ids=rollout_position_ids,
block_start=commit_start,
block_end=commit_end,
)
window_start_block += 1
window_token_start = window_start_block * block_length
window_token_end = (block_idx + 1) * block_length
window_slice = slice(window_token_start, window_token_end)
frozen_window_inputs = x[:, window_slice].clone()
window_inputs = x[:, window_slice].clone()
window_attention_mask = attention_mask[:, :, window_token_start:window_token_end, :window_token_end]
window_position_ids = rollout_position_ids[:, window_slice]
window_inputs = self._decode_gap_eval_window(
window_inputs=window_inputs,
window_attention_mask=window_attention_mask,
window_position_ids=window_position_ids,
prefix_cache=prefix_cache,
mask_id=self.config.mask_token_id,
block_length=block_length,
denoising_steps=max(1, int(getattr(self.config, "gap_rollout_steps", block_length))),
temperature=sample_temperature,
top_k=sample_top_k,
top_p=sample_top_p,
rollout_strategy=rollout_strategy,
confidence_threshold=rollout_confidence_threshold,
)
window_inputs[~active_rows] = frozen_window_inputs[~active_rows]
x[:, window_slice] = window_inputs
for row_idx in torch.nonzero(active_rows, as_tuple=False).flatten().tolist():
row_prompt_length = prompt_lengths[row_idx]
row_valid_length = valid_lengths[row_idx]
generated_prefix = x[row_idx, row_prompt_length:min(row_valid_length, window_token_end)]
if self._should_stop_gap_eval_rollout(generated_prefix):
eos_token_id = getattr(self.config, "eos_token_id", None)
if isinstance(eos_token_id, (list, tuple)):
eos_fill = int(eos_token_id[0])
elif eos_token_id is None:
eos_fill = self.config.mask_token_id
else:
eos_fill = int(eos_token_id)
if window_token_end < row_valid_length:
x[row_idx, window_token_end:row_valid_length] = eos_fill
finished[row_idx] = True
finished |= row_num_blocks.le(block_idx + 1)
terminal_input_ids = noisy_input_ids.clone()
for row_idx, valid_length in enumerate(valid_lengths):
if valid_length > 0:
terminal_input_ids[row_idx, :valid_length] = x[row_idx, :valid_length]
return terminal_input_ids
@torch.no_grad()
def _rollout_gap_group_eval_with_sampled_remask(
self,
noisy_input_ids: torch.LongTensor,
labels: torch.LongTensor,
position_ids: torch.LongTensor,
valid_lengths: list[int],
prompt_lengths: list[int],
num_samples: int,
rollout_strategy: str,
rollout_confidence_threshold: float,
sample_temperature: float,
sample_top_k: int,
sample_top_p: float,
enable_remask_actions: bool = True,
) -> tuple[torch.LongTensor, torch.BoolTensor, torch.FloatTensor, torch.FloatTensor, torch.FloatTensor, torch.BoolTensor, torch.BoolTensor, torch.BoolTensor]:
base_batch_size = noisy_input_ids.shape[0]
block_length = int(self.config.block_size)
num_samples = max(1, int(num_samples))
batch_size = base_batch_size * num_samples
source_valid_lengths = [max(0, int(v)) for _ in range(num_samples) for v in valid_lengths]
prompt_lengths = [max(0, int(p)) for _ in range(num_samples) for p in prompt_lengths]
eval_gen_length = _gap_env_int("SDAR_GAP_GRPO_EVAL_GEN_LENGTH", 1024)
eval_gen_length = max(1, int(eval_gen_length))
valid_lengths = [prompt_length + eval_gen_length for prompt_length in prompt_lengths]
total_lengths = [
int(math.ceil(valid_length / max(1, block_length)) * block_length)
for valid_length in valid_lengths
]
max_total_length = max(total_lengths, default=0)
if max_total_length <= 0:
empty = noisy_input_ids.unsqueeze(0).repeat(num_samples, 1, 1).view(batch_size, -1)
zeros = torch.zeros((num_samples, base_batch_size), dtype=torch.float32, device=noisy_input_ids.device)
empty_mask = torch.zeros_like(empty, dtype=torch.bool)
empty_flags = torch.zeros((num_samples, base_batch_size), dtype=torch.bool, device=noisy_input_ids.device)
return empty, empty_mask, zeros, zeros.mean(dim=0), zeros, empty_flags, empty_flags, torch.zeros((num_samples, base_batch_size, empty.shape[1]), dtype=torch.bool, device=noisy_input_ids.device)
strict_eval_decode = _gap_env_flag("SDAR_GAP_GRPO_STRICT_EVAL_DECODE", False)
restore_model_training = bool(strict_eval_decode and self.model.training)
if restore_model_training:
self.model.eval()
device = noisy_input_ids.device
dtype = noisy_input_ids.dtype
x = torch.full((batch_size, max_total_length), self.config.mask_token_id, dtype=dtype, device=device)
repeated_noisy = noisy_input_ids.unsqueeze(0).repeat(num_samples, 1, 1).view(batch_size, -1)
repeated_positions = position_ids.unsqueeze(0).repeat(num_samples, 1, 1).view(batch_size, -1)
for row_idx, prompt_length in enumerate(prompt_lengths):
prompt_copy_len = min(prompt_length, repeated_noisy.shape[1])
if prompt_copy_len > 0:
x[row_idx, :prompt_copy_len] = repeated_noisy[row_idx, :prompt_copy_len]
num_blocks = max_total_length // block_length
block_mask = torch.tril(torch.ones(num_blocks, num_blocks, device=device), diagonal=0)
attention_mask = (
block_mask.repeat_interleave(block_length, dim=0)
.repeat_interleave(block_length, dim=1)
.unsqueeze(0)
.unsqueeze(1)
.expand(batch_size, -1, -1, -1)
.clone()
)
valid_positions = torch.zeros((batch_size, max_total_length), dtype=attention_mask.dtype, device=device)
for row_idx, valid_length in enumerate(valid_lengths):
if valid_length > 0:
valid_positions[row_idx, :valid_length] = 1
attention_mask = attention_mask * valid_positions[:, None, None, :]
attention_mask = attention_mask * valid_positions[:, None, :, None]
rollout_position_ids = torch.zeros((batch_size, max_total_length), dtype=position_ids.dtype, device=device)
for row_idx, valid_length in enumerate(valid_lengths):
if valid_length <= 0:
continue
source_valid_length = min(source_valid_lengths[row_idx], repeated_positions.shape[1])
copy_len = min(source_valid_length, valid_length)
if copy_len > 0:
rollout_position_ids[row_idx, :copy_len] = repeated_positions[row_idx, :copy_len]
start_position = int(repeated_positions[row_idx, copy_len - 1].item()) + 1
else:
start_position = 0
if valid_length > copy_len:
rollout_position_ids[row_idx, copy_len:valid_length] = torch.arange(
start_position,
start_position + (valid_length - copy_len),
dtype=position_ids.dtype,
device=device,
)
prefill_blocks = min(prompt_length // block_length for prompt_length in prompt_lengths)
prefill_length = prefill_blocks * block_length
prefix_cache = self._build_gap_eval_rollout_prefix_cache(
x=x,
attention_mask=attention_mask,
position_ids=rollout_position_ids,
prefix_token_end=prefill_length,
)
window_start_block = prefill_blocks
row_num_blocks = torch.tensor([max(0, total_length // block_length) for total_length in total_lengths], dtype=torch.long, device=device)
finished = row_num_blocks.le(prefill_blocks)
logprob_sums = torch.zeros(batch_size, dtype=torch.float32, device=device)
entropy_sums = torch.zeros(batch_size, dtype=torch.float32, device=device)
candidate_counts = torch.zeros(batch_size, dtype=torch.float32, device=device)
remask_counts = torch.zeros(batch_size, dtype=torch.float32, device=device)
action_full_flat = torch.zeros((batch_size, max_total_length), dtype=torch.bool, device=device)
stop_hits = torch.zeros(batch_size, dtype=torch.bool, device=device)
prefix_guard_tokens = _gap_env_int("SDAR_GAP_GRPO_REMASK_PREFIX_GUARD_TOKENS", 0)
tail_guard_blocks = _gap_env_int("SDAR_GAP_GRPO_REMASK_TAIL_GUARD_BLOCKS", 1)
window_blocks = max(1, _gap_env_int("SDAR_GAP_GRPO_MIDDLE_WINDOW_BLOCKS", int(getattr(self.config, "gap_grpo_candidate_window_blocks", 4) or 4)))
exclude_frontier_blocks = max(0, _gap_env_int("SDAR_GAP_GRPO_EXCLUDE_FRONTIER_BLOCKS", 0))
remask_interval_blocks = max(1, _gap_env_int("SDAR_GAP_GRPO_REMASK_INTERVAL_BLOCKS", 1))
max_remask_candidates = max(1, int(getattr(self.config, "gap_remask_adv_max_candidates", 1) or 1))
prob_eps = float(getattr(self.config, "gap_grpo_sample_prob_eps", 1e-4) or 1e-4)
for block_idx in range(prefill_blocks, num_blocks):
if bool(finished.all().item()):
break
active_rows = (~finished) & row_num_blocks.gt(block_idx)
if not bool(active_rows.any().item()):
break
while block_idx - window_start_block >= window_blocks:
commit_start = window_start_block * block_length
commit_end = commit_start + block_length
self._commit_gap_eval_rollout_block(
cache=prefix_cache,
x=x,
attention_mask=attention_mask,
position_ids=rollout_position_ids,
block_start=commit_start,
block_end=commit_end,
)
window_start_block += 1
window_token_start = window_start_block * block_length
window_token_end = (block_idx + 1) * block_length
window_slice = slice(window_token_start, window_token_end)
frozen_window_inputs = x[:, window_slice].clone()
window_inputs = x[:, window_slice].clone()
window_attention_mask = attention_mask[:, :, window_token_start:window_token_end, :window_token_end]
window_position_ids = rollout_position_ids[:, window_slice]
window_inputs = self._decode_gap_eval_window(
window_inputs=window_inputs,
window_attention_mask=window_attention_mask,
window_position_ids=window_position_ids,
prefix_cache=prefix_cache,
mask_id=self.config.mask_token_id,
block_length=block_length,
denoising_steps=max(1, int(getattr(self.config, "gap_rollout_steps", block_length))),
temperature=sample_temperature,
top_k=sample_top_k,
top_p=sample_top_p,
rollout_strategy=rollout_strategy,
confidence_threshold=rollout_confidence_threshold,
)
global_positions = torch.arange(window_token_start, window_token_end, device=device).unsqueeze(0)
rolling_end = (block_idx + 1 - exclude_frontier_blocks) * block_length
rolling_start = max(prefill_blocks * block_length, rolling_end - window_blocks * block_length)
candidate_mask = (
global_positions.ge(rolling_start)
& global_positions.lt(rolling_end)
& window_inputs.ne(self.config.mask_token_id)
& active_rows.unsqueeze(1)
)
for row_idx, prompt_length in enumerate(prompt_lengths):
candidate_mask[row_idx] &= global_positions[0].ge(prompt_length + max(0, prefix_guard_tokens))
if tail_guard_blocks > 0:
tail_guard_start = window_token_end - tail_guard_blocks * block_length
candidate_mask &= global_positions.lt(tail_guard_start)
generated_blocks = (block_idx - prefill_blocks) + 1
remask_active = (generated_blocks - 1) % remask_interval_blocks == 0
if enable_remask_actions and remask_active and candidate_mask.any():
score_attention_mask = self._to_gap_block_mask(window_attention_mask) if self.model.training and not strict_eval_decode else window_attention_mask
outputs = self.model(
window_inputs,
attention_mask=score_attention_mask,
position_ids=window_position_ids,
past_key_values=prefix_cache,
use_cache=True,
store_kv=False,
output_hidden_states=True,
return_dict=True,
)
score_logits = self.lm_head(outputs.last_hidden_state).float()
action_probs = torch.sigmoid(self.gap_remask_head(outputs.last_hidden_state, score_logits)).float().clamp(prob_eps, 1.0 - prob_eps)
sampled_actions = torch.bernoulli(action_probs).to(torch.bool) & candidate_mask
for row_idx in range(batch_size):
row_candidates = torch.nonzero(candidate_mask[row_idx], as_tuple=False).flatten()
if row_candidates.numel() == 0:
continue
row_actions = torch.nonzero(sampled_actions[row_idx], as_tuple=False).flatten()
if row_actions.numel() > max_remask_candidates:
keep = torch.topk(action_probs[row_idx, row_actions], k=max_remask_candidates, sorted=False).indices
new_row = torch.zeros_like(sampled_actions[row_idx])
new_row[row_actions[keep]] = True
sampled_actions[row_idx] = new_row
p = action_probs[row_idx, row_candidates]
a = sampled_actions[row_idx, row_candidates].to(p.dtype)
logprob_sums[row_idx] += (a * p.log() + (1.0 - a) * (1.0 - p).log()).sum()
entropy_sums[row_idx] += (-(p * p.log() + (1.0 - p) * (1.0 - p).log())).sum()
candidate_counts[row_idx] += float(row_candidates.numel())
if sampled_actions.any():
window_inputs[sampled_actions] = self.config.mask_token_id
global_action_positions = global_positions.expand(batch_size, -1)[sampled_actions]
global_action_rows = torch.nonzero(sampled_actions, as_tuple=False)[:, 0]
valid_action_mask = global_action_positions.lt(action_full_flat.shape[1])
if valid_action_mask.any():
action_full_flat[global_action_rows[valid_action_mask], global_action_positions[valid_action_mask]] = True
remask_counts += sampled_actions.to(torch.float32).sum(dim=1)
window_inputs = self._decode_gap_eval_window(
window_inputs=window_inputs,
window_attention_mask=window_attention_mask,
window_position_ids=window_position_ids,
prefix_cache=prefix_cache,
mask_id=self.config.mask_token_id,
block_length=block_length,
denoising_steps=max(1, int(getattr(self.config, "gap_rollout_steps", block_length))),
temperature=sample_temperature,
top_k=sample_top_k,
top_p=sample_top_p,
rollout_strategy=rollout_strategy,
confidence_threshold=rollout_confidence_threshold,
)
window_inputs[~active_rows] = frozen_window_inputs[~active_rows]
x[:, window_slice] = window_inputs
for row_idx in torch.nonzero(active_rows, as_tuple=False).flatten().tolist():
generated_prefix = x[row_idx, prompt_lengths[row_idx]:min(valid_lengths[row_idx], window_token_end)]
if self._should_stop_gap_eval_rollout(generated_prefix):
eos_token_id = getattr(self.config, "eos_token_id", None)
if isinstance(eos_token_id, (list, tuple)):
eos_fill = int(eos_token_id[0])
elif eos_token_id is None:
eos_fill = self.config.mask_token_id
else:
eos_fill = int(eos_token_id)
if window_token_end < valid_lengths[row_idx]:
x[row_idx, window_token_end:valid_lengths[row_idx]] = eos_fill
stop_hits[row_idx] = True
finished[row_idx] = True
finished |= row_num_blocks.le(block_idx + 1)
terminal = x.clone()
eval_target_mask = torch.zeros_like(terminal, dtype=torch.bool)
for row_idx, valid_length in enumerate(valid_lengths):
prompt_length = min(prompt_lengths[row_idx], valid_length)
if valid_length > prompt_length:
eval_target_mask[row_idx, prompt_length:valid_length] = True
length_cap_hits = ~stop_hits
logprob = (logprob_sums / candidate_counts.clamp_min(1.0)).view(num_samples, base_batch_size)
entropy = (entropy_sums / candidate_counts.clamp_min(1.0)).view(num_samples, base_batch_size).mean(dim=0)
remask_rate = (remask_counts / candidate_counts.clamp_min(1.0)).view(num_samples, base_batch_size)
action_full = action_full_flat.view(num_samples, base_batch_size, -1)
if restore_model_training:
self.model.train()
return (
terminal,
eval_target_mask,
logprob,
entropy,
remask_rate,
stop_hits.view(num_samples, base_batch_size),
length_cap_hits.view(num_samples, base_batch_size),
action_full,
)
@torch.no_grad()
def _compute_gap_masked_proposals(
self,
clean_input_ids: torch.LongTensor,
noisy_input_ids: torch.LongTensor,
position_ids: torch.LongTensor,
masked_indices: torch.BoolTensor,
num_tokens,
sample_temperature: float = 0.0,
sample_top_k: int = 0,
sample_top_p: float = 1.0,
) -> tuple[torch.LongTensor, torch.FloatTensor]:
concat_inputs_ids, concat_position_ids, flex_attention_mask_3d, logits_to_keep_half, logits_to_keep, _ = self.build_bd_training_inputs(
inputs_ids=clean_input_ids,
noisy_inputs_ids=noisy_input_ids,
position_ids=position_ids,
logits_to_keep_half=masked_indices,
num_tokens=num_tokens,
)
outputs = self.model(
input_ids=concat_inputs_ids,
attention_mask=flex_attention_mask_3d,
position_ids=concat_position_ids,
output_attentions=False,
output_hidden_states=False,
return_dict=True,
)
hidden_states = outputs.last_hidden_state[logits_to_keep].contiguous()
proposal_logits = self.lm_head(hidden_states).float()
proposal_ids = self._sample_from_logits(
logits=proposal_logits,
temperature=sample_temperature,
top_k=sample_top_k,
top_p=sample_top_p,
)
proposal_probs = proposal_logits.softmax(dim=-1).gather(-1, proposal_ids.unsqueeze(-1)).squeeze(-1)
proposal_ids_full = torch.full_like(noisy_input_ids, self.config.mask_token_id)
proposal_scores_full = torch.full(
noisy_input_ids.shape,
float("-inf"),
dtype=proposal_probs.dtype,
device=proposal_probs.device,
)
proposal_ids_full[masked_indices] = proposal_ids
proposal_scores_full[masked_indices] = proposal_probs
return proposal_ids_full, proposal_scores_full
@torch.no_grad()
def _rollout_gap_state_to_terminal(
self,
clean_input_ids: torch.LongTensor,
noisy_input_ids: torch.LongTensor,
labels: torch.LongTensor,
position_ids: torch.LongTensor,
num_tokens,
start_stage: int = 0,
rollout_strategy: Optional[str] = None,
rollout_confidence_threshold: Optional[float] = None,
rollout_scope: Optional[str] = None,
sample_temperature: float = 0.0,
sample_top_k: int = 0,
sample_top_p: float = 1.0,
) -> torch.LongTensor:
rollout_strategy = rollout_strategy or getattr(self.config, "gap_rollout_strategy", "low_confidence_dynamic")
rollout_confidence_threshold = float(
rollout_confidence_threshold
if rollout_confidence_threshold is not None
else getattr(self.config, "gap_rollout_confidence_threshold", 0.95)
)
_ = rollout_scope or getattr(self.config, "gap_grpo_terminal_rollout_scope", None) or getattr(self.config, "gap_rollout_scope", "all")
terminal_input_ids = noisy_input_ids.clone()
rollout_groups: dict[tuple[int, int], list[int]] = {}
valid_lengths: list[int] = []
prompt_lengths: list[int] = []
block_length = int(self.config.block_size)
for row_idx in range(terminal_input_ids.shape[0]):
valid_length = self._resolve_gap_valid_length(num_tokens[row_idx])
prompt_length = self._compute_gap_answer_start(labels[row_idx], valid_length)
valid_lengths.append(valid_length)
prompt_lengths.append(prompt_length)
total_length = int(math.ceil(valid_length / max(1, block_length)) * block_length) if valid_length > 0 else 0
group_key = (prompt_length // max(1, block_length), total_length)
rollout_groups.setdefault(group_key, []).append(row_idx)
was_base_training = self.model.training
if was_base_training:
self.model.eval()
try:
for (_, _), row_indices in rollout_groups.items():
group_clean = clean_input_ids[row_indices]
group_noisy = terminal_input_ids[row_indices]
group_labels = labels[row_indices]
group_positions = position_ids[row_indices]
group_valid_lengths = [valid_lengths[idx] for idx in row_indices]
group_prompt_lengths = [prompt_lengths[idx] for idx in row_indices]
group_terminal = self._rollout_gap_group_to_terminal_eval_style(
clean_input_ids=group_clean,
noisy_input_ids=group_noisy,
labels=group_labels,
position_ids=group_positions,
valid_lengths=group_valid_lengths,
prompt_lengths=group_prompt_lengths,
rollout_strategy=rollout_strategy,
rollout_confidence_threshold=rollout_confidence_threshold,
sample_temperature=sample_temperature,
sample_top_k=sample_top_k,
sample_top_p=sample_top_p,
)
terminal_input_ids[row_indices] = group_terminal
finally:
if was_base_training:
self.model.train()
return terminal_input_ids
@torch.no_grad()
def _compute_gap_terminal_answer_rewards(
self,
clean_input_ids: torch.LongTensor,
terminal_input_ids: torch.LongTensor,
target_scope_mask: torch.BoolTensor,
pred_scope_mask: Optional[torch.BoolTensor] = None,
) -> torch.FloatTensor:
if target_scope_mask.dim() == 1:
target_scope_mask = target_scope_mask.unsqueeze(0)
if pred_scope_mask is None:
pred_scope_mask = target_scope_mask
elif pred_scope_mask.dim() == 1:
pred_scope_mask = pred_scope_mask.unsqueeze(0)
if clean_input_ids.dim() == 1:
clean_input_ids = clean_input_ids.unsqueeze(0)
if terminal_input_ids.dim() == 1:
terminal_input_ids = terminal_input_ids.unsqueeze(0)
if clean_input_ids.shape[0] == 1 and terminal_input_ids.shape[0] > 1:
clean_input_ids = clean_input_ids.expand(terminal_input_ids.shape[0], -1)
if target_scope_mask.shape[0] == 1 and terminal_input_ids.shape[0] > 1:
target_scope_mask = target_scope_mask.expand(terminal_input_ids.shape[0], -1)
if pred_scope_mask.shape[0] == 1 and terminal_input_ids.shape[0] > 1:
pred_scope_mask = pred_scope_mask.expand(terminal_input_ids.shape[0], -1)
rewards = torch.zeros(terminal_input_ids.shape[0], dtype=torch.float32, device=terminal_input_ids.device)
for row_idx in range(terminal_input_ids.shape[0]):
gold_text = self._decode_gap_response_tokens(clean_input_ids[row_idx][target_scope_mask[row_idx]])
pred_text = self._decode_gap_response_tokens(terminal_input_ids[row_idx][pred_scope_mask[row_idx]])
rewards[row_idx] = 1.0 if self._score_gap_answer_with_opencompass(pred_text, gold_text) else 0.0
return rewards
def _compute_gap_sequence_logprob_means(
self,
model,
input_ids: torch.LongTensor,
position_ids: torch.LongTensor,
target_scope_mask: torch.BoolTensor,
require_grad: bool = False,
chunk_size: int = 0,
) -> torch.FloatTensor:
if target_scope_mask.dim() == 1:
target_scope_mask = target_scope_mask.unsqueeze(0)
if input_ids.dim() == 1:
input_ids = input_ids.unsqueeze(0)
if position_ids.dim() == 1:
position_ids = position_ids.unsqueeze(0)
position_ids = modify_padded_position_ids_2d(position_ids)
chunk_size = int(chunk_size or input_ids.shape[0] or 1)
def _score_chunk(chunk_input_ids, chunk_position_ids, chunk_target_scope_mask):
num_tokens = calculate_token_nums(chunk_position_ids)
masked_indices = chunk_target_scope_mask.to(dtype=torch.bool)
concat_inputs_ids, concat_position_ids, flex_attention_mask_3d, _, logits_to_keep, _ = model.build_bd_training_inputs(
inputs_ids=chunk_input_ids,
noisy_inputs_ids=chunk_input_ids,
position_ids=chunk_position_ids,
logits_to_keep_half=masked_indices,
num_tokens=num_tokens,
)
was_training = model.training
model.train()
try:
outputs = model.model(
input_ids=concat_inputs_ids,
attention_mask=flex_attention_mask_3d,
position_ids=concat_position_ids,
output_attentions=False,
output_hidden_states=False,
return_dict=True,
)
finally:
if not was_training:
model.eval()
hidden_states = outputs.last_hidden_state[logits_to_keep].contiguous()
logits = model.lm_head(hidden_states).float()
target_ids = chunk_input_ids[masked_indices]
target_batch = torch.nonzero(masked_indices, as_tuple=False)[:, 0]
token_logprobs = logits.log_softmax(dim=-1).gather(-1, target_ids.unsqueeze(-1)).squeeze(-1)
batch_sums = torch.zeros(chunk_input_ids.shape[0], dtype=token_logprobs.dtype, device=token_logprobs.device)
batch_counts = torch.zeros_like(batch_sums)
batch_sums.scatter_add_(0, target_batch, token_logprobs)
batch_counts.scatter_add_(0, target_batch, torch.ones_like(token_logprobs))
return batch_sums / batch_counts.clamp_min(1.0)
def _forward():
if input_ids.shape[0] <= chunk_size:
return _score_chunk(input_ids, position_ids, target_scope_mask)
outputs = []
for start in range(0, input_ids.shape[0], chunk_size):
end = min(start + chunk_size, input_ids.shape[0])
outputs.append(
_score_chunk(
input_ids[start:end],
position_ids[start:end],
target_scope_mask[start:end],
)
)
return torch.cat(outputs, dim=0)
if require_grad:
return _forward()
with torch.no_grad():
return _forward()
@staticmethod
def _extend_gap_position_ids_to_length(
position_ids: torch.LongTensor,
target_length: int,
) -> torch.LongTensor:
if position_ids.dim() == 1:
position_ids = position_ids.unsqueeze(0)
target_length = int(target_length)
if position_ids.shape[1] == target_length:
return position_ids
batch_size, source_length = position_ids.shape
output = torch.zeros(
(batch_size, target_length),
dtype=position_ids.dtype,
device=position_ids.device,
)
copy_length = min(source_length, target_length)
if copy_length > 0:
output[:, :copy_length] = position_ids[:, :copy_length]
if target_length > copy_length:
if copy_length > 0:
start_positions = output[:, copy_length - 1] + 1
else:
start_positions = torch.zeros(batch_size, dtype=position_ids.dtype, device=position_ids.device)
offsets = torch.arange(
target_length - copy_length,
dtype=position_ids.dtype,
device=position_ids.device,
).unsqueeze(0)
output[:, copy_length:] = start_positions.unsqueeze(1) + offsets
return output
@staticmethod
def _compute_gap_group_advantages(
rewards: torch.FloatTensor,
baseline_reward: Optional[torch.FloatTensor],
advantage_eps: float,
) -> tuple[torch.FloatTensor, torch.FloatTensor]:
if baseline_reward is not None:
centered_rewards = rewards - baseline_reward.unsqueeze(0)
else:
centered_rewards = rewards
reward_mean = centered_rewards.mean(dim=0, keepdim=True)
reward_std = centered_rewards.std(dim=0, keepdim=True)
advantages = (centered_rewards - reward_mean) / reward_std.clamp_min(advantage_eps)
advantages = torch.where(reward_std.gt(advantage_eps), advantages, torch.zeros_like(advantages))
return advantages, reward_std
def _compute_gap_current_remask_action_logprob(
self,
remask_logits: torch.FloatTensor,
masked_indices: torch.BoolTensor,
full_candidate_mask: torch.BoolTensor,
sampled_full: torch.BoolTensor,
sample_prob_eps: float,
) -> tuple[torch.FloatTensor, torch.FloatTensor]:
candidate_mask_flat = full_candidate_mask[masked_indices]
batch_size = full_candidate_mask.shape[0]
candidate_count = int(full_candidate_mask.sum().item())
if candidate_count <= 0:
num_samples = sampled_full.shape[0]
zero = remask_logits.sum() * 0.0
return zero.expand(num_samples, batch_size), zero.expand(batch_size)
flat_probs = torch.sigmoid(remask_logits[candidate_mask_flat]).clamp(
min=sample_prob_eps,
max=1.0 - sample_prob_eps,
)
batch_ids = torch.nonzero(full_candidate_mask, as_tuple=False)[:, 0]
candidate_counts_per_batch = torch.zeros(batch_size, dtype=flat_probs.dtype, device=flat_probs.device)
candidate_counts_per_batch.scatter_add_(
0,
batch_ids,
torch.ones_like(batch_ids, dtype=flat_probs.dtype),
)
candidate_counts_per_batch = candidate_counts_per_batch.clamp_min(1.0)
num_samples = sampled_full.shape[0]
aligned_actions = torch.zeros(
(num_samples, batch_size, full_candidate_mask.shape[1]),
dtype=torch.bool,
device=full_candidate_mask.device,
)
copy_len = min(full_candidate_mask.shape[1], sampled_full.shape[-1])
if copy_len > 0:
aligned_actions[:, :, :copy_len] = sampled_full[:, :, :copy_len]
sampled_flat = aligned_actions[:, full_candidate_mask]
sample_logprob = (
sampled_flat.to(flat_probs.dtype) * flat_probs.log().unsqueeze(0)
+ (~sampled_flat).to(flat_probs.dtype) * (1.0 - flat_probs).log().unsqueeze(0)
)
sample_entropy = -(
flat_probs * flat_probs.log() + (1.0 - flat_probs) * (1.0 - flat_probs).log()
)
logprob_per_batch = torch.zeros((num_samples, batch_size), dtype=sample_logprob.dtype, device=sample_logprob.device)
entropy_per_batch = torch.zeros(batch_size, dtype=sample_entropy.dtype, device=sample_entropy.device)
logprob_per_batch.scatter_add_(1, batch_ids.unsqueeze(0).expand(num_samples, -1), sample_logprob)
entropy_per_batch.scatter_add_(0, batch_ids, sample_entropy)
logprob_per_batch = logprob_per_batch / candidate_counts_per_batch.unsqueeze(0)
entropy_per_batch = entropy_per_batch / candidate_counts_per_batch
return logprob_per_batch, entropy_per_batch
def _compute_gap_grpo_loss(
self,
clean_input_ids: torch.LongTensor,
labels: torch.LongTensor,
position_ids: torch.LongTensor,
num_tokens,
remask_logits: torch.FloatTensor,
grpo_hidden_states: torch.FloatTensor,
gap_outputs,
masked_indices: torch.BoolTensor,
rollout_strategy: str,
rollout_confidence_threshold: float,
target_scope_mask: torch.BoolTensor,
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
grpo_weight = float(getattr(self.config, "gap_grpo_loss_weight", 0.0) or 0.0)
num_samples = int(getattr(self.config, "gap_grpo_num_samples", 0) or 0)
if grpo_weight <= 0.0 or num_samples <= 0:
zero = remask_logits.sum() * 0.0
return zero, {}
use_eval_rollout_actions = _gap_env_flag("SDAR_GAP_GRPO_USE_EVAL_ROLLOUT_ACTIONS", False)
if use_eval_rollout_actions:
batch_size = clean_input_ids.shape[0]
entropy_coef = float(getattr(self.config, "gap_grpo_entropy_coef", 0.0) or 0.0)
use_baseline_branch = bool(getattr(self.config, "gap_grpo_use_baseline_branch", True))
terminal_weight = float(getattr(self.config, "gap_grpo_terminal_reward_weight", 1.0) or 1.0)
format_weight = float(getattr(self.config, "gap_grpo_format_reward_weight", 0.0) or 0.0)
remask_penalty = float(getattr(self.config, "gap_grpo_remask_penalty", 0.0) or 0.0)
advantage_eps = float(getattr(self.config, "gap_grpo_advantage_eps", 1e-4) or 1e-4)
rollout_temperature = float(getattr(self.config, "gap_grpo_rollout_temperature", 0.0) or 0.0)
rollout_top_k = int(getattr(self.config, "gap_grpo_rollout_top_k", 0) or 0)
rollout_top_p = float(getattr(self.config, "gap_grpo_rollout_top_p", 1.0) or 1.0)
terminal_rollout_strategy = getattr(self.config, "gap_grpo_terminal_rollout_strategy", None) or rollout_strategy
valid_lengths = [self._resolve_gap_valid_length(num_tokens[row_idx]) for row_idx in range(batch_size)]
prompt_lengths = [
self._compute_gap_answer_start(labels[row_idx], valid_lengths[row_idx])
for row_idx in range(batch_size)
]
with torch.no_grad():
if use_baseline_branch:
(
baseline_terminal,
baseline_eval_target_mask,
_baseline_logprob,
_baseline_entropy,
_baseline_remask_rate,
baseline_stop_hit,
baseline_length_cap_hit,
_baseline_sampled_full,
) = self._rollout_gap_group_eval_with_sampled_remask(
noisy_input_ids=gap_outputs.z_accept,
labels=labels,
position_ids=position_ids,
valid_lengths=valid_lengths,
prompt_lengths=prompt_lengths,
num_samples=1,
rollout_strategy=terminal_rollout_strategy,
rollout_confidence_threshold=rollout_confidence_threshold,
sample_temperature=rollout_temperature,
sample_top_k=rollout_top_k,
sample_top_p=rollout_top_p,
enable_remask_actions=False,
)
baseline_terminal_reward = self._compute_gap_terminal_answer_rewards(
clean_input_ids=clean_input_ids,
terminal_input_ids=baseline_terminal,
target_scope_mask=target_scope_mask,
pred_scope_mask=baseline_eval_target_mask,
)
baseline_format_reward = torch.zeros_like(baseline_terminal_reward)
if format_weight > 0.0:
for row_idx in range(batch_size):
pred_text = self._decode_gap_response_tokens(baseline_terminal[row_idx][baseline_eval_target_mask[row_idx]])
baseline_format_reward[row_idx] = 1.0 if self._extract_gap_boxed_answer(pred_text) else 0.0
else:
baseline_terminal = None
baseline_eval_target_mask = None
baseline_terminal_reward = None
baseline_format_reward = None
baseline_stop_hit = None
baseline_length_cap_hit = None
(
sampled_terminal,
sampled_eval_target_mask,
logprob_per_batch,
entropy_per_batch,
remask_rate,
sampled_stop_hit,
sampled_length_cap_hit,
sampled_full,
) = (
self._rollout_gap_group_eval_with_sampled_remask(
noisy_input_ids=gap_outputs.z_accept,
labels=labels,
position_ids=position_ids,
valid_lengths=valid_lengths,
prompt_lengths=prompt_lengths,
num_samples=num_samples,
rollout_strategy=terminal_rollout_strategy,
rollout_confidence_threshold=rollout_confidence_threshold,
sample_temperature=rollout_temperature,
sample_top_k=rollout_top_k,
sample_top_p=rollout_top_p,
enable_remask_actions=True,
)
)
clean_flat = clean_input_ids.unsqueeze(0).repeat(num_samples, 1, 1).view(num_samples * batch_size, -1)
target_scope_flat = target_scope_mask.unsqueeze(0).repeat(num_samples, 1, 1).view(num_samples * batch_size, -1)
sampled_terminal_reward = self._compute_gap_terminal_answer_rewards(
clean_input_ids=clean_flat,
terminal_input_ids=sampled_terminal,
target_scope_mask=target_scope_flat,
pred_scope_mask=sampled_eval_target_mask,
).view(num_samples, batch_size)
sampled_format_reward = torch.zeros_like(sampled_terminal_reward)
if format_weight > 0.0:
for branch_idx in range(num_samples):
for row_idx in range(batch_size):
flat_idx = branch_idx * batch_size + row_idx
pred_text = self._decode_gap_response_tokens(sampled_terminal[flat_idx][sampled_eval_target_mask[flat_idx]])
sampled_format_reward[branch_idx, row_idx] = 1.0 if self._extract_gap_boxed_answer(pred_text) else 0.0
rewards = terminal_weight * sampled_terminal_reward + format_weight * sampled_format_reward - remask_penalty * remask_rate
if use_baseline_branch:
assert baseline_terminal_reward is not None and baseline_format_reward is not None
baseline_reward = terminal_weight * baseline_terminal_reward + format_weight * baseline_format_reward
else:
baseline_reward = None
kl_coef = float(getattr(self.config, "gap_grpo_kl_coef", 0.0) or 0.0)
reference_model = self._get_gap_reference_model() if kl_coef > 0.0 else None
if reference_model is not None:
position_flat = position_ids.unsqueeze(0).repeat(num_samples, 1, 1).view(num_samples * batch_size, -1)
position_flat = self._extend_gap_position_ids_to_length(position_flat, sampled_terminal.shape[1])
kl_chunk_size = 1
actor_logprob = self._compute_gap_sequence_logprob_means(
model=self,
input_ids=sampled_terminal,
position_ids=position_flat,
target_scope_mask=sampled_eval_target_mask,
require_grad=False,
chunk_size=kl_chunk_size,
).view(num_samples, batch_size)
reference_logprob = self._compute_gap_sequence_logprob_means(
model=reference_model,
input_ids=sampled_terminal,
position_ids=position_flat,
target_scope_mask=sampled_eval_target_mask,
require_grad=False,
chunk_size=kl_chunk_size,
).view(num_samples, batch_size)
sampled_kl = (actor_logprob - reference_logprob).clamp_min(0.0)
rewards = rewards - kl_coef * sampled_kl.detach()
else:
actor_logprob = None
reference_logprob = None
sampled_kl = None
reward_gain = rewards - baseline_reward.unsqueeze(0) if baseline_reward is not None else rewards
advantages, reward_std = self._compute_gap_group_advantages(
rewards=rewards,
baseline_reward=baseline_reward,
advantage_eps=advantage_eps,
)
current_logprob_per_batch, current_entropy_per_batch = self._compute_gap_current_remask_action_logprob(
remask_logits=remask_logits,
masked_indices=masked_indices,
full_candidate_mask=gap_outputs.full_candidate_mask,
sampled_full=sampled_full,
sample_prob_eps=float(getattr(self.config, "gap_grpo_sample_prob_eps", 1e-4) or 1e-4),
)
valid_advantage_mask = advantages.ne(0.0)
if valid_advantage_mask.any():
policy_terms = -(advantages.detach() * current_logprob_per_batch)
policy_loss = policy_terms[valid_advantage_mask].mean()
entropy_loss = current_entropy_per_batch.mean()
else:
policy_loss = current_logprob_per_batch.sum() * 0.0
entropy_loss = current_entropy_per_batch.sum() * 0.0
entropy_bonus = current_entropy_per_batch.mean()
total_loss = grpo_weight * (policy_loss - entropy_coef * entropy_loss)
total_loss = total_loss + remask_logits.sum() * 0.0 + self.gap_value_head(grpo_hidden_states[:1]).sum() * 0.0
if use_baseline_branch:
assert baseline_eval_target_mask is not None and baseline_terminal is not None
baseline_target_counts = baseline_eval_target_mask.to(torch.float32).sum(dim=-1).clamp_min(1.0)
baseline_remaining_mask_rate = (
baseline_terminal.eq(self.config.mask_token_id) & baseline_eval_target_mask
).to(torch.float32).sum(dim=-1) / baseline_target_counts
else:
baseline_remaining_mask_rate = None
sampled_target_counts = sampled_eval_target_mask.to(torch.float32).sum(dim=-1).clamp_min(1.0)
sampled_remaining_mask_rate = (
sampled_terminal.eq(self.config.mask_token_id) & sampled_eval_target_mask
).to(torch.float32).sum(dim=-1).view(num_samples, batch_size) / sampled_target_counts.view(num_samples, batch_size)
action_candidate_mask = sampled_full.any(dim=0)
self._maybe_capture_gap_branch_debug(
clean_input_ids=clean_input_ids,
target_scope_mask=target_scope_mask,
shared_state_input_ids=gap_outputs.z_accept,
baseline_terminal=baseline_terminal,
baseline_terminal_reward=baseline_terminal_reward,
baseline_reward=baseline_reward,
sampled_terminal=sampled_terminal,
target_scope_flat=target_scope_flat,
sampled_terminal_reward=sampled_terminal_reward,
rewards=rewards,
reward_gain=reward_gain,
remask_rate=remask_rate,
baseline_remaining_mask_rate=baseline_remaining_mask_rate,
sampled_remaining_mask_rate=sampled_remaining_mask_rate,
sampled_full=sampled_full,
full_candidate_mask=action_candidate_mask,
baseline_debug_mask=baseline_eval_target_mask,
sampled_debug_mask=sampled_eval_target_mask,
baseline_stop_hit=baseline_stop_hit.view(batch_size) if baseline_stop_hit is not None else None,
sampled_stop_hit=sampled_stop_hit,
baseline_length_cap_hit=baseline_length_cap_hit.view(batch_size) if baseline_length_cap_hit is not None else None,
sampled_length_cap_hit=sampled_length_cap_hit,
)
gain_positive_rate = (reward_gain > 0).to(torch.float32).mean()
gain_negative_rate = (reward_gain < 0).to(torch.float32).mean()
branch_positive_rate = (sampled_terminal_reward > 0).to(torch.float32).mean()
zero_metric = rewards.new_tensor(0.0)
metrics = {
"grpo_reward": rewards.mean().detach(),
"grpo_terminal_reward": sampled_terminal_reward.mean().detach(),
"grpo_reward_gain": reward_gain.mean().detach(),
"grpo_reward_std": reward_std.mean().detach(),
"grpo_group_advantage_abs": advantages.abs().mean().detach(),
"grpo_value": grpo_hidden_states.new_tensor(0.0),
"grpo_value_advantage_abs": reward_gain.abs().mean().detach(),
"grpo_value_loss": reward_gain.pow(2).mean().detach(),
"grpo_entropy": entropy_bonus.detach(),
"grpo_policy_active_rate": valid_advantage_mask.to(torch.float32).mean().detach(),
"grpo_rollout_logprob": logprob_per_batch.mean().detach(),
"grpo_current_logprob": current_logprob_per_batch.mean().detach(),
"grpo_branch_correct_rate": sampled_terminal_reward.mean().detach(),
"grpo_branch_positive_rate": branch_positive_rate.detach(),
"grpo_branch_negative_rate": (1.0 - branch_positive_rate).detach(),
"grpo_baseline_correct_rate": ((baseline_terminal_reward > 0).to(torch.float32).mean().detach() if baseline_terminal_reward is not None else zero_metric),
"grpo_baseline_reward": (baseline_reward.mean().detach() if baseline_reward is not None else zero_metric),
"grpo_baseline_remaining_mask_rate": (baseline_remaining_mask_rate.mean().detach() if baseline_remaining_mask_rate is not None else zero_metric),
"grpo_sampled_remaining_mask_rate": sampled_remaining_mask_rate.mean().detach(),
"grpo_sampled_remaining_mask_rate_max": sampled_remaining_mask_rate.max().detach(),
"grpo_gain_positive_rate": gain_positive_rate.detach(),
"grpo_gain_negative_rate": gain_negative_rate.detach(),
"grpo_gain_tie_rate": (1.0 - gain_positive_rate - gain_negative_rate).detach(),
"grpo_any_win_rate": (reward_gain > 0).any(dim=0).to(torch.float32).mean().detach(),
"grpo_any_lose_rate": (reward_gain < 0).any(dim=0).to(torch.float32).mean().detach(),
"grpo_loss": total_loss.detach(),
}
if sampled_kl is not None:
metrics["grpo_kl"] = sampled_kl.mean().detach()
metrics["grpo_actor_logp"] = actor_logprob.mean().detach()
metrics["grpo_ref_logp"] = reference_logprob.mean().detach()
return total_loss, metrics
full_candidate_mask = gap_outputs.full_candidate_mask
candidate_mask_flat = full_candidate_mask[masked_indices]
candidate_count = int(full_candidate_mask.sum().item())
if candidate_count <= 0:
zero = remask_logits.sum() * 0.0 + self.gap_value_head(grpo_hidden_states[:1]).sum() * 0.0
return zero, {}
timing_interval = _gap_env_int("SDAR_GAP_GRPO_TIMING_INTERVAL", 0)
debug_step = int(getattr(self, "_gap_debug_global_step", -1))
should_log_timing = timing_interval > 0 and _gap_is_rank0() and debug_step >= 0 and (debug_step % timing_interval == 0)
timing_marks = {}
def _mark(name: str) -> None:
if should_log_timing:
timing_marks[name] = time.perf_counter()
sample_prob_eps = float(getattr(self.config, "gap_grpo_sample_prob_eps", 1e-4) or 1e-4)
entropy_coef = float(getattr(self.config, "gap_grpo_entropy_coef", 0.0) or 0.0)
terminal_weight = float(getattr(self.config, "gap_grpo_terminal_reward_weight", 1.0) or 1.0)
format_weight = float(getattr(self.config, "gap_grpo_format_reward_weight", 0.0) or 0.0)
remask_penalty = float(getattr(self.config, "gap_grpo_remask_penalty", 0.0) or 0.0)
advantage_eps = float(getattr(self.config, "gap_grpo_advantage_eps", 1e-4) or 1e-4)
value_loss_weight = float(getattr(self.config, "gap_grpo_value_loss_weight", 0.0) or 0.0)
value_baseline_weight = float(getattr(self.config, "gap_grpo_value_baseline_weight", 0.0) or 0.0)
use_baseline_branch = bool(getattr(self.config, "gap_grpo_use_baseline_branch", True))
rollout_temperature = float(getattr(self.config, "gap_grpo_rollout_temperature", 0.0) or 0.0)
rollout_top_k = int(getattr(self.config, "gap_grpo_rollout_top_k", 0) or 0)
rollout_top_p = float(getattr(self.config, "gap_grpo_rollout_top_p", 1.0) or 1.0)
kl_coef = float(getattr(self.config, "gap_grpo_kl_coef", 0.0) or 0.0)
reference_model = self._get_gap_reference_model() if kl_coef > 0.0 else None
terminal_rollout_strategy = (
getattr(self.config, "gap_grpo_terminal_rollout_strategy", None)
or rollout_strategy
)
terminal_rollout_scope = (
getattr(self.config, "gap_grpo_terminal_rollout_scope", None)
or getattr(self.config, "gap_rollout_scope", "all")
)
flat_probs = torch.sigmoid(remask_logits[candidate_mask_flat]).clamp(min=sample_prob_eps, max=1.0 - sample_prob_eps)
batch_ids = torch.nonzero(full_candidate_mask, as_tuple=False)[:, 0]
batch_size = full_candidate_mask.shape[0]
candidate_counts_per_batch = torch.zeros(batch_size, dtype=torch.float32, device=flat_probs.device)
candidate_counts_per_batch.scatter_add_(
0,
batch_ids,
torch.ones_like(batch_ids, dtype=torch.float32),
)
candidate_counts_per_batch = candidate_counts_per_batch.clamp_min(1.0)
flat_values = self.gap_value_head(grpo_hidden_states[candidate_mask_flat]).squeeze(-1).float()
value_per_batch = torch.zeros(batch_size, dtype=flat_values.dtype, device=flat_values.device)
value_per_batch.scatter_add_(0, batch_ids, flat_values)
value_per_batch = value_per_batch / candidate_counts_per_batch
if use_baseline_branch:
_mark("baseline_start")
with torch.no_grad():
baseline_terminal = self._rollout_gap_state_to_terminal(
clean_input_ids=clean_input_ids,
noisy_input_ids=gap_outputs.z_accept,
labels=labels,
position_ids=position_ids,
num_tokens=num_tokens,
start_stage=0,
rollout_strategy=terminal_rollout_strategy,
rollout_confidence_threshold=rollout_confidence_threshold,
rollout_scope=terminal_rollout_scope,
sample_temperature=rollout_temperature,
sample_top_k=rollout_top_k,
sample_top_p=rollout_top_p,
)
baseline_terminal_reward = self._compute_gap_terminal_answer_rewards(
clean_input_ids=clean_input_ids,
terminal_input_ids=baseline_terminal,
target_scope_mask=target_scope_mask,
)
if format_weight > 0.0:
baseline_format_reward = torch.zeros_like(baseline_terminal_reward)
for row_idx in range(batch_size):
pred_text = self._decode_gap_response_tokens(baseline_terminal[row_idx][target_scope_mask[row_idx]])
baseline_format_reward[row_idx] = 1.0 if self._extract_gap_boxed_answer(pred_text) else 0.0
else:
baseline_format_reward = torch.zeros_like(baseline_terminal_reward)
_mark("baseline_end")
else:
baseline_terminal = None
baseline_terminal_reward = None
baseline_format_reward = None
sampled_flat = torch.bernoulli(flat_probs.unsqueeze(0).expand(num_samples, -1)).to(dtype=torch.bool)
sampled_full = torch.zeros(
(num_samples, batch_size, full_candidate_mask.shape[1]),
dtype=torch.bool,
device=full_candidate_mask.device,
)
sampled_full[:, full_candidate_mask] = sampled_flat
sample_logprob = (
sampled_flat.to(flat_probs.dtype) * flat_probs.log().unsqueeze(0)
+ (~sampled_flat).to(flat_probs.dtype) * (1.0 - flat_probs).log().unsqueeze(0)
)
sample_entropy = -(
flat_probs * flat_probs.log() + (1.0 - flat_probs) * (1.0 - flat_probs).log()
)
logprob_per_batch = torch.zeros((num_samples, batch_size), dtype=sample_logprob.dtype, device=sample_logprob.device)
entropy_per_batch = torch.zeros(batch_size, dtype=sample_entropy.dtype, device=sample_entropy.device)
logprob_per_batch.scatter_add_(1, batch_ids.unsqueeze(0).expand(num_samples, -1), sample_logprob)
entropy_per_batch.scatter_add_(0, batch_ids, sample_entropy)
logprob_per_batch = logprob_per_batch / candidate_counts_per_batch.unsqueeze(0)
entropy_per_batch = entropy_per_batch / candidate_counts_per_batch
_mark("sampled_start")
with torch.no_grad():
sampled_states = gap_outputs.z_accept.unsqueeze(0).repeat(num_samples, 1, 1)
sampled_states[sampled_full] = self.config.mask_token_id
sampled_states_flat = sampled_states.view(num_samples * batch_size, -1)
clean_flat = clean_input_ids.unsqueeze(0).repeat(num_samples, 1, 1).view(num_samples * batch_size, -1)
labels_flat = labels.unsqueeze(0).repeat(num_samples, 1, 1).view(num_samples * batch_size, -1)
position_flat = position_ids.unsqueeze(0).repeat(num_samples, 1, 1).view(num_samples * batch_size, -1)
target_scope_flat = target_scope_mask.unsqueeze(0).repeat(num_samples, 1, 1).view(num_samples * batch_size, -1)
num_tokens_flat = [num_tokens[row_idx % batch_size] for row_idx in range(num_samples * batch_size)]
sampled_terminal = self._rollout_gap_state_to_terminal(
clean_input_ids=clean_flat,
noisy_input_ids=sampled_states_flat,
labels=labels_flat,
position_ids=position_flat,
num_tokens=num_tokens_flat,
start_stage=0,
rollout_strategy=terminal_rollout_strategy,
rollout_confidence_threshold=rollout_confidence_threshold,
rollout_scope=terminal_rollout_scope,
sample_temperature=rollout_temperature,
sample_top_k=rollout_top_k,
sample_top_p=rollout_top_p,
)
sampled_terminal_reward = self._compute_gap_terminal_answer_rewards(
clean_input_ids=clean_flat,
terminal_input_ids=sampled_terminal,
target_scope_mask=target_scope_flat,
).view(num_samples, batch_size)
if format_weight > 0.0:
sampled_format_reward = torch.zeros_like(sampled_terminal_reward)
for branch_idx in range(num_samples):
for row_idx in range(batch_size):
flat_idx = branch_idx * batch_size + row_idx
pred_text = self._decode_gap_response_tokens(sampled_terminal[flat_idx][target_scope_flat[flat_idx]])
sampled_format_reward[branch_idx, row_idx] = 1.0 if self._extract_gap_boxed_answer(pred_text) else 0.0
else:
sampled_format_reward = torch.zeros_like(sampled_terminal_reward)
_mark("sampled_end")
if use_baseline_branch:
assert baseline_terminal is not None
baseline_target_counts = target_scope_mask.to(torch.float32).sum(dim=-1).clamp_min(1.0)
baseline_remaining_mask_rate = (
baseline_terminal.eq(self.config.mask_token_id) & target_scope_mask
).to(torch.float32).sum(dim=-1) / baseline_target_counts
else:
baseline_remaining_mask_rate = None
sampled_target_counts = target_scope_flat.to(torch.float32).sum(dim=-1).clamp_min(1.0)
sampled_remaining_mask_rate = (
sampled_terminal.eq(self.config.mask_token_id) & target_scope_flat
).to(torch.float32).sum(dim=-1).view(num_samples, batch_size) / sampled_target_counts.view(num_samples, batch_size)
_mark("reward_start")
remask_rate = sampled_full.to(torch.float32).sum(dim=-1) / candidate_counts_per_batch.unsqueeze(0)
rewards = (
terminal_weight * sampled_terminal_reward
+ format_weight * sampled_format_reward
- remask_penalty * remask_rate
)
if use_baseline_branch:
assert baseline_terminal_reward is not None and baseline_format_reward is not None
baseline_reward = (
terminal_weight * baseline_terminal_reward
+ format_weight * baseline_format_reward
)
else:
baseline_reward = None
if reference_model is not None:
kl_chunk_size = 1
actor_logprob = self._compute_gap_sequence_logprob_means(
model=self,
input_ids=sampled_terminal,
position_ids=position_flat,
target_scope_mask=target_scope_flat,
require_grad=False,
chunk_size=kl_chunk_size,
).view(num_samples, batch_size)
reference_logprob = self._compute_gap_sequence_logprob_means(
model=reference_model,
input_ids=sampled_terminal,
position_ids=position_flat,
target_scope_mask=target_scope_flat,
require_grad=False,
chunk_size=kl_chunk_size,
).view(num_samples, batch_size)
sampled_kl = (actor_logprob - reference_logprob).clamp_min(0.0)
rewards = rewards - kl_coef * sampled_kl.detach()
else:
actor_logprob = None
reference_logprob = None
sampled_kl = None
reward_gain = rewards - baseline_reward.unsqueeze(0) if baseline_reward is not None else rewards
_mark("reward_end")
group_advantages, reward_std = self._compute_gap_group_advantages(
rewards=rewards,
baseline_reward=baseline_reward,
advantage_eps=advantage_eps,
)
critic_advantages = reward_gain - value_per_batch.detach().unsqueeze(0)
if value_baseline_weight > 0.0:
blend_weight = max(0.0, min(1.0, value_baseline_weight))
advantages = (1.0 - blend_weight) * group_advantages + blend_weight * critic_advantages
else:
advantages = group_advantages
if num_samples == 1:
advantages = critic_advantages if value_baseline_weight > 0.0 else reward_gain
self._maybe_capture_gap_branch_debug(
clean_input_ids=clean_input_ids,
target_scope_mask=target_scope_mask,
shared_state_input_ids=gap_outputs.z_accept,
baseline_terminal=baseline_terminal,
baseline_terminal_reward=baseline_terminal_reward,
baseline_reward=baseline_reward,
sampled_terminal=sampled_terminal,
target_scope_flat=target_scope_flat,
sampled_terminal_reward=sampled_terminal_reward,
rewards=rewards,
reward_gain=reward_gain,
remask_rate=remask_rate,
baseline_remaining_mask_rate=baseline_remaining_mask_rate,
sampled_remaining_mask_rate=sampled_remaining_mask_rate,
sampled_full=sampled_full,
full_candidate_mask=full_candidate_mask,
)
gain_positive_rate = (reward_gain > 0).to(torch.float32).mean()
gain_negative_rate = (reward_gain < 0).to(torch.float32).mean()
gain_tie_rate = 1.0 - gain_positive_rate - gain_negative_rate
branch_positive_rate = (sampled_terminal_reward > 0).to(torch.float32).mean()
branch_negative_rate = 1.0 - branch_positive_rate
baseline_correct_rate = (baseline_terminal_reward > 0).to(torch.float32).mean() if baseline_terminal_reward is not None else rewards.new_tensor(0.0)
any_win_rate = (reward_gain > 0).any(dim=0).to(torch.float32).mean()
any_lose_rate = (reward_gain < 0).any(dim=0).to(torch.float32).mean()
valid_advantage_mask = advantages.ne(0.0)
if valid_advantage_mask.any():
policy_terms = -(advantages.detach() * logprob_per_batch)
policy_loss = policy_terms[valid_advantage_mask].mean()
entropy_loss = entropy_per_batch.mean()
else:
policy_loss = logprob_per_batch.sum() * 0.0
entropy_loss = entropy_per_batch.sum() * 0.0
entropy_bonus = entropy_per_batch.mean()
value_loss = ((value_per_batch.unsqueeze(0) - reward_gain.detach()) ** 2).mean()
total_loss = grpo_weight * (policy_loss - entropy_coef * entropy_loss)
if value_loss_weight > 0.0:
total_loss = total_loss + value_loss_weight * value_loss
else:
total_loss = total_loss + value_loss * 0.0
metrics = {
"grpo_reward": rewards.mean().detach(),
"grpo_terminal_reward": sampled_terminal_reward.mean().detach(),
"grpo_reward_gain": reward_gain.mean().detach(),
"grpo_reward_std": reward_std.mean().detach(),
"grpo_group_advantage_abs": group_advantages.abs().mean().detach(),
"grpo_value": value_per_batch.mean().detach(),
"grpo_value_advantage_abs": critic_advantages.abs().mean().detach(),
"grpo_value_loss": value_loss.detach(),
"grpo_entropy": entropy_bonus.detach(),
"grpo_policy_active_rate": valid_advantage_mask.to(torch.float32).mean().detach(),
"grpo_branch_correct_rate": sampled_terminal_reward.mean().detach(),
"grpo_branch_positive_rate": branch_positive_rate.detach(),
"grpo_branch_negative_rate": branch_negative_rate.detach(),
"grpo_baseline_correct_rate": baseline_correct_rate.detach(),
"grpo_baseline_reward": baseline_reward.mean().detach() if baseline_reward is not None else rewards.new_tensor(0.0),
"grpo_baseline_remaining_mask_rate": baseline_remaining_mask_rate.mean().detach() if baseline_remaining_mask_rate is not None else rewards.new_tensor(0.0),
"grpo_sampled_remaining_mask_rate": sampled_remaining_mask_rate.mean().detach(),
"grpo_sampled_remaining_mask_rate_max": sampled_remaining_mask_rate.max().detach(),
"grpo_gain_positive_rate": gain_positive_rate.detach(),
"grpo_gain_negative_rate": gain_negative_rate.detach(),
"grpo_gain_tie_rate": gain_tie_rate.detach(),
"grpo_any_win_rate": any_win_rate.detach(),
"grpo_any_lose_rate": any_lose_rate.detach(),
"grpo_loss": total_loss.detach(),
}
if sampled_kl is not None:
metrics["grpo_kl"] = sampled_kl.mean().detach()
metrics["grpo_actor_logp"] = actor_logprob.mean().detach()
metrics["grpo_ref_logp"] = reference_logprob.mean().detach()
if should_log_timing:
baseline_sec = max(0.0, timing_marks.get("baseline_end", 0.0) - timing_marks.get("baseline_start", 0.0))
sampled_sec = max(0.0, timing_marks.get("sampled_end", 0.0) - timing_marks.get("sampled_start", 0.0))
reward_sec = max(0.0, timing_marks.get("reward_end", 0.0) - timing_marks.get("reward_start", 0.0))
logger.info(
"[GAP grpo timing] step=%s batch=%s num_samples=%s candidate_tokens=%s baseline_sec=%.2f sampled_sec=%.2f reward_sec=%.2f",
debug_step,
batch_size,
num_samples,
candidate_count,
baseline_sec,
sampled_sec,
reward_sec,
)
return total_loss, metrics
def _compute_gap_sft_ce_anchor(
self,
clean_input_ids: torch.LongTensor,
clean_labels: torch.LongTensor,
clean_position_ids: torch.LongTensor,
) -> torch.Tensor:
clean_position_ids = modify_padded_position_ids_2d(clean_position_ids)
target_mask = clean_labels.ne(-100)
num_tokens = calculate_token_nums(clean_position_ids)
concat_inputs_ids, concat_position_ids, flex_attention_mask_3d, _, logits_to_keep, _ = self.build_bd_training_inputs(
inputs_ids=clean_input_ids,
noisy_inputs_ids=clean_input_ids,
position_ids=clean_position_ids,
logits_to_keep_half=target_mask,
num_tokens=num_tokens,
)
outputs = self.model(
input_ids=concat_inputs_ids,
attention_mask=flex_attention_mask_3d,
position_ids=concat_position_ids,
output_attentions=False,
output_hidden_states=False,
return_dict=True,
)
hidden_states = outputs.last_hidden_state[logits_to_keep].contiguous()
logits = self.lm_head(hidden_states).float()
target_ids = clean_input_ids[target_mask]
return nn.functional.cross_entropy(logits, target_ids, reduction="mean")
def set_puma_streaming_context(self, slot_offset: int, buffer_size: int) -> None:
self._puma_streaming_context = {
"slot_offset": max(int(slot_offset), 0),
"buffer_size": max(int(buffer_size), 1),
}
def reset_puma_streaming_state(self) -> None:
self._puma_streaming_state = None
self._puma_streaming_context = {"slot_offset": 0, "buffer_size": None}
def _ensure_puma_streaming_state(self, buffer_size, inputs_ids, labels, position_ids):
expected_shape = (buffer_size, inputs_ids.shape[1])
state = self._puma_streaming_state
if (
state is None
or tuple(state["clean_input_ids"].shape) != expected_shape
or state["clean_input_ids"].device != inputs_ids.device
or state["clean_input_ids"].dtype != inputs_ids.dtype
):
state = {
"clean_input_ids": torch.zeros(expected_shape, dtype=inputs_ids.dtype, device=inputs_ids.device),
"labels": torch.full(expected_shape, -100, dtype=labels.dtype, device=labels.device),
"position_ids": torch.zeros(expected_shape, dtype=position_ids.dtype, device=position_ids.device),
"noisy_inputs_ids": torch.zeros(expected_shape, dtype=inputs_ids.dtype, device=inputs_ids.device),
"stages": torch.full((buffer_size,), self.config.block_size, dtype=torch.long, device=inputs_ids.device),
"max_progress": torch.full((buffer_size,), self.config.block_size, dtype=torch.long, device=inputs_ids.device),
"active": torch.zeros(buffer_size, dtype=torch.bool, device=inputs_ids.device),
}
self._puma_streaming_state = state
return state
def build_bd_training_inputs(self, inputs_ids, noisy_inputs_ids, position_ids, logits_to_keep_half, num_tokens=None):
bsz, seq_len = inputs_ids.shape
if num_tokens is None:
num_tokens = calculate_token_nums(position_ids)
router_noisy_part_list = []
for i in range(bsz):
cur_router_noisy_part = (torch.arange(num_tokens[i].shape[0] *2) % 2 == 0).to(inputs_ids.device)
cur_router_noisy_part = cur_router_noisy_part.repeat_interleave(num_tokens[i].repeat_interleave(2))
router_noisy_part_list.append(cur_router_noisy_part)
router_noisy_part = torch.stack(router_noisy_part_list, dim=0)
# concated inputs_ids: (bzs, seq_len x 2)
concat_inputs_ids = inputs_ids.repeat(1, 2)
# concated logits_to_keep: (bsz, seq_len x 2)
logits_to_keep = torch.zeros(
bsz, 2 * seq_len, dtype=torch.bool, device=inputs_ids.device)
# concated position_ids: (bsz, seq_len x 2)
concat_position_ids = torch.zeros(
bsz, 2 * seq_len, dtype=position_ids.dtype, device=position_ids.device)
for i in range(bsz):
concat_inputs_ids[i][router_noisy_part[i]] = noisy_inputs_ids[i]
concat_inputs_ids[i][~router_noisy_part[i]] = inputs_ids[i]
logits_to_keep[i][router_noisy_part[i]] = logits_to_keep_half[i]
concat_position_ids[i][router_noisy_part[i]] = position_ids[i]
concat_position_ids[i][~router_noisy_part[i]] = position_ids[i]
# create flex_attention mask
attention_mask = block_attn_mask(num_tokens, self.config.block_size, inputs_ids.device)
flex_attention_mask_3d = create_block_mask(
lambda b, h, q_idx, kv_idx: attention_mask[b, q_idx, kv_idx],
B=attention_mask.size(0), H=None,
Q_LEN=attention_mask.size(1), KV_LEN=attention_mask.size(2),
)
return concat_inputs_ids, concat_position_ids, flex_attention_mask_3d, logits_to_keep_half, logits_to_keep, num_tokens
def prepare_for_bd_training(self, inputs_ids, position_ids, prompt_mask):
num_tokens = calculate_token_nums(position_ids) # List[torch.Tensor]
noisy_inputs_ids, logits_to_keep_half, p_mask = forward_add_noise_packed(
inputs_ids=inputs_ids,
num_tokens_list=num_tokens,
prompt_mask=prompt_mask,
mask_id=self.config.mask_token_id,
)
concat_inputs_ids, concat_position_ids, flex_attention_mask_3d, logits_to_keep_half, logits_to_keep, _ = self.build_bd_training_inputs(
inputs_ids=inputs_ids,
noisy_inputs_ids=noisy_inputs_ids,
position_ids=position_ids,
logits_to_keep_half=logits_to_keep_half,
num_tokens=num_tokens,
)
return {
"concat_inputs_ids": concat_inputs_ids,
"concat_position_ids": concat_position_ids,
"flex_attention_mask_3d": flex_attention_mask_3d,
"logits_to_keep_half": logits_to_keep_half,
"logits_to_keep": logits_to_keep,
"p_mask": p_mask,
"noisy_inputs_ids": noisy_inputs_ids,
"num_tokens": num_tokens,
}
def prepare_for_puma_streaming_training(self, inputs_ids, labels, position_ids, use_remask_aux: bool = False):
batch_size = inputs_ids.shape[0]
rollout_steps = int(getattr(self.config, "gap_rollout_steps", self.config.block_size))
rollout_steps = max(1, rollout_steps)
transfer_schedule = get_num_transfer_tokens(self.config.block_size, rollout_steps).to(inputs_ids.device)
context = getattr(self, "_puma_streaming_context", {}) or {}
buffer_size = int(context.get("buffer_size") or batch_size)
slot_offset = int(context.get("slot_offset", 0))
slot_indices = (torch.arange(batch_size, device=inputs_ids.device) + slot_offset) % buffer_size
state = self._ensure_puma_streaming_state(buffer_size, inputs_ids, labels, position_ids)
if self._should_use_gap_prefix_frontier_state():
need_refill = (~state["active"][slot_indices]) | (state["stages"][slot_indices] >= state["max_progress"][slot_indices])
else:
need_refill = (~state["active"][slot_indices]) | (state["stages"][slot_indices] >= rollout_steps)
if need_refill.any():
refill_slots = slot_indices[need_refill]
refill_inputs = inputs_ids[need_refill].detach().clone()
refill_labels = labels[need_refill].detach().clone()
refill_position_ids = position_ids[need_refill].detach().clone()
refill_num_tokens = calculate_token_nums(refill_position_ids)
refill_max_progress = self._compute_gap_prefix_progress_limits(
labels=refill_labels,
num_tokens=refill_num_tokens,
block_size=self.config.block_size,
rollout_steps=rollout_steps,
)
state["clean_input_ids"][refill_slots] = refill_inputs
state["labels"][refill_slots] = refill_labels
state["position_ids"][refill_slots] = refill_position_ids
refill_progress = torch.zeros(refill_inputs.shape[0], dtype=torch.long, device=inputs_ids.device)
refill_noisy = self._build_gap_prefix_teacher_forced_state(
clean_input_ids=refill_inputs,
labels=refill_labels,
num_tokens=refill_num_tokens,
progress_units=refill_progress,
)
state["noisy_inputs_ids"][refill_slots] = refill_noisy
state["stages"][refill_slots] = 0
state["max_progress"][refill_slots] = refill_max_progress
state["active"][refill_slots] = True
clean_input_ids = state["clean_input_ids"][slot_indices].clone()
clean_labels = state["labels"][slot_indices].clone()
clean_position_ids = state["position_ids"][slot_indices].clone()
current_stages = state["stages"][slot_indices].clone()
num_tokens = calculate_token_nums(clean_position_ids)
if self._should_use_gap_prefix_frontier_state():
noisy_inputs_ids = self._build_gap_prefix_teacher_forced_state(
clean_input_ids=clean_input_ids,
labels=clean_labels,
num_tokens=num_tokens,
progress_units=current_stages,
)
else:
noisy_inputs_ids = state["noisy_inputs_ids"][slot_indices].clone()
target_scope_mask = clean_labels.ne(-100)
logits_to_keep_half = noisy_inputs_ids.eq(self.config.mask_token_id) & target_scope_mask
if not logits_to_keep_half.any() and target_scope_mask.any():
fallback_index = torch.nonzero(target_scope_mask, as_tuple=False)[0]
noisy_inputs_ids[fallback_index[0], fallback_index[1]] = self.config.mask_token_id
logits_to_keep_half[fallback_index[0], fallback_index[1]] = True
p_mask = build_rollout_p_mask(
masked_indices=logits_to_keep_half,
labels=clean_labels,
num_tokens=num_tokens,
target_scope_mask=target_scope_mask,
per_block=True,
block_size=self.config.block_size,
)
concat_inputs_ids, concat_position_ids, flex_attention_mask_3d, logits_to_keep_half, logits_to_keep, _ = self.build_bd_training_inputs(
inputs_ids=clean_input_ids,
noisy_inputs_ids=noisy_inputs_ids,
position_ids=clean_position_ids,
logits_to_keep_half=logits_to_keep_half,
num_tokens=num_tokens,
)
return {
"concat_inputs_ids": concat_inputs_ids,
"concat_position_ids": concat_position_ids,
"flex_attention_mask_3d": flex_attention_mask_3d,
"logits_to_keep_half": logits_to_keep_half,
"logits_to_keep": logits_to_keep,
"p_mask": p_mask,
"noisy_inputs_ids": noisy_inputs_ids,
"num_tokens": num_tokens,
"target_scope_mask": target_scope_mask,
"loss_target_count": target_scope_mask.sum().clamp_min(1),
"gap_training_mode": "puma",
"use_remask_aux": use_remask_aux,
"rollout_depth": current_stages.float() / rollout_steps,
"rollout_progress_units": current_stages,
"next_transfer_tokens": transfer_schedule[current_stages.clamp_max(rollout_steps - 1)],
"rollout_strategy": getattr(self.config, "gap_rollout_strategy", "low_confidence_dynamic"),
"rollout_confidence_threshold": float(getattr(self.config, "gap_rollout_confidence_threshold", 0.95)),
"terminal_rollout_strategy": getattr(self.config, "gap_grpo_terminal_rollout_strategy", None)
or getattr(self.config, "gap_rollout_strategy", "low_confidence_dynamic"),
"terminal_rollout_scope": getattr(self.config, "gap_grpo_terminal_rollout_scope", None)
or getattr(self.config, "gap_rollout_scope", "all"),
"clean_input_ids": clean_input_ids,
"clean_labels": clean_labels,
"clean_position_ids": clean_position_ids,
"streaming_slot_indices": slot_indices,
"streaming_refills": need_refill.sum(),
"max_rollout_progress": self._compute_gap_prefix_progress_limits(
labels=clean_labels,
num_tokens=num_tokens,
block_size=self.config.block_size,
rollout_steps=rollout_steps,
) if self._should_use_gap_prefix_frontier_state() else state["max_progress"][slot_indices].clone(),
}
def advance_puma_streaming_state(self, training_batch, proposal_scores_full):
state = self._puma_streaming_state
if state is None:
return
slot_indices = training_batch.get("streaming_slot_indices")
if slot_indices is None:
return
rollout_steps = int(getattr(self.config, "gap_rollout_steps", self.config.block_size))
rollout_steps = max(1, rollout_steps)
if self._should_use_gap_prefix_frontier_state():
next_stages = training_batch["rollout_progress_units"].to(torch.long) + 1
max_progress = training_batch.get("max_rollout_progress")
if max_progress is None:
max_progress = torch.full_like(next_stages, rollout_steps)
finished_mask = next_stages >= max_progress
next_stages = torch.where(finished_mask, max_progress, next_stages)
state["stages"][slot_indices] = next_stages
state["active"][slot_indices] = True
return
reveal_mask = select_teacher_forced_rollout_tokens(
masked_indices=training_batch["logits_to_keep_half"],
proposal_scores_full=proposal_scores_full,
num_tokens=training_batch["num_tokens"],
block_size=self.config.block_size,
num_transfer_tokens=training_batch["next_transfer_tokens"],
strategy=training_batch["rollout_strategy"],
confidence_threshold=training_batch["rollout_confidence_threshold"],
scope=getattr(self.config, "gap_rollout_scope", "all"),
)
next_noisy_inputs_ids = training_batch["noisy_inputs_ids"].clone()
next_noisy_inputs_ids[reveal_mask] = training_batch["clean_input_ids"][reveal_mask]
next_stages = training_batch["rollout_depth"].to(torch.long) + 1
finished_mask = next_stages >= rollout_steps
finished_mask |= ~(next_noisy_inputs_ids.eq(self.config.mask_token_id) & training_batch["clean_labels"].ne(-100)).any(dim=1)
next_stages = torch.where(
finished_mask,
torch.full_like(next_stages, rollout_steps),
next_stages,
)
state["noisy_inputs_ids"][slot_indices] = next_noisy_inputs_ids.detach()
state["stages"][slot_indices] = next_stages
state["active"][slot_indices] = True
def prepare_for_teacher_forced_rollout_training(
self,
inputs_ids,
labels,
position_ids,
prompt_mask,
output_attentions,
output_hidden_states,
cache_position,
**kwargs,
):
num_tokens = calculate_token_nums(position_ids)
answer_mask = ~prompt_mask
gap_training_mode = getattr(self.config, "gap_training_mode", "remask")
if gap_training_mode not in {"puma", "remask"}:
raise ValueError(f"Unsupported GAP training mode: {gap_training_mode}")
use_remask_aux = gap_training_mode == "remask"
if getattr(self.config, "gap_puma_streaming", True):
return self.prepare_for_puma_streaming_training(
inputs_ids,
labels,
position_ids,
use_remask_aux=use_remask_aux,
)
noisy_inputs_ids = torch.where(
answer_mask,
torch.full_like(inputs_ids, self.config.mask_token_id),
inputs_ids,
)
rollout_steps = int(getattr(self.config, "gap_rollout_steps", self.config.block_size))
rollout_steps = max(1, rollout_steps)
transfer_schedule = get_num_transfer_tokens(self.config.block_size, rollout_steps).tolist()
rollout_depth = int(torch.randint(0, rollout_steps, (1,), device=inputs_ids.device).item())
rollout_strategy = getattr(self.config, "gap_rollout_strategy", "low_confidence_dynamic")
rollout_confidence_threshold = float(getattr(self.config, "gap_rollout_confidence_threshold", 0.95))
default_rollout_scope = "all"
rollout_scope = getattr(self.config, "gap_rollout_scope", default_rollout_scope)
completed_steps = 0
if self._should_use_gap_prefix_frontier_state():
max_progress = self._compute_gap_prefix_progress_limits(
labels=labels,
num_tokens=num_tokens,
block_size=self.config.block_size,
rollout_steps=rollout_steps,
)
rollout_progress = torch.floor(
torch.rand(inputs_ids.shape[0], device=inputs_ids.device) * max_progress.to(dtype=torch.float32)
).to(dtype=torch.long)
noisy_inputs_ids = self._build_gap_prefix_teacher_forced_state(
clean_input_ids=inputs_ids,
labels=labels,
num_tokens=num_tokens,
progress_units=rollout_progress,
)
completed_steps = int(rollout_progress.float().mean().item())
else:
rollout_depth = int(torch.randint(0, rollout_steps, (1,), device=inputs_ids.device).item())
for step_idx in range(rollout_depth):
masked_indices = noisy_inputs_ids.eq(self.config.mask_token_id) & answer_mask
if not masked_indices.any():
break
concat_inputs_ids, concat_position_ids, flex_attention_mask_3d, logits_to_keep_half, logits_to_keep, _ = self.build_bd_training_inputs(
inputs_ids=inputs_ids,
noisy_inputs_ids=noisy_inputs_ids,
position_ids=position_ids,
logits_to_keep_half=masked_indices,
num_tokens=num_tokens,
)
outputs = self.model(
input_ids=concat_inputs_ids,
attention_mask=flex_attention_mask_3d,
position_ids=concat_position_ids,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=True,
cache_position=cache_position,
**kwargs,
)
hidden_states = outputs.last_hidden_state[logits_to_keep].contiguous()
proposal_logits = self.lm_head(hidden_states).float()
proposal_ids = proposal_logits.argmax(dim=-1)
proposal_scores = (
proposal_logits.gather(-1, proposal_ids.unsqueeze(-1)).squeeze(-1)
- torch.logsumexp(proposal_logits, dim=-1)
).exp()
proposal_scores_full = torch.full(
noisy_inputs_ids.shape,
float("-inf"),
dtype=proposal_scores.dtype,
device=proposal_scores.device,
)
proposal_scores_full[masked_indices] = proposal_scores
reveal_mask = select_teacher_forced_rollout_tokens(
masked_indices=masked_indices,
proposal_scores_full=proposal_scores_full,
num_tokens=num_tokens,
block_size=self.config.block_size,
num_transfer_tokens=int(transfer_schedule[step_idx]),
strategy=rollout_strategy,
confidence_threshold=rollout_confidence_threshold,
scope=rollout_scope,
)
if not reveal_mask.any():
break
noisy_inputs_ids[reveal_mask] = inputs_ids[reveal_mask]
completed_steps += 1
remaining_masked = noisy_inputs_ids.eq(self.config.mask_token_id) & answer_mask
target_scope_mask = labels.ne(-100)
logits_to_keep_half = remaining_masked
if not logits_to_keep_half.any() and answer_mask.any():
fallback_mask = target_scope_mask if target_scope_mask.any() else labels.ne(-100)
if not fallback_mask.any():
fallback_mask = answer_mask
fallback_index = torch.nonzero(fallback_mask, as_tuple=False)[0]
noisy_inputs_ids[fallback_index[0], fallback_index[1]] = self.config.mask_token_id
logits_to_keep_half[fallback_index[0], fallback_index[1]] = True
target_scope_mask[fallback_index[0], fallback_index[1]] = labels[fallback_index[0], fallback_index[1]].ne(-100)
p_mask = build_rollout_p_mask(
masked_indices=logits_to_keep_half,
labels=labels,
num_tokens=num_tokens,
target_scope_mask=target_scope_mask,
per_block=True,
block_size=self.config.block_size,
)
concat_inputs_ids, concat_position_ids, flex_attention_mask_3d, logits_to_keep_half, logits_to_keep, _ = self.build_bd_training_inputs(
inputs_ids=inputs_ids,
noisy_inputs_ids=noisy_inputs_ids,
position_ids=position_ids,
logits_to_keep_half=logits_to_keep_half,
num_tokens=num_tokens,
)
return {
"concat_inputs_ids": concat_inputs_ids,
"concat_position_ids": concat_position_ids,
"flex_attention_mask_3d": flex_attention_mask_3d,
"logits_to_keep_half": logits_to_keep_half,
"logits_to_keep": logits_to_keep,
"p_mask": p_mask,
"noisy_inputs_ids": noisy_inputs_ids,
"num_tokens": num_tokens,
"target_scope_mask": target_scope_mask,
"loss_target_count": target_scope_mask.sum().clamp_min(1),
"gap_training_mode": "puma",
"use_remask_aux": use_remask_aux,
"rollout_depth": (
rollout_progress.float() / rollout_steps if self._should_use_gap_prefix_frontier_state() else completed_steps
),
"rollout_progress_units": rollout_progress if self._should_use_gap_prefix_frontier_state() else torch.full(
(inputs_ids.shape[0],),
int(completed_steps),
dtype=torch.long,
device=inputs_ids.device,
),
"next_transfer_tokens": int(transfer_schedule[min(completed_steps, rollout_steps - 1)]),
"rollout_strategy": rollout_strategy,
"rollout_confidence_threshold": rollout_confidence_threshold,
"terminal_rollout_strategy": getattr(self.config, "gap_grpo_terminal_rollout_strategy", None)
or rollout_strategy,
"terminal_rollout_scope": getattr(self.config, "gap_grpo_terminal_rollout_scope", None)
or rollout_scope,
"max_rollout_progress": self._compute_gap_prefix_progress_limits(
labels=labels,
num_tokens=num_tokens,
block_size=self.config.block_size,
rollout_steps=rollout_steps,
) if self._should_use_gap_prefix_frontier_state() else torch.full(
(inputs_ids.shape[0],),
rollout_steps,
dtype=torch.long,
device=inputs_ids.device,
),
}
@can_return_tuple
@auto_docstring
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Cache] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
labels: Optional[torch.LongTensor] = None,
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
logits_to_keep: Union[int, torch.Tensor] = 0,
**kwargs: Unpack[KwargsForCausalLM],
) -> CausalLMOutputWithPast:
r"""
labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
(masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
Example:
```python
>>> from transformers import AutoTokenizer, SDARForCausalLM
>>> model = SDARForCausalLM.from_pretrained("DiffuOpen/SDAR-1.7B-Chat")
>>> tokenizer = AutoTokenizer.from_pretrained("DiffuOpen/SDAR-1.7B-Chat")
>>> prompt = "Hey, are you conscious? Can you talk to me?"
>>> inputs = tokenizer(prompt, return_tensors="pt")
>>> # Generate
>>> generate_ids = model.generate(inputs.input_ids, max_length=30)
>>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
"Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you."
```"""
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
if self.training:
assert inputs_embeds is None, "only support input_ids during training"
prompt_mask = (labels == -100) if labels is not None else None
coarse_debug = _gap_debug_enabled()
coarse_step = int(getattr(self, "_gap_debug_global_step", -1))
if position_ids is None:
position_ids = torch.arange(
input_ids.shape[1], device=input_ids.device, dtype=torch.long
).unsqueeze(0).expand(input_ids.shape[0], -1)
position_ids = modify_padded_position_ids_2d(position_ids)
if coarse_debug:
logger.info(
"[GAP coarse] step=%s entering_prepare_rollout batch=%s seq=%s gap_enable=%s",
coarse_step,
int(input_ids.shape[0]),
int(input_ids.shape[1]),
bool(getattr(self.config, "gap_enable", False)),
)
if getattr(self.config, "gap_enable", False):
training_batch = self.prepare_for_teacher_forced_rollout_training(
input_ids,
labels,
position_ids,
prompt_mask,
output_attentions,
output_hidden_states,
cache_position,
**kwargs,
)
else:
training_batch = self.prepare_for_bd_training(input_ids, position_ids, prompt_mask)
if coarse_debug:
logger.info(
"[GAP coarse] step=%s finished_prepare_rollout mode=%s logits_to_keep=%s target_scope=%s",
coarse_step,
str(training_batch.get("gap_training_mode", "bd")),
int(training_batch["logits_to_keep"].sum().item()) if "logits_to_keep" in training_batch else -1,
int(training_batch["target_scope_mask"].sum().item()) if "target_scope_mask" in training_batch else -1,
)
train_input_ids = training_batch.get("clean_input_ids", input_ids)
train_labels = training_batch.get("clean_labels", labels)
train_position_ids = training_batch.get("clean_position_ids", position_ids)
pre_timing_interval = _gap_env_int("SDAR_GAP_GRPO_TIMING_INTERVAL", 0)
pre_debug_step = int(getattr(self, "_gap_debug_global_step", -1))
should_log_pre_timing = (
pre_timing_interval > 0
and _gap_is_rank0()
and pre_debug_step >= 0
and (pre_debug_step % pre_timing_interval == 0)
)
pre_forward_start = time.perf_counter() if should_log_pre_timing else 0.0
if coarse_debug:
logger.info("[GAP coarse] step=%s entering_model_forward", coarse_step)
outputs = self.model(
input_ids=training_batch["concat_inputs_ids"],
attention_mask=training_batch["flex_attention_mask_3d"],
position_ids=training_batch["concat_position_ids"],
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=True,
cache_position=cache_position,
**kwargs,
)
pre_forward_sec = (time.perf_counter() - pre_forward_start) if should_log_pre_timing else 0.0
if coarse_debug:
logger.info(
"[GAP coarse] step=%s finished_model_forward hidden_shape=%s",
coarse_step,
tuple(outputs.last_hidden_state.shape),
)
hidden_states = outputs.last_hidden_state
assert train_labels is not None, "Labels must be provided for training."
answer_len = (train_labels != -100).sum()
hidden_states = hidden_states[training_batch["logits_to_keep"]].contiguous()
p_mask = training_batch["p_mask"]
diffusion_loss_weight = float(getattr(self.config, "gap_diffusion_loss_weight", 1.0) or 0.0)
rollout_depth_metric = training_batch.get("rollout_depth", 0)
if torch.is_tensor(rollout_depth_metric):
rollout_depth_metric = rollout_depth_metric.detach().to(torch.float32).mean()
else:
rollout_depth_metric = hidden_states.new_tensor(float(rollout_depth_metric))
if getattr(self.config, "gap_enable", False) and training_batch.get("gap_training_mode") == "puma":
loss_fct = FusedLinearDiffusionCrossEntropyLoss(reduction='sum')
diffusion_obj = loss_fct(
x=hidden_states,
target=train_labels[training_batch["logits_to_keep_half"]].contiguous(),
weight=self.lm_head.weight,
bias=self.lm_head.bias,
p_mask=training_batch["p_mask"],
)
diffusion_loss = diffusion_obj / answer_len.to(diffusion_obj.dtype)
use_remask_aux = bool(training_batch.get("use_remask_aux", False))
proposal_scores_full = None
proposal_ids = None
if "streaming_slot_indices" in training_batch or use_remask_aux:
if coarse_debug:
logger.info("[GAP coarse] step=%s entering_proposal_prep", coarse_step)
proposal_start = time.perf_counter() if should_log_pre_timing else 0.0
proposal_logits = self.lm_head(hidden_states).float()
proposal_ids = proposal_logits.argmax(dim=-1)
proposal_scores = (
proposal_logits.gather(-1, proposal_ids.unsqueeze(-1)).squeeze(-1)
- torch.logsumexp(proposal_logits, dim=-1)
).exp()
proposal_scores_full = torch.full(
training_batch["noisy_inputs_ids"].shape,
float("-inf"),
dtype=proposal_scores.dtype,
device=proposal_scores.device,
)
proposal_scores_full[training_batch["logits_to_keep_half"]] = proposal_scores
proposal_sec = (time.perf_counter() - proposal_start) if should_log_pre_timing else 0.0
if coarse_debug:
logger.info(
"[GAP coarse] step=%s finished_proposal_prep proposal_shape=%s",
coarse_step,
tuple(proposal_logits.shape),
)
else:
proposal_sec = 0.0
if "streaming_slot_indices" in training_batch:
self.advance_puma_streaming_state(training_batch, proposal_scores_full)
weighted_diffusion_loss = diffusion_loss * diffusion_loss_weight
loss = weighted_diffusion_loss
loss_metrics = {"rollout_depth": rollout_depth_metric}
streaming_refills_metric = training_batch.get("streaming_refills")
if streaming_refills_metric is not None:
if torch.is_tensor(streaming_refills_metric):
streaming_refills_metric = streaming_refills_metric.detach().to(torch.float32)
else:
streaming_refills_metric = hidden_states.new_tensor(float(streaming_refills_metric))
loss_metrics["streaming_refills"] = streaming_refills_metric
if use_remask_aux:
if coarse_debug:
logger.info("[GAP coarse] step=%s entering_remask_prep", coarse_step)
remask_prep_start = time.perf_counter() if should_log_pre_timing else 0.0
remask_scope = getattr(self.config, "gap_remask_scope", "frontier_block")
if (
_gap_env_int("SDAR_GAP_GRPO_REMASK_PREFIX_GUARD_TOKENS", 0) > 0
or _gap_env_int("SDAR_GAP_GRPO_REMASK_TAIL_GUARD_BLOCKS", 0) > 0
):
remask_scope = "all"
remask_candidate_mask = select_policy_transfer_tokens(
masked_indices=training_batch["logits_to_keep_half"],
proposal_scores_full=proposal_scores_full,
num_tokens=training_batch["num_tokens"],
block_size=self.config.block_size,
num_transfer_tokens=training_batch["next_transfer_tokens"],
strategy=training_batch["rollout_strategy"],
confidence_threshold=training_batch["rollout_confidence_threshold"],
scope=remask_scope,
)
remask_candidate_mask = self._apply_gap_grpo_remask_guards(
remask_candidate_mask,
training_batch["target_scope_mask"],
training_batch["logits_to_keep_half"],
)
remask_logits = self.gap_remask_head(hidden_states, proposal_logits)
gap_outputs = apply_gap_remask(
noisy_input_ids=training_batch["noisy_inputs_ids"],
clean_input_ids=train_input_ids,
labels=train_labels,
masked_indices=training_batch["logits_to_keep_half"],
p_mask=training_batch["p_mask"],
proposal_ids=proposal_ids,
remask_logits=remask_logits,
candidate_mask_full=remask_candidate_mask,
mask_token_id=self.config.mask_token_id,
remask_threshold=getattr(self.config, "gap_remask_threshold", 0.5),
remask_loss_weight=getattr(self.config, "gap_remask_loss_weight", 1.0),
remask_default_p_mask=getattr(self.config, "gap_remask_default_p_mask", 1.0),
block_size=self.config.block_size,
supervision=getattr(self.config, "gap_remask_supervision", "adv_bce"),
target_scope_mask=training_batch["target_scope_mask"],
)
remask_prep_sec = (time.perf_counter() - remask_prep_start) if should_log_pre_timing else 0.0
if coarse_debug:
logger.info(
"[GAP coarse] step=%s finished_remask_prep candidate_tokens=%s",
coarse_step,
int(gap_outputs.full_candidate_mask.sum().item()) if getattr(gap_outputs, "full_candidate_mask", None) is not None else -1,
)
remask_loss = gap_outputs.remask_loss
loss = loss + remask_loss
loss_metrics["diffusion_loss"] = diffusion_loss.detach()
loss_metrics["weighted_diffusion_loss"] = weighted_diffusion_loss.detach()
loss_metrics["remask_loss"] = remask_loss.detach()
for metric_name, metric_value in gap_outputs.metrics.items():
if metric_name == "remask_loss":
continue
loss_metrics[metric_name] = hidden_states.new_tensor(float(metric_value))
if should_log_pre_timing:
logger.info(
"[GAP pre-grpo timing] step=%s model_forward_sec=%.2f proposal_sec=%.2f remask_prep_sec=%.2f candidate_tokens=%s",
pre_debug_step,
pre_forward_sec,
proposal_sec,
remask_prep_sec,
int(gap_outputs.full_candidate_mask.sum().item()) if getattr(gap_outputs, "full_candidate_mask", None) is not None else -1,
)
logger.info("[GAP pre-grpo timing] step=%s entering_grpo_loss=1", pre_debug_step)
max_grpo_target_tokens = _gap_env_int("SDAR_GAP_GRPO_MAX_TARGET_TOKENS", 0)
max_grpo_valid_tokens = _gap_env_int("SDAR_GAP_GRPO_MAX_VALID_TOKENS", 0)
target_token_counts = training_batch["target_scope_mask"].sum(dim=1)
max_target_tokens = int(target_token_counts.max().item()) if target_token_counts.numel() > 0 else 0
max_valid_tokens = int(train_input_ids.shape[1])
skip_long_target = max_grpo_target_tokens > 0 and max_target_tokens > max_grpo_target_tokens
skip_long_valid = max_grpo_valid_tokens > 0 and max_valid_tokens > max_grpo_valid_tokens
if skip_long_target or skip_long_valid:
grpo_loss = remask_logits.sum() * 0.0 + self.gap_value_head(hidden_states[:1]).sum() * 0.0
grpo_metrics = {
"grpo_skipped_long_batch": hidden_states.new_tensor(1.0),
"grpo_max_target_tokens": hidden_states.new_tensor(float(max_target_tokens)),
"grpo_max_valid_tokens": hidden_states.new_tensor(float(max_valid_tokens)),
}
if coarse_debug:
_gap_stderr(
"[GAP grpo skip] "
f"step={coarse_step} max_target_tokens={max_target_tokens} "
f"target_limit={max_grpo_target_tokens} max_valid_tokens={max_valid_tokens} "
f"valid_limit={max_grpo_valid_tokens}"
)
else:
if coarse_debug:
_gap_stderr(f"[GAP coarse] step={coarse_step} entering_grpo_loss")
grpo_loss, grpo_metrics = self._compute_gap_grpo_loss(
clean_input_ids=train_input_ids,
labels=train_labels,
position_ids=train_position_ids,
num_tokens=training_batch["num_tokens"],
remask_logits=remask_logits,
grpo_hidden_states=hidden_states,
gap_outputs=gap_outputs,
masked_indices=training_batch["logits_to_keep_half"],
rollout_strategy=training_batch.get("terminal_rollout_strategy", training_batch["rollout_strategy"]),
rollout_confidence_threshold=training_batch["rollout_confidence_threshold"],
target_scope_mask=training_batch["target_scope_mask"],
)
if coarse_debug:
_gap_stderr(f"[GAP coarse] step={coarse_step} finished_grpo_loss")
loss = loss + grpo_loss
for metric_name, metric_value in grpo_metrics.items():
loss_metrics[metric_name] = metric_value.detach()
sft_ce_weight = float(getattr(self.config, "gap_grpo_sft_ce_weight", 0.0) or 0.0)
if sft_ce_weight > 0.0:
if coarse_debug:
logger.info("[GAP coarse] step=%s entering_sft_ce", coarse_step)
ce_timing_interval = _gap_env_int("SDAR_GAP_GRPO_TIMING_INTERVAL", 0)
ce_debug_step = int(getattr(self, "_gap_debug_global_step", -1))
should_log_ce_timing = (
ce_timing_interval > 0
and _gap_is_rank0()
and ce_debug_step >= 0
and (ce_debug_step % ce_timing_interval == 0)
)
ce_start = time.perf_counter() if should_log_ce_timing else 0.0
sft_ce_loss = self._compute_gap_sft_ce_anchor(
clean_input_ids=train_input_ids,
clean_labels=train_labels,
clean_position_ids=train_position_ids,
)
ce_sec = (time.perf_counter() - ce_start) if should_log_ce_timing else 0.0
weighted_sft_ce_loss = sft_ce_loss * sft_ce_weight
loss = loss + weighted_sft_ce_loss
loss_metrics["grpo_sft_ce_loss"] = sft_ce_loss.detach()
loss_metrics["grpo_sft_ce_weighted"] = weighted_sft_ce_loss.detach()
if coarse_debug:
logger.info("[GAP coarse] step=%s finished_sft_ce", coarse_step)
if should_log_ce_timing:
logger.info(
"[GAP ce timing] step=%s ce_sec=%.2f sft_ce_weight=%.3f",
ce_debug_step,
ce_sec,
sft_ce_weight,
)
else:
loss_metrics["diffusion_loss"] = diffusion_loss.detach()
loss_metrics["weighted_diffusion_loss"] = weighted_diffusion_loss.detach()
self._last_loss_metrics = loss_metrics
elif getattr(self.config, "gap_enable", False):
raise ValueError(f'Unsupported GAP training batch mode: {training_batch.get("gap_training_mode")}')
else:
loss_fct = FusedLinearDiffusionCrossEntropyLoss(reduction='sum')
loss = loss_fct( # it will return (sum_loss, unreduced_loss)
# conduct `view(-1, V)` inside the function
x=hidden_states,
target=train_labels[training_batch["logits_to_keep_half"]].contiguous(),
weight=self.lm_head.weight,
bias=self.lm_head.bias,
p_mask=training_batch["p_mask"],
)
diffusion_loss = loss / answer_len
loss = diffusion_loss
self._last_loss_metrics = None
logits = None
else:
# decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn)
self._last_loss_metrics = None
outputs: BaseModelOutputWithPast = self.model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
cache_position=cache_position,
**kwargs,
)
hidden_states = outputs.last_hidden_state
# Only compute necessary logits, and do not upcast them to float if we are not computing the loss
slice_indices = slice(-logits_to_keep,
None) if isinstance(logits_to_keep, int) else logits_to_keep
hidden_states = hidden_states[:, slice_indices, :].contiguous()
fuse_linear_and_cross_entropy = self.config.fuse_cross_entropy and self.training
if fuse_linear_and_cross_entropy:
# When using fused_linear_ce_loss, we do not compute the whole logits on HBM
logits = None
else:
logits = self.lm_head(hidden_states)
loss = None
if labels is not None:
# FusedLinearCrossEntropyLoss will be implemented by monkey patch when training
# We don't use it when inferencing
loss_fct = nn.CrossEntropyLoss() # nn.CE
loss = loss_fct(
logits.view(-1, self.config.vocab_size), labels.view(-1))
return CausalLMOutputWithPast(
loss=loss,
logits=logits,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
__all__ = [
"SDARForCausalLM",
"SDARModel",
"SDARPreTrainedModel",
]