diffusion-51M-base / modeling_diffusion_lm.py
d0rj's picture
Publish evaluated diffusion v2 with PLL intervals and TensorBoard traces
80aea5b verified
Raw
History Blame Contribute Delete
16.9 kB
"""Modeling for DiffusionLM: masked (absorbing-state) discrete diffusion
language model, LLaDA / Diffusion-LM style.
Bidirectional transformer over a sequence where a fraction t of tokens is
replaced by a learned [MASK] embedding. Time conditioning is configurable:
legacy additive, bounded normalized, or absent. The model predicts the original token
at masked positions. Training loss: masked-position cross-entropy weighted
by 1/t and normalized by source token count, with t ~ U(0, 1).
Generation: iterative denoising from all-[MASK]; at each step the most
confident tokens are committed (confidence = max softmax prob), the rest stay
masked.
Follows the transformers v5 modeling pattern where applicable
(masking_utils, GradientCheckpointingLayer).
"""
import math
from collections.abc import Callable
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers.masking_utils import create_bidirectional_mask
from transformers.modeling_layers import GradientCheckpointingLayer
from transformers.modeling_outputs import MaskedLMOutput
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
from transformers.processing_utils import Unpack
from transformers.utils import TransformersKwargs
from .configuration_diffusion_lm import DiffusionLMConfig
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def _apply_rotary_pos_emb(x, cos, sin):
cos = cos.unsqueeze(1)
sin = sin.unsqueeze(1)
return x * cos + _rotate_half(x) * sin
class DiffusionRMSNorm(nn.Module):
def __init__(self, hidden_size: int, eps: float = 1e-6) -> None:
super().__init__()
self.weight = nn.Parameter(torch.ones(hidden_size))
self.variance_epsilon = eps
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
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)
return (self.weight * hidden_states.to(input_dtype)).to(input_dtype)
class DiffusionRotaryEmbedding(nn.Module):
def __init__(self, config: DiffusionLMConfig, device=None):
super().__init__()
self.config = config
inv_freq = 1.0 / (
config.rope_theta
** (torch.arange(0, config.head_dim, 2, dtype=torch.float32, device=device) / config.head_dim)
)
self.inv_freq = nn.Buffer(inv_freq, persistent=True)
@torch.no_grad()
def forward(self, x, position_ids):
inv_freq_expanded = (
self.inv_freq[None, :, None].expand(position_ids.shape[0], -1, 1)
.to(dtype=torch.float32, device=x.device)
)
position_ids_expanded = position_ids[:, None, :].float()
freqs = (inv_freq_expanded @ position_ids_expanded).transpose(1, 2)
emb = torch.cat((freqs, freqs), dim=-1)
return emb.cos().to(dtype=x.dtype), emb.sin().to(dtype=x.dtype)
def _eager_attention_forward(
module: nn.Module,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attention_mask: torch.Tensor | None,
scaling: float,
dropout: float = 0.0,
**kwargs: Unpack[TransformersKwargs],
):
attn_weights = torch.matmul(query, key.transpose(2, 3)) * scaling
if attention_mask is not None:
attn_weights = attn_weights + attention_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)
attn_output = attn_output.transpose(1, 2).contiguous()
return attn_output, attn_weights
def _sdpa_attention_forward(
module: nn.Module,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attention_mask: torch.Tensor | None,
scaling: float,
dropout: float = 0.0,
**kwargs: Unpack[TransformersKwargs],
):
"""Local SDPA wrapper. HF's generic SDPA interface falls back to the math
backend for GQA shapes (q heads != kv heads), which materializes
(batch, heads, seq, seq) fp32 attention scores. Expanding kv first keeps
the fused flash / memory-efficient kernels eligible."""
n_rep = getattr(module, "num_key_value_groups", 1)
is_causal = (
attention_mask is None
and getattr(module, "is_causal", False)
and query.shape[2] > 1
)
attn_output = nn.functional.scaled_dot_product_attention(
query,
key,
value,
attn_mask=attention_mask,
dropout_p=dropout,
is_causal=is_causal,
scale=scaling,
)
return attn_output.transpose(1, 2).contiguous(), None
class DiffusionAttention(nn.Module):
def __init__(self, config: DiffusionLMConfig, layer_idx: int):
super().__init__()
self.config = config
self.layer_idx = layer_idx
self.head_dim = config.head_dim
self.num_heads = config.num_attention_heads
self.scaling = self.head_dim**-0.5
self.attention_dropout = config.attention_dropout
self.is_causal = False
inner = self.num_heads * self.head_dim
self.q_proj = nn.Linear(config.hidden_size, inner, bias=False)
self.k_proj = nn.Linear(config.hidden_size, inner, bias=False)
self.v_proj = nn.Linear(config.hidden_size, inner, bias=False)
self.o_proj = nn.Linear(inner, config.hidden_size, bias=False)
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
attention_mask: torch.Tensor | None = None,
**kwargs: Unpack[TransformersKwargs],
) -> tuple[torch.Tensor, torch.Tensor]:
input_shape = hidden_states.shape[:-1]
hidden_shape = (*input_shape, -1, self.head_dim)
q = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2)
k = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2)
v = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
cos, sin = position_embeddings
q = _apply_rotary_pos_emb(q, cos, sin)
k = _apply_rotary_pos_emb(k, cos, sin)
if self.config._attn_implementation == "sdpa":
attention_interface: Callable = _sdpa_attention_forward
else:
attention_interface = ALL_ATTENTION_FUNCTIONS.get_interface(
self.config._attn_implementation, _eager_attention_forward
)
attn_output, attn_weights = attention_interface(
self,
q, k, v,
attention_mask,
dropout=0.0 if not self.training else self.attention_dropout,
scaling=self.scaling,
**kwargs,
)
attn_output = attn_output.reshape(*input_shape, -1).contiguous()
attn_output = self.o_proj(attn_output)
return attn_output, attn_weights
class DiffusionMLP(nn.Module):
def __init__(self, config: DiffusionLMConfig):
super().__init__()
self.gate_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
self.up_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
def forward(self, hidden_states):
return self.down_proj(F.silu(self.gate_proj(hidden_states)) * self.up_proj(hidden_states))
class DiffusionLayer(GradientCheckpointingLayer):
def __init__(self, config: DiffusionLMConfig, layer_idx: int):
super().__init__()
self.hidden_size = config.hidden_size
self.self_attn = DiffusionAttention(config, layer_idx)
self.mlp = DiffusionMLP(config)
self.input_layernorm = DiffusionRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = DiffusionRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
**kwargs: Unpack[TransformersKwargs],
) -> torch.Tensor:
hidden_states = hidden_states + self.self_attn(
self.input_layernorm(hidden_states),
position_embeddings=position_embeddings,
attention_mask=attention_mask,
**kwargs,
)[0]
hidden_states = hidden_states + self.mlp(self.post_attention_layernorm(hidden_states))
return hidden_states
class DiffusionLMPreTrainedModel(PreTrainedModel):
config_class = DiffusionLMConfig
base_model_prefix = "model"
supports_gradient_checkpointing = True
_no_split_modules = ["DiffusionLayer"]
_supports_sdpa = True
_supports_flash_attn = False
_supports_flex_attn = False
class DiffusionLMModel(DiffusionLMPreTrainedModel):
def __init__(self, config: DiffusionLMConfig):
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)
# learned [MASK] vector used for masked positions
self.mask_embedding = nn.Parameter(torch.zeros(1, config.hidden_size))
self.layers = nn.ModuleList(
[DiffusionLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
)
self.norm = DiffusionRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.rotary_emb = DiffusionRotaryEmbedding(config=config)
self.timestep_proj = nn.Sequential(
nn.Linear(config.hidden_size, config.hidden_size),
nn.SiLU(),
nn.Linear(config.hidden_size, config.hidden_size),
)
self.gradient_checkpointing = False
self.post_init()
@staticmethod
def _timestep_embedding(timesteps: torch.Tensor, dim: int, max_period: int = 10000) -> torch.Tensor:
half = dim // 2
exponent = -math.log(max_period) * torch.arange(
half, dtype=torch.float32, device=timesteps.device
)
emb = torch.exp(exponent / half)
emb = timesteps.float()[:, None] * emb[None, :]
return torch.cat([emb.cos(), emb.sin()], dim=-1)
def forward(
self,
input_ids: torch.LongTensor,
timesteps: torch.Tensor,
attention_mask: torch.Tensor | None = None,
position_ids: torch.LongTensor | None = None,
**kwargs: Unpack[TransformersKwargs],
):
inputs_embeds = self.embed_tokens(input_ids.clamp(0, self.config.vocab_size - 1))
is_mask = (input_ids == self.config.mask_token_id).unsqueeze(-1)
inputs_embeds = torch.where(
is_mask, self.mask_embedding.to(inputs_embeds.dtype), inputs_embeds
)
# Preserve legacy checkpoint behavior; the recovery recipe disables this
# branch to avoid a shared time vector dominating token content.
if self.config.time_conditioning != 'none':
t_emb = self._timestep_embedding(timesteps, self.config.hidden_size)
t_emb = self.timestep_proj(t_emb.to(self.timestep_proj[0].weight.dtype)).to(inputs_embeds.dtype)
if self.config.time_conditioning == 'normalized':
t_emb = F.normalize(t_emb.float(), dim=-1) * (self.config.hidden_size ** .5 * self.config.time_conditioning_scale)
t_emb = t_emb.to(inputs_embeds.dtype)
inputs_embeds = inputs_embeds + t_emb[:, None, :]
if position_ids is None:
position_ids = torch.arange(inputs_embeds.shape[1], device=inputs_embeds.device).unsqueeze(0)
# Fully bidirectional unpadded attention needs no mask, even in a
# compiled graph. A dense all-true mask prevents the fastest SDPA path.
padding_mask = None if attention_mask is None else create_bidirectional_mask(
config=self.config,
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
)
position_embeddings = self.rotary_emb(inputs_embeds, position_ids=position_ids)
hidden_states = inputs_embeds
for layer in self.layers:
if self.gradient_checkpointing and self.training:
hidden_states = self._gradient_checkpointing_func(
layer.forward, hidden_states, padding_mask, position_embeddings
)
else:
hidden_states = layer(
hidden_states,
attention_mask=padding_mask,
position_embeddings=position_embeddings,
**kwargs,
)
hidden_states = self.norm(hidden_states)
return hidden_states
class DiffusionLMForMaskedLM(DiffusionLMPreTrainedModel):
_tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
def __init__(self, config: DiffusionLMConfig):
super().__init__(config)
self.model = DiffusionLMModel(config)
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
self.post_init()
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 forward(
self,
input_ids: torch.LongTensor,
timesteps: torch.Tensor,
labels: torch.LongTensor | None = None,
attention_mask: torch.Tensor | None = None,
**kwargs: Unpack[TransformersKwargs],
) -> MaskedLMOutput:
hidden_states = self.model(
input_ids=input_ids,
timesteps=timesteps,
attention_mask=attention_mask,
**kwargs,
)
logits = self.lm_head(hidden_states)
loss = None
if labels is not None:
token_loss = F.cross_entropy(
logits.float().view(-1, logits.size(-1)),
labels.view(-1),
ignore_index=-100,
reduction="none",
)
# Linear absorbing noise: E[sum(masked CE / t)] / source tokens.
loss = (token_loss.view_as(labels) / timesteps[:, None].clamp_min(1e-5)).mean()
return MaskedLMOutput(loss=loss, logits=logits)
@torch.no_grad()
def generate_masked(
self,
batch_size: int,
seq_len: int,
steps: int | None = None,
temperature: float = 0.0,
device: torch.device | str | None = None,
attention_mask: torch.Tensor | None = None,
) -> torch.Tensor:
"""Iterative denoising from all-[MASK] to a fully unmasked sequence.
Commit the most confident remaining predictions on a linear schedule.
Noise time decreases from one to zero; committed tokens never change.
"""
steps = self.config.num_diffusion_steps if steps is None else steps
if steps < 1 or temperature < 0:
raise ValueError("steps must be positive and temperature nonnegative")
device = device or next(self.parameters()).device
mask_id = self.config.mask_token_id
x = torch.full((batch_size, seq_len), mask_id, dtype=torch.long, device=device)
if attention_mask is None:
attention_mask = torch.ones_like(x)
valid = attention_mask.bool()
x.masked_fill_(~valid, self.config.pad_token_id)
lengths = valid.sum(dim=1)
for i in range(steps):
t = 1.0 - i / steps
timesteps = torch.full((batch_size,), t, device=device)
logits = self(input_ids=x, timesteps=timesteps, attention_mask=attention_mask).logits
probs = torch.softmax(logits.float(), dim=-1)
if temperature > 0:
sampling_probs = torch.softmax(logits.float() / temperature, dim=-1)
predicted = torch.multinomial(sampling_probs.reshape(-1, sampling_probs.shape[-1]), 1).reshape_as(x)
confidence = probs.gather(-1, predicted[..., None]).squeeze(-1)
else:
confidence, predicted = probs.max(dim=-1)
masked = (x == mask_id) & valid
remaining = masked.sum(dim=1)
desired_remaining = torch.floor(lengths * (1.0 - (i + 1) / steps)).long()
n_commit = (remaining - desired_remaining).clamp_min(0)
order = confidence.masked_fill(~masked, -torch.inf).argsort(dim=1, descending=True)
rank = order.argsort(dim=1)
commit = masked & (rank < n_commit[:, None])
x = torch.where(commit, predicted, x)
return x