| """Absorbing-mask forward corruption, objective, and reverse samplers.""" |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from collections.abc import Iterator |
| from typing import Callable, Literal, Protocol |
|
|
| import torch |
| import torch.nn.functional as F |
| from torch import Tensor |
|
|
|
|
| class Denoiser(Protocol): |
| config: object |
|
|
| def __call__( |
| self, |
| input_ids: Tensor, |
| attention_mask: Tensor | None = None, |
| output_positions: Tensor | None = None, |
| attn_mask: Tensor | None = None, |
| ) -> Tensor: ... |
|
|
|
|
| @dataclass(frozen=True) |
| class CorruptionBatch: |
| noisy_tokens: Tensor |
| mask: Tensor |
| mask_probability: Tensor |
| valid_mask: Tensor |
|
|
|
|
| @dataclass(frozen=True) |
| class LossOutput: |
| loss: Tensor |
| masked_accuracy: Tensor |
| masked_tokens: int |
|
|
|
|
| @dataclass(frozen=True) |
| class UnmaskStep: |
| """One observable state of the reverse diffusion process.""" |
|
|
| step: int |
| total_steps: int |
| tokens: Tensor |
| masked_remaining: int |
|
|
|
|
| def sample_mask_probabilities( |
| batch_size: int, |
| *, |
| device: torch.device | str, |
| eps: float = 1e-3, |
| low_discrepancy: bool = True, |
| generator: torch.Generator | None = None, |
| ) -> Tensor: |
| """Sample linear noise levels in ``[eps, 1]``. |
| |
| A random cyclic shift of an evenly spaced grid preserves uniform marginals |
| while covering the complete noise range in every reasonably sized batch. |
| """ |
|
|
| if batch_size <= 0: |
| raise ValueError("batch_size must be positive") |
| if not 0.0 < eps < 1.0: |
| raise ValueError("eps must be in (0, 1)") |
|
|
| if low_discrepancy: |
| offset = torch.rand((), device=device, generator=generator) |
| unit = (offset + torch.arange(batch_size, device=device) / batch_size) % 1.0 |
| else: |
| unit = torch.rand(batch_size, device=device, generator=generator) |
| return eps + (1.0 - eps) * unit |
|
|
|
|
| def corrupt_tokens( |
| clean_tokens: Tensor, |
| mask_token_id: int, |
| *, |
| valid_mask: Tensor | None = None, |
| mask_probability: Tensor | None = None, |
| eps: float = 1e-3, |
| low_discrepancy: bool = True, |
| generator: torch.Generator | None = None, |
| ) -> CorruptionBatch: |
| """Apply the absorbing forward process at one random time per sequence.""" |
|
|
| if clean_tokens.ndim != 2: |
| raise ValueError("clean_tokens must have shape [batch, sequence]") |
| batch_size, _ = clean_tokens.shape |
| if valid_mask is None: |
| valid_mask = torch.ones_like(clean_tokens, dtype=torch.bool) |
| elif valid_mask.shape != clean_tokens.shape: |
| raise ValueError("valid_mask must match clean_tokens") |
| else: |
| valid_mask = valid_mask.bool() |
|
|
| if mask_probability is None: |
| mask_probability = sample_mask_probabilities( |
| batch_size, |
| device=clean_tokens.device, |
| eps=eps, |
| low_discrepancy=low_discrepancy, |
| generator=generator, |
| ) |
| else: |
| mask_probability = torch.as_tensor( |
| mask_probability, device=clean_tokens.device, dtype=torch.float32 |
| ) |
| if mask_probability.ndim == 0: |
| mask_probability = mask_probability.repeat(batch_size) |
| if mask_probability.shape != (batch_size,): |
| raise ValueError("mask_probability must be scalar or have shape [batch]") |
| if bool(((mask_probability <= 0) | (mask_probability > 1)).any()): |
| raise ValueError("mask probabilities must be in (0, 1]") |
|
|
| random_values = torch.rand(clean_tokens.shape, device=clean_tokens.device, generator=generator) |
| mask = (random_values < mask_probability[:, None]) & valid_mask |
| noisy_tokens = torch.where(mask, mask_token_id, clean_tokens) |
| return CorruptionBatch(noisy_tokens, mask, mask_probability, valid_mask) |
|
|
|
|
| def diffusion_cross_entropy( |
| logits: Tensor, |
| clean_tokens: Tensor, |
| corruption: CorruptionBatch, |
| ) -> LossOutput: |
| """Compute the continuous-time masked-diffusion likelihood bound. |
| |
| ``logits`` may contain all positions as ``[B, L, V]`` or only the masked |
| positions as ``[N_masked, V]``. The latter is substantially more memory |
| efficient for small models with non-trivial vocabularies. |
| """ |
|
|
| if clean_tokens.shape != corruption.noisy_tokens.shape: |
| raise ValueError("clean_tokens must match the corruption batch") |
| targets = clean_tokens[corruption.mask] |
| if logits.ndim == 3: |
| if logits.shape[:2] != clean_tokens.shape: |
| raise ValueError("full logits must have shape [batch, sequence, vocab]") |
| selected_logits = logits[corruption.mask] |
| elif logits.ndim == 2: |
| selected_logits = logits |
| else: |
| raise ValueError("logits must have shape [B, L, V] or [N_masked, V]") |
| if selected_logits.shape[0] != targets.numel(): |
| raise ValueError("selected logits count does not match the number of masked tokens") |
|
|
| masked_tokens = int(targets.numel()) |
| if masked_tokens == 0: |
| zero = logits.sum() * 0.0 |
| return LossOutput(zero, zero.detach(), 0) |
|
|
| per_token = F.cross_entropy(selected_logits.float(), targets, reduction="none") |
| probabilities = corruption.mask_probability[:, None].expand_as(clean_tokens) |
| weights = probabilities[corruption.mask].reciprocal() |
| normalizer = corruption.valid_mask.sum().clamp_min(1) |
| loss = (per_token * weights).sum() / normalizer |
| accuracy = (selected_logits.argmax(dim=-1) == targets).float().mean() |
| return LossOutput(loss, accuracy, masked_tokens) |
|
|
|
|
| def _sample_categorical( |
| logits: Tensor, |
| temperature: float, |
| generator: torch.Generator | None, |
| ) -> tuple[Tensor, Tensor]: |
| """Sample with fp64 Gumbel noise and return token ids plus model confidence.""" |
|
|
| if temperature < 0: |
| raise ValueError("temperature must be non-negative") |
| log_probs = F.log_softmax(logits.float(), dim=-1) |
| if temperature == 0: |
| tokens = logits.argmax(dim=-1) |
| else: |
| |
| |
| sampling_device = torch.device("cpu") if logits.device.type == "mps" else logits.device |
| if logits.device.type == "mps": |
| logits64 = logits.float().cpu().double() / temperature |
| else: |
| logits64 = logits.double() / temperature |
| sampling_generator = generator |
| if generator is not None and generator.device != sampling_device: |
| sampling_generator = None |
| uniform = torch.rand( |
| logits64.shape, |
| device=sampling_device, |
| dtype=torch.float64, |
| generator=sampling_generator, |
| ).clamp_(1e-12, 1.0 - 1e-12) |
| gumbel = -torch.log(-torch.log(uniform)) |
| tokens = (logits64 + gumbel).argmax(dim=-1).to(logits.device) |
| confidence = log_probs.gather(-1, tokens[:, None]).squeeze(-1).exp() |
| return tokens, confidence |
|
|
|
|
| def iterative_unmask_steps( |
| model: Denoiser, |
| input_ids: Tensor, |
| mask_token_id: int, |
| *, |
| steps: int = 64, |
| temperature: float = 1.0, |
| strategy: Literal["ancestral", "confidence", "left_to_right"] = "ancestral", |
| blocked_token_ids: tuple[int, ...] = (), |
| attn_mask: Tensor | None = None, |
| generator: torch.Generator | None = None, |
| logits_fn: Callable[[Tensor, Tensor], Tensor] | None = None, |
| ) -> Iterator[UnmaskStep]: |
| """Yield each state while filling masks and clamping visible prompt tokens. |
| |
| ``ancestral`` implements the absorbing reverse transition from mask rate |
| ``t`` to ``s``. ``confidence`` reveals an equal-sized highest-confidence |
| group on each pass; it is faster-looking and often useful, but is a heuristic. |
| ``left_to_right`` reveals equal-sized position-ordered groups, which keeps |
| arithmetic left operands visible before their results are committed. |
| |
| ``logits_fn(tokens, masked)`` overrides how predictions are obtained, so a caller |
| holding a key/value cache can score only the masked window instead of the whole |
| sequence. The revealing schedule is unchanged either way. |
| """ |
|
|
| if input_ids.ndim != 2: |
| raise ValueError("input_ids must have shape [batch, sequence]") |
| if steps <= 0: |
| raise ValueError("steps must be positive") |
| if strategy not in {"ancestral", "confidence", "left_to_right"}: |
| raise ValueError("strategy must be ancestral, confidence, or left_to_right") |
|
|
| tokens = input_ids.clone() |
| batch_size, _ = tokens.shape |
| yield UnmaskStep(0, steps, tokens.detach(), int(tokens.eq(mask_token_id).sum())) |
|
|
| for step in range(steps): |
| masked = tokens.eq(mask_token_id) |
| if not bool(masked.any()): |
| break |
|
|
| |
| |
| with torch.inference_mode(): |
| if logits_fn is not None: |
| logits = logits_fn(tokens, masked) |
| else: |
| |
| extra = {} if attn_mask is None else {"attn_mask": attn_mask} |
| logits = model(tokens, output_positions=masked, **extra) |
| if blocked_token_ids: |
| logits = logits.clone() |
| for token_id in blocked_token_ids: |
| logits[:, token_id] = torch.finfo(logits.dtype).min |
| predictions, confidence = _sample_categorical(logits, temperature, generator) |
|
|
| proposed = tokens.clone() |
| proposed[masked] = predictions |
| reveal = torch.zeros_like(masked) |
| steps_left = steps - step |
|
|
| if strategy == "ancestral": |
| |
| reveal_probability = 1.0 / steps_left |
| reveal = ( |
| torch.rand(tokens.shape, device=tokens.device, generator=generator) |
| < reveal_probability |
| ) & masked |
| elif strategy == "left_to_right": |
| for row in range(batch_size): |
| masked_positions = masked[row].nonzero(as_tuple=True)[0] |
| remaining = int(masked_positions.numel()) |
| count = (remaining + steps_left - 1) // steps_left |
| if count: |
| reveal[row, masked_positions[:count]] = True |
| else: |
| confidence_grid = torch.full( |
| tokens.shape, |
| -torch.inf, |
| device=tokens.device, |
| dtype=confidence.dtype, |
| ) |
| confidence_grid[masked] = confidence |
| for row in range(batch_size): |
| remaining = int(masked[row].sum()) |
| count = (remaining + steps_left - 1) // steps_left |
| if count: |
| positions = confidence_grid[row].topk(count).indices |
| reveal[row, positions] = True |
|
|
| tokens = torch.where(reveal, proposed, tokens) |
|
|
| yield UnmaskStep( |
| step + 1, |
| steps, |
| tokens.detach(), |
| int(tokens.eq(mask_token_id).sum()), |
| ) |
|
|
| if bool(tokens.eq(mask_token_id).any()): |
| raise RuntimeError( |
| "sampler finished with masked positions; this indicates an internal error" |
| ) |
|
|
|
|
| @torch.no_grad() |
| def iterative_unmask( |
| model: Denoiser, |
| input_ids: Tensor, |
| mask_token_id: int, |
| *, |
| steps: int = 64, |
| temperature: float = 1.0, |
| strategy: Literal["ancestral", "confidence"] = "ancestral", |
| blocked_token_ids: tuple[int, ...] = (), |
| attn_mask: Tensor | None = None, |
| generator: torch.Generator | None = None, |
| logits_fn: Callable[[Tensor, Tensor], Tensor] | None = None, |
| ) -> Tensor: |
| """Return the final state from :func:`iterative_unmask_steps`.""" |
|
|
| final_state: UnmaskStep | None = None |
| for state in iterative_unmask_steps( |
| model, |
| input_ids, |
| mask_token_id, |
| steps=steps, |
| temperature=temperature, |
| strategy=strategy, |
| blocked_token_ids=blocked_token_ids, |
| attn_mask=attn_mask, |
| generator=generator, |
| logits_fn=logits_fn, |
| ): |
| final_state = state |
| if final_state is None: |
| raise RuntimeError("sampler produced no state") |
| return final_state.tokens |
|
|