Download src/diffusion_lm/diffusion.py from goldenfox/marimo-diffusion: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/goldenfox/marimo-diffusion/resolve/main/src/diffusion_lm/diffusion.py
- Command line
-
hf download hf://goldenfox/marimo-diffusion/src/diffusion_lm/diffusion.py
-
curl -L -o diffusion.py https://huggingface.co/goldenfox/marimo-diffusion/resolve/main/src/diffusion_lm/diffusion.py
12.5 kB
| """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: ... | |
| class CorruptionBatch: | |
| noisy_tokens: Tensor | |
| mask: Tensor | |
| mask_probability: Tensor | |
| valid_mask: Tensor | |
| class LossOutput: | |
| loss: Tensor | |
| masked_accuracy: Tensor | |
| masked_tokens: int | |
| 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: | |
| # MPS has no float64 kernels. Preserve fp64 categorical sampling by | |
| # moving only the sampling calculation to CPU on Apple Silicon. | |
| 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 | |
| # This function is itself a generator, so a decorator would leave the | |
| # inference context before iteration begins. Scope it around each pass. | |
| with torch.inference_mode(): | |
| if logits_fn is not None: | |
| logits = logits_fn(tokens, masked) | |
| else: | |
| # Kept as a conditional kwarg so mask-free denoiser doubles stay valid. | |
| 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": | |
| # Linear t grid: P(unmask from t to s | still masked) = 1 - s/t. | |
| 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" | |
| ) | |
| 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: # Defensive: the iterator always yields its initial state. | |
| raise RuntimeError("sampler produced no state") | |
| return final_state.tokens | |