Gertlek's picture
Keep release inference-only and simplify usage instructions
b6411a6 verified
Raw History Blame Contribute Delete
29.2 kB
"""Masked-scope scoring helpers for LLaDA-Guard inference."""
from __future__ import annotations
import torch
import torch.nn.functional as F
from tqdm import tqdm
from .data import PROMPT_BLOCK_ID, RESPONSE_BLOCK_ID, build_eval_loader
from .model import MASK_ID
_MASK_SEED_EXAMPLE_STRIDE = 104729
def _build_cached_eval_loader(
examples,
tokenizer,
max_seq_length,
batch_size,
gen_only=False,
skip_cls=False,
prompt_format: str = "aegis_taxonomy",
):
"""Compatibility wrapper for the deterministic public eval loader."""
del skip_cls
return build_eval_loader(
examples,
tokenizer,
max_seq_length,
batch_size,
gen_only=gen_only,
prompt_format=prompt_format,
)
def _mask_seed(batch, batch_idx: int, base_seed: int, t_idx: int) -> int | torch.Tensor:
"""Stable mask seed keyed by example id when available."""
example_index = batch.get("example_index")
if torch.is_tensor(example_index) and bool((example_index >= 0).all().item()):
return example_index.to(dtype=torch.long) * _MASK_SEED_EXAMPLE_STRIDE + int(base_seed) + int(t_idx)
return int(base_seed) + int(batch_idx) * 97 + int(t_idx)
def _base_llada_lm(model):
get_base_model = getattr(model, "get_base_model", None)
return get_base_model() if callable(get_base_model) else model
def _prefer_native_rms_norm(model) -> None:
"""Patch LLaDA RMSLayerNorm modules to PyTorch's native RMSNorm fast path."""
if not hasattr(F, "rms_norm"):
return
modules = getattr(_base_llada_lm(model), "modules", None)
if not callable(modules):
return
for module in modules():
if module.__class__.__name__ != "RMSLayerNorm" or getattr(module, "_native_rms_norm", False):
continue
normalized_shape = getattr(module, "normalized_shape", None)
weight = getattr(module, "weight", None)
eps = getattr(module, "eps", None)
if normalized_shape is None or weight is None or eps is None:
continue
def native_forward(x: torch.Tensor, rms_module=module) -> torch.Tensor:
y = F.rms_norm(x, rms_module.normalized_shape, rms_module.weight, rms_module.eps)
bias = getattr(rms_module, "bias", None)
if bias is not None:
y = y + bias
return y
module.forward = native_forward
module._native_rms_norm = True
def _prefer_inplace_silu(model) -> None:
"""Enable in-place SiLU on existing activation modules."""
modules = getattr(_base_llada_lm(model), "modules", None)
if not callable(modules):
return
for module in modules():
if module.__class__.__name__ == "SiLU":
module.inplace = True
def apply_eval_inference_fastpaths(model) -> None:
"""Apply idempotent inference-only module fast paths used by eval scoring."""
_prefer_native_rms_norm(model)
_prefer_inplace_silu(model)
def _mask_ratio_upper_bound(eps: float, max_t: float) -> float:
return max(float(eps), min(1.0, float(max_t)))
def _scale_mask_ratio(t_values, scale: float, eps: float, max_t: float = 1.0):
"""Scale a masking ratio while keeping it inside a valid range."""
upper = _mask_ratio_upper_bound(eps, max_t)
if abs(float(scale) - 1.0) <= 1e-12 and upper >= 1.0:
return t_values
if torch.is_tensor(t_values):
return torch.clamp(t_values.to(dtype=torch.float32) * float(scale), min=eps, max=upper)
return max(eps, min(upper, float(t_values) * float(scale)))
def _sample_relative_mask_indices(
block_ids: torch.Tensor,
real_lengths: torch.Tensor,
eps: float = 1e-3,
target_block_ids: tuple[int, ...] = (1, 2),
t_override: float | torch.Tensor | None = None,
max_t: float = 1.0,
seed: int | torch.Tensor | list[int] | tuple[int, ...] | None = None,
) -> tuple[list[torch.Tensor], torch.Tensor]:
"""Sample exact-k relative mask positions for the selected target region."""
device = block_ids.device
batch_size = block_ids.shape[0]
upper = _mask_ratio_upper_bound(eps, max_t)
if t_override is None:
base = torch.rand(batch_size, device=device)
offset = torch.arange(batch_size, device=device).float() / max(batch_size, 1)
t_values = (upper - eps) * ((base / max(batch_size, 1) + offset) % 1.0) + eps
elif torch.is_tensor(t_override):
t_values = torch.clamp(t_override.to(device=device, dtype=torch.float32), min=eps, max=upper)
else:
t_values = torch.full(
(batch_size,),
max(eps, min(upper, float(t_override))),
device=device,
)
row_seeds = None
generator = None
if torch.is_tensor(seed):
row_seeds = seed.detach().to(device="cpu", dtype=torch.long).tolist()
elif isinstance(seed, (list, tuple)):
row_seeds = [int(value) for value in seed]
elif seed is not None:
generator = torch.Generator(device=device)
generator.manual_seed(int(seed))
if row_seeds is not None and len(row_seeds) != batch_size:
raise ValueError(f"Expected {batch_size} row seeds, got {len(row_seeds)}")
positions = torch.arange(block_ids.shape[1], device=device).unsqueeze(0)
selections: list[torch.Tensor] = []
for idx in range(batch_size):
target_mask = torch.zeros(block_ids.shape[1], dtype=torch.bool, device=device)
for block_id in target_block_ids:
target_mask |= block_ids[idx] == block_id
target_positions = (
target_mask & (positions[0] < real_lengths[idx])
).nonzero(as_tuple=True)[0]
target_len = target_positions.numel()
if target_len == 0:
selections.append(torch.empty(0, dtype=torch.long, device=device))
continue
n_mask = max(1, min(target_len, int(round(float(t_values[idx]) * target_len))))
row_generator = generator
if row_seeds is not None:
row_generator = torch.Generator(device=device)
row_generator.manual_seed(int(row_seeds[idx]))
perm = torch.randperm(target_len, generator=row_generator, device=device)
selections.append(perm[:n_mask])
return selections, t_values
def _relative_to_absolute_mask(
block_ids: torch.Tensor,
real_lengths: torch.Tensor,
relative_indices: list[torch.Tensor],
target_block_ids: tuple[int, ...] = (1, 2),
) -> torch.Tensor:
"""Map relative target-region positions to absolute sequence positions."""
device = block_ids.device
batch_size, seq_len = block_ids.shape
positions = torch.arange(seq_len, device=device).unsqueeze(0)
mask = torch.zeros(batch_size, seq_len, dtype=torch.bool, device=device)
for idx in range(batch_size):
rel_idx = relative_indices[idx]
if rel_idx.numel() == 0:
continue
target_mask = torch.zeros(seq_len, dtype=torch.bool, device=device)
for block_id in target_block_ids:
target_mask |= block_ids[idx] == block_id
target_positions = (
target_mask & (positions[0] < real_lengths[idx])
).nonzero(as_tuple=True)[0]
mask[idx, target_positions[rel_idx]] = True
return mask
def _full_target_mask(
block_ids: torch.Tensor,
real_lengths: torch.Tensor,
target_block_ids: tuple[int, ...],
) -> torch.Tensor:
"""Build a mask covering every token in the selected target region."""
device = block_ids.device
positions = torch.arange(block_ids.shape[1], device=device).unsqueeze(0)
target_mask = torch.zeros_like(block_ids, dtype=torch.bool)
for block_id in target_block_ids:
target_mask |= block_ids == block_id
return target_mask & (positions < real_lengths.unsqueeze(1))
def _pad_to_match_length(
tensor: torch.Tensor,
target_len: int,
pad_id: int,
) -> torch.Tensor:
"""Right-pad a batch tensor to a target length."""
if tensor.shape[1] >= target_len:
return tensor
pad_len = target_len - tensor.shape[1]
pad = torch.full((tensor.shape[0], pad_len), pad_id, dtype=tensor.dtype, device=tensor.device)
return torch.cat([tensor, pad], dim=1)
def _pad_mask_to_match_length(mask: torch.Tensor, target_len: int) -> torch.Tensor:
"""Right-pad a boolean mask to a target length."""
if mask.shape[1] >= target_len:
return mask
pad_len = target_len - mask.shape[1]
pad = torch.zeros((mask.shape[0], pad_len), dtype=mask.dtype, device=mask.device)
return torch.cat([mask, pad], dim=1)
def _build_attention_mask_from_lengths(
real_lengths: torch.Tensor,
target_len: int,
) -> torch.Tensor:
"""Build a right-padded attention mask from per-example real lengths."""
positions = torch.arange(target_len, device=real_lengths.device).unsqueeze(0)
return (positions < real_lengths.unsqueeze(1)).to(dtype=torch.long)
def _compute_masked_token_losses_from_logits(
logits: torch.Tensor,
input_ids: torch.Tensor,
mask: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Compute masked token CE, aggregated per example, from precomputed logits."""
device = input_ids.device
batch_size = input_ids.shape[0]
if not mask.any():
zeros = logits.float().sum(dim=(1, 2)) * 0.0
return zeros, zeros
raw_ce = F.cross_entropy(logits[mask], input_ids[mask], reduction="none").float()
example_idx = (
torch.arange(batch_size, device=device).unsqueeze(1).expand_as(mask)[mask]
)
per_ex_sum = torch.zeros(batch_size, device=device, dtype=raw_ce.dtype)
per_ex_sum.scatter_add_(0, example_idx, raw_ce)
denom = mask.sum(dim=1).float()
per_ex_loss = per_ex_sum / denom.clamp(min=1)
return per_ex_loss, -per_ex_loss
def _compute_paired_masked_token_outputs(
model,
safe_input_ids: torch.Tensor,
safe_mask: torch.Tensor,
safe_real_lengths: torch.Tensor,
unsafe_input_ids: torch.Tensor,
unsafe_mask: torch.Tensor,
unsafe_real_lengths: torch.Tensor,
pad_id: int,
mask_id: int = MASK_ID,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Run one paired safe/unsafe forward and return padded ids + logits."""
max_len = max(safe_input_ids.shape[1], unsafe_input_ids.shape[1])
safe_ids = _pad_to_match_length(safe_input_ids, max_len, pad_id)
unsafe_ids = _pad_to_match_length(unsafe_input_ids, max_len, pad_id)
safe_attention_mask = _build_attention_mask_from_lengths(safe_real_lengths, max_len)
unsafe_attention_mask = _build_attention_mask_from_lengths(unsafe_real_lengths, max_len)
safe_mask_padded = _pad_mask_to_match_length(safe_mask, max_len)
unsafe_mask_padded = _pad_mask_to_match_length(unsafe_mask, max_len)
combined_ids = torch.cat([safe_ids, unsafe_ids], dim=0)
combined_attention_mask = torch.cat([safe_attention_mask, unsafe_attention_mask], dim=0)
combined_mask = torch.cat([safe_mask_padded, unsafe_mask_padded], dim=0)
noisy = combined_ids.clone()
noisy[combined_mask] = mask_id
logits = model(input_ids=noisy, attention_mask=combined_attention_mask).logits
half = safe_input_ids.shape[0]
return safe_ids, logits[:half], unsafe_ids, logits[half:]
def _target_token_counts(
block_ids: torch.Tensor,
real_lengths: torch.Tensor,
*,
target_block_ids: tuple[int, ...],
) -> torch.Tensor:
"""Count valid tokens in the scored target region for each row."""
positions = torch.arange(block_ids.shape[1], device=block_ids.device).unsqueeze(0)
target_mask = torch.zeros_like(block_ids, dtype=torch.bool)
for block_id in target_block_ids:
target_mask |= block_ids == block_id
valid_target = target_mask & (positions < real_lengths.unsqueeze(1))
return valid_target.sum(dim=1).clamp(min=1).float()
def _adaptive_optimal_t_values(
batch,
*,
target_block_ids: tuple[int, ...],
repeats: int,
sigma_over_c: float,
min_t: float,
max_t: float,
) -> tuple[torch.Tensor, ...]:
"""Bias-variance optimal t per row: (sigma^2 / (2 c^2 R n))^(1/3)."""
repeats = max(1, int(repeats))
counts = _target_token_counts(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
target_block_ids=target_block_ids,
)
t = ((float(sigma_over_c) ** 2) / (2.0 * repeats * counts)).pow(1.0 / 3.0)
t = torch.clamp(t, min=float(min_t), max=float(max_t))
return tuple(t.clone() for _ in range(repeats))
def _slice_batch_rows(batch: dict[str, torch.Tensor], row_mask: torch.Tensor) -> dict[str, torch.Tensor]:
"""Select a subset of rows from a batch dict."""
row_mask = row_mask.to(dtype=torch.bool)
sliced = {}
for key, value in batch.items():
if torch.is_tensor(value) and value.dim() > 0 and value.shape[0] == row_mask.shape[0]:
sliced[key] = value[row_mask]
else:
sliced[key] = value
return sliced
def _score_batch_masked_scope_sequential(
model,
batch,
batch_idx,
t_values,
pad_id,
response_mask_scale=1.0,
adaptive_t_repeats: int | None = None,
adaptive_t_sigma_over_c: float = 1.0,
adaptive_t_min: float = 1e-3,
adaptive_t_max: float = 0.5,
):
"""Score a batch with prompt/response scoped masks, one t at a time."""
device = batch["gen_safe_input_ids"].device
batch_size = batch["gen_safe_input_ids"].shape[0]
has_response = batch.get("has_response")
if has_response is None:
has_response = (batch["gen_safe_block_ids"] == RESPONSE_BLOCK_ID).any(dim=1).float()
has_response = has_response > 0.5
has_prompt_only = bool((~has_response).any().item())
has_with_response = bool(has_response.any().item())
if has_prompt_only and has_with_response:
scores = torch.zeros(batch_size, device=device)
prompt_mask = ~has_response
response_mask = has_response
if prompt_mask.any():
scores[prompt_mask] = _score_batch_masked_scope_sequential(
model,
_slice_batch_rows(batch, prompt_mask),
batch_idx,
t_values,
pad_id,
response_mask_scale=response_mask_scale,
adaptive_t_repeats=adaptive_t_repeats,
adaptive_t_sigma_over_c=adaptive_t_sigma_over_c,
adaptive_t_min=adaptive_t_min,
adaptive_t_max=adaptive_t_max,
)
if response_mask.any():
scores[response_mask] = _score_batch_masked_scope_sequential(
model,
_slice_batch_rows(batch, response_mask),
batch_idx,
t_values,
pad_id,
response_mask_scale=response_mask_scale,
adaptive_t_repeats=adaptive_t_repeats,
adaptive_t_sigma_over_c=adaptive_t_sigma_over_c,
adaptive_t_min=adaptive_t_min,
adaptive_t_max=adaptive_t_max,
)
return scores.detach()
active_t_values = t_values
target_block_ids = (PROMPT_BLOCK_ID,) if has_prompt_only else (RESPONSE_BLOCK_ID,)
if adaptive_t_repeats is not None:
active_t_values = _adaptive_optimal_t_values(
batch,
target_block_ids=target_block_ids,
repeats=adaptive_t_repeats,
sigma_over_c=adaptive_t_sigma_over_c,
min_t=adaptive_t_min,
max_t=adaptive_t_max,
)
score_accum = torch.zeros(batch_size, device=device)
for t_idx, t_value in enumerate(active_t_values):
if has_prompt_only:
prompt_relative_indices, _ = _sample_relative_mask_indices(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
t_override=t_value,
seed=_mask_seed(batch, batch_idx, 1337, t_idx),
target_block_ids=(PROMPT_BLOCK_ID,),
)
safe_mask = _relative_to_absolute_mask(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
prompt_relative_indices,
target_block_ids=(PROMPT_BLOCK_ID,),
)
unsafe_mask = _relative_to_absolute_mask(
batch["gen_unsafe_block_ids"],
batch["gen_unsafe_real_lengths"],
prompt_relative_indices,
target_block_ids=(PROMPT_BLOCK_ID,),
)
scope_safe_mask = safe_mask | _full_target_mask(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
target_block_ids=(RESPONSE_BLOCK_ID,),
)
scope_unsafe_mask = unsafe_mask | _full_target_mask(
batch["gen_unsafe_block_ids"],
batch["gen_unsafe_real_lengths"],
target_block_ids=(RESPONSE_BLOCK_ID,),
)
else:
response_t_value = _scale_mask_ratio(t_value, response_mask_scale, 1e-3)
response_relative_indices, _ = _sample_relative_mask_indices(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
t_override=response_t_value,
seed=_mask_seed(batch, batch_idx, 7331, t_idx),
target_block_ids=(RESPONSE_BLOCK_ID,),
)
safe_mask = _relative_to_absolute_mask(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
response_relative_indices,
target_block_ids=(RESPONSE_BLOCK_ID,),
)
unsafe_mask = _relative_to_absolute_mask(
batch["gen_unsafe_block_ids"],
batch["gen_unsafe_real_lengths"],
response_relative_indices,
target_block_ids=(RESPONSE_BLOCK_ID,),
)
scope_safe_mask = safe_mask
scope_unsafe_mask = unsafe_mask
safe_ids, safe_logits, unsafe_ids, unsafe_logits = _compute_paired_masked_token_outputs(
model,
batch["gen_safe_input_ids"],
scope_safe_mask,
batch["gen_safe_real_lengths"],
batch["gen_unsafe_input_ids"],
scope_unsafe_mask,
batch["gen_unsafe_real_lengths"],
pad_id,
)
max_len = safe_ids.shape[1]
safe_mask_padded = _pad_mask_to_match_length(safe_mask, max_len)
unsafe_mask_padded = _pad_mask_to_match_length(unsafe_mask, max_len)
_, safe_logp = _compute_masked_token_losses_from_logits(
safe_logits,
safe_ids,
safe_mask_padded,
)
_, unsafe_logp = _compute_masked_token_losses_from_logits(
unsafe_logits,
unsafe_ids,
unsafe_mask_padded,
)
score_accum += unsafe_logp - safe_logp
return (score_accum / len(active_t_values)).detach()
def _score_batch_masked_scope_tiled(
model,
batch,
batch_idx,
t_values,
pad_id,
response_mask_scale=1.0,
adaptive_t_repeats: int | None = None,
adaptive_t_sigma_over_c: float = 1.0,
adaptive_t_min: float = 1e-3,
adaptive_t_max: float = 0.5,
):
"""Score a batch with prompt/response scoped masks, tiled over t values."""
device = batch["gen_safe_input_ids"].device
has_response = batch.get("has_response")
if has_response is None:
has_response = (batch["gen_safe_block_ids"] == RESPONSE_BLOCK_ID).any(dim=1).float()
has_response = has_response > 0.5
has_prompt_only = bool((~has_response).any().item())
has_with_response = bool(has_response.any().item())
batch_size = batch["gen_safe_input_ids"].shape[0]
if has_prompt_only and has_with_response:
scores = torch.zeros(batch_size, device=device)
prompt_mask = ~has_response
response_mask = has_response
if prompt_mask.any():
scores[prompt_mask] = _score_batch_masked_scope_tiled(
model,
_slice_batch_rows(batch, prompt_mask),
batch_idx,
t_values,
pad_id,
response_mask_scale=response_mask_scale,
adaptive_t_repeats=adaptive_t_repeats,
adaptive_t_sigma_over_c=adaptive_t_sigma_over_c,
adaptive_t_min=adaptive_t_min,
adaptive_t_max=adaptive_t_max,
)
if response_mask.any():
scores[response_mask] = _score_batch_masked_scope_tiled(
model,
_slice_batch_rows(batch, response_mask),
batch_idx,
t_values,
pad_id,
response_mask_scale=response_mask_scale,
adaptive_t_repeats=adaptive_t_repeats,
adaptive_t_sigma_over_c=adaptive_t_sigma_over_c,
adaptive_t_min=adaptive_t_min,
adaptive_t_max=adaptive_t_max,
)
return scores.detach()
target_block_ids = (PROMPT_BLOCK_ID,) if has_prompt_only else (RESPONSE_BLOCK_ID,)
active_t_values = t_values
if adaptive_t_repeats is not None:
active_t_values = _adaptive_optimal_t_values(
batch,
target_block_ids=target_block_ids,
repeats=adaptive_t_repeats,
sigma_over_c=adaptive_t_sigma_over_c,
min_t=adaptive_t_min,
max_t=adaptive_t_max,
)
all_safe_masks = []
all_unsafe_masks = []
for t_idx, t_value in enumerate(active_t_values):
if has_prompt_only:
relative_indices, _ = _sample_relative_mask_indices(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
t_override=t_value,
seed=_mask_seed(batch, batch_idx, 1337, t_idx),
target_block_ids=(PROMPT_BLOCK_ID,),
)
else:
response_t_value = _scale_mask_ratio(t_value, response_mask_scale, 1e-3)
relative_indices, _ = _sample_relative_mask_indices(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
t_override=response_t_value,
seed=_mask_seed(batch, batch_idx, 7331, t_idx),
target_block_ids=(RESPONSE_BLOCK_ID,),
)
all_safe_masks.append(
_relative_to_absolute_mask(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
relative_indices,
target_block_ids=target_block_ids,
)
)
all_unsafe_masks.append(
_relative_to_absolute_mask(
batch["gen_unsafe_block_ids"],
batch["gen_unsafe_real_lengths"],
relative_indices,
target_block_ids=target_block_ids,
)
)
num_t_values = len(active_t_values)
def _repeat_batch_tensor(x: torch.Tensor) -> torch.Tensor:
return torch.cat([x] * num_t_values, dim=0)
repeated_safe_ids = _repeat_batch_tensor(batch["gen_safe_input_ids"])
repeated_unsafe_ids = _repeat_batch_tensor(batch["gen_unsafe_input_ids"])
repeated_safe_lengths = _repeat_batch_tensor(batch["gen_safe_real_lengths"])
repeated_unsafe_lengths = _repeat_batch_tensor(batch["gen_unsafe_real_lengths"])
tiled_safe_mask = torch.cat(all_safe_masks, dim=0)
tiled_unsafe_mask = torch.cat(all_unsafe_masks, dim=0)
if has_prompt_only:
full_response_safe_mask = _full_target_mask(
batch["gen_safe_block_ids"],
batch["gen_safe_real_lengths"],
target_block_ids=(RESPONSE_BLOCK_ID,),
)
full_response_unsafe_mask = _full_target_mask(
batch["gen_unsafe_block_ids"],
batch["gen_unsafe_real_lengths"],
target_block_ids=(RESPONSE_BLOCK_ID,),
)
scope_safe_mask = tiled_safe_mask | _repeat_batch_tensor(full_response_safe_mask)
scope_unsafe_mask = tiled_unsafe_mask | _repeat_batch_tensor(full_response_unsafe_mask)
else:
scope_safe_mask = tiled_safe_mask
scope_unsafe_mask = tiled_unsafe_mask
safe_ids, safe_logits, unsafe_ids, unsafe_logits = _compute_paired_masked_token_outputs(
model,
repeated_safe_ids,
scope_safe_mask,
repeated_safe_lengths,
repeated_unsafe_ids,
scope_unsafe_mask,
repeated_unsafe_lengths,
pad_id,
)
max_len = safe_ids.shape[1]
tiled_safe_mask = _pad_mask_to_match_length(tiled_safe_mask, max_len)
tiled_unsafe_mask = _pad_mask_to_match_length(tiled_unsafe_mask, max_len)
_, safe_logp = _compute_masked_token_losses_from_logits(
safe_logits,
safe_ids,
tiled_safe_mask,
)
_, unsafe_logp = _compute_masked_token_losses_from_logits(
unsafe_logits,
unsafe_ids,
tiled_unsafe_mask,
)
return (unsafe_logp - safe_logp).view(num_t_values, batch_size).mean(dim=0).detach()
def _select_eval_labels_and_valid_mask(batch, *, masked_scope: bool):
"""Choose labels/masks to match prompt-only or response-conditioned scoring."""
if masked_scope:
has_response = batch.get("has_response")
prompt_labels = batch.get("prompt_is_unsafe")
prompt_valid = batch.get("prompt_label_mask")
response_labels = batch.get("response_is_unsafe")
response_valid = batch.get("response_label_mask")
if (
has_response is not None
and prompt_labels is not None
and prompt_valid is not None
and response_labels is not None
and response_valid is not None
):
has_response_mask = has_response > 0.5
labels = torch.where(has_response_mask, response_labels, prompt_labels)
valid = torch.where(has_response_mask, response_valid > 0.5, prompt_valid > 0.5)
return labels, valid
labels = batch["is_unsafe"]
valid = batch.get("conv_label_mask", torch.ones_like(labels)) > 0.5
return labels, valid
@torch.no_grad()
def collect_eval_scores(
model,
eval_loader,
device,
t_values=(0.15, 0.15, 0.15),
pad_id: int = 0,
tile_t_values: bool = True,
masked_scope: bool = True,
response_mask_scale: float = 1.0,
return_indices: bool = False,
adaptive_t_repeats: int | None = None,
adaptive_t_sigma_over_c: float = 1.0,
adaptive_t_min: float = 1e-3,
adaptive_t_max: float = 0.5,
progress: bool = True,
):
"""Collect masked-scope unsafe-minus-safe scores for an eval loader."""
if not masked_scope:
raise ValueError("This release supports masked_scope=True scoring only.")
model.eval()
apply_eval_inference_fastpaths(model)
y_true = []
scores = []
example_indices = []
for batch_idx, batch in enumerate(tqdm(eval_loader, desc="LLaDA eval", disable=not progress)):
batch = {key: value.to(device, non_blocking=True) for key, value in batch.items()}
if tile_t_values:
try:
batch_scores = _score_batch_masked_scope_tiled(
model,
batch,
batch_idx,
t_values,
pad_id,
response_mask_scale=response_mask_scale,
adaptive_t_repeats=adaptive_t_repeats,
adaptive_t_sigma_over_c=adaptive_t_sigma_over_c,
adaptive_t_min=adaptive_t_min,
adaptive_t_max=adaptive_t_max,
)
except torch.cuda.OutOfMemoryError:
torch.cuda.empty_cache()
batch_scores = _score_batch_masked_scope_sequential(
model,
batch,
batch_idx,
t_values,
pad_id,
response_mask_scale=response_mask_scale,
adaptive_t_repeats=adaptive_t_repeats,
adaptive_t_sigma_over_c=adaptive_t_sigma_over_c,
adaptive_t_min=adaptive_t_min,
adaptive_t_max=adaptive_t_max,
)
else:
batch_scores = _score_batch_masked_scope_sequential(
model,
batch,
batch_idx,
t_values,
pad_id,
response_mask_scale=response_mask_scale,
adaptive_t_repeats=adaptive_t_repeats,
adaptive_t_sigma_over_c=adaptive_t_sigma_over_c,
adaptive_t_min=adaptive_t_min,
adaptive_t_max=adaptive_t_max,
)
labels, valid = _select_eval_labels_and_valid_mask(batch, masked_scope=masked_scope)
if not valid.any():
continue
scores.extend(batch_scores[valid].detach().cpu().tolist())
y_true.extend(labels[valid].detach().cpu().tolist())
if return_indices:
example_indices.extend(batch["example_index"][valid].detach().cpu().tolist())
if return_indices:
return y_true, scores, example_indices
return y_true, scores