| """Region-aware attention masks, objectives, and samplers for reasoning variants. |
| |
| Three inference modes share one ``DiffusionTransformer``: |
| |
| - ``ar``: prefix-LM. Bidirectional over the problem, causal generation after it. |
| - ``diffusion``: full-sequence denoising of the response region in parallel. |
| - ``hybrid``: thought slots denoised block-by-block, then the answer decoded |
| autoregressively under a bidirectional-prefix mask. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import time |
| from dataclasses import dataclass |
|
|
| import torch |
| import torch.nn.functional as F |
| from torch import Tensor |
|
|
| from diffusion_lm.diffusion import ( |
| CorruptionBatch, |
| corrupt_tokens, |
| iterative_unmask, |
| _sample_categorical, |
| ) |
| from diffusion_lm.model import DiffusionTransformer |
|
|
|
|
| def prefix_causal_blocked( |
| prefix_lens: Tensor, seq_len: int, *, causal_prefix: bool = False |
| ) -> Tensor: |
| """Blocked-attention mask: bidirectional before ``prefix_len``, causal after. |
| |
| ``allowed[b, i, j] = j < prefix_lens[b] or j <= i``; the returned tensor is the |
| inverse, matching the src_mask convention where ``True`` blocks attention. |
| ``causal_prefix=True`` degenerates to the plain causal mask — the geometry |
| pretrained autoregressive backbones were trained under. |
| """ |
|
|
| positions = torch.arange(seq_len, device=prefix_lens.device) |
| causal = positions[None, :, None] >= positions[None, None, :] |
| if causal_prefix: |
| return (~causal).expand(prefix_lens.shape[0], seq_len, seq_len) |
| prefix = positions[None, None, :] < prefix_lens[:, None, None] |
| return ~(prefix | causal) |
|
|
|
|
| def window_blocked(window_ends: Tensor, seq_len: int) -> Tensor: |
| """Blocked-attention mask hiding all keys at or beyond each sample's window end.""" |
|
|
| positions = torch.arange(seq_len, device=window_ends.device) |
| allowed = positions[None, None, :] < window_ends[:, None, None] |
| return ~allowed.expand(window_ends.shape[0], seq_len, seq_len) |
|
|
|
|
| def slot_causal_blocked( |
| problem_len: Tensor, n_slots: Tensor, block: int, seq_len: int |
| ) -> Tensor: |
| """Block-causal mask over thought slots: each slot attends its prefix slots only. |
| |
| Problem tokens (plus ``<think>``) attend the problem window; tokens of slot k |
| attend everything up to slot k's end; positions past the think region attend |
| the whole think window. Denoising slot k at inference with clean prefix slots |
| is the per-slot ``t -> 0`` limit of the training distribution. |
| """ |
|
|
| device = problem_len.device |
| positions = torch.arange(seq_len, device=device)[None, :] |
| prefix_end = (problem_len + 1)[:, None] |
| think_end = prefix_end + n_slots[:, None] * block |
| slot_index = torch.clamp((positions - prefix_end) // block, min=0) |
| slot_end = prefix_end + (slot_index + 1) * block |
| window = torch.where(positions < prefix_end, prefix_end, slot_end) |
| window = torch.where(positions >= think_end, think_end, window) |
| allowed = positions[:, None, :] < window[:, :, None] |
| return ~allowed |
|
|
|
|
| def adaptive_block_mask( |
| tokens: Tensor, |
| problem_len: Tensor, |
| answer_start: Tensor, |
| size_ids: Tensor, |
| end_think_id: int, |
| *, |
| causal_prefix: bool = False, |
| ) -> Tensor: |
| """Block-causal mask over variable-length thought blocks. |
| |
| Block boundaries are read directly from the token stream: every ``<szN>`` |
| control token and the terminal ``</think>`` mark the start of the next region. |
| A thought-content token attends its clean prefix plus its own block |
| bidirectionally, never a following block; problem and ``<think>`` positions see |
| the problem window only — or, with ``causal_prefix=True``, only their causal |
| past, matching a pretrained autoregressive backbone. The returned tensor |
| follows the src_mask convention where ``True`` blocks a key. |
| """ |
|
|
| positions = torch.arange(tokens.shape[1], device=tokens.device) |
| prefix_end = (problem_len + 1)[:, None] |
| in_think = (positions[None, :] >= prefix_end) & ( |
| positions[None, :] < answer_start[:, None] |
| ) |
| is_size = (tokens.unsqueeze(-1) == size_ids).any(dim=-1) |
| boundary = (is_size | (tokens == end_think_id)) & in_think |
| return block_mask_from_boundaries( |
| boundary, problem_len + 1, answer_start, causal_prefix=causal_prefix |
| ) |
|
|
|
|
| def block_mask_from_boundaries( |
| boundary: Tensor, |
| prefix_end: Tensor, |
| answer_start: Tensor, |
| *, |
| causal_prefix: bool = False, |
| ) -> Tensor: |
| """Blocked mask from per-position block-start marks (the adaptive-mask core). |
| |
| ``boundary[b, p]`` is True where a new block starts. A position attends every key |
| before the next boundary after it: its own block bidirectionally plus all preceding |
| context. Rows before ``prefix_end`` see the prefix window (or their causal past with |
| ``causal_prefix=True``); rows at or past ``answer_start`` see up to ``answer_start``. |
| """ |
|
|
| device = boundary.device |
| batch_size, seq_len = boundary.shape |
| positions = torch.arange(seq_len, device=device) |
| prefix_end = prefix_end[:, None] |
| answer_start = answer_start[:, None] |
|
|
| boundary_index = torch.where( |
| boundary, positions[None, :].expand(batch_size, seq_len), seq_len |
| ) |
| reverse_cummin = boundary_index.flip(1).cummin(dim=1).values.flip(1) |
| next_boundary = torch.full((batch_size, seq_len), seq_len, device=device) |
| next_boundary[:, :-1] = reverse_cummin[:, 1:] |
|
|
| if causal_prefix: |
| prefix_limit = (positions[None, :] + 1).expand(batch_size, seq_len) |
| else: |
| prefix_limit = prefix_end.expand(batch_size, seq_len) |
| attend_limit = next_boundary |
| attend_limit = torch.where(positions[None, :] < prefix_end, prefix_limit, attend_limit) |
| attend_limit = torch.where( |
| positions[None, :] >= answer_start, |
| answer_start.expand(batch_size, seq_len), |
| attend_limit, |
| ) |
| allowed = positions[None, None, :] < attend_limit[:, :, None] |
| return ~allowed |
|
|
|
|
| @dataclass(frozen=True) |
| class HybridBatchLoss: |
| loss: Tensor |
| think_loss: float |
| answer_loss: float |
| think_accuracy: float |
| answer_accuracy: float |
| think_samples: int |
| answer_samples: int |
| |
| |
| control_accuracy: float = 0.0 |
| stop_accuracy: float = 0.0 |
|
|
|
|
| def hybrid_objective( |
| model: DiffusionTransformer, |
| tokens: Tensor, |
| regions: Tensor, |
| *, |
| block: int, |
| think_probability: float, |
| mask_eps: float, |
| generator: torch.Generator | None = None, |
| ) -> HybridBatchLoss: |
| """Mixed objective: denoise all thought slots or predict the answer tokens. |
| |
| Each sample is assigned one mode. Think samples corrupt every slot at an |
| independent noise level under a block-causal mask, so one forward trains all |
| slot conditionals; answer samples see the full clean reasoning prefix |
| bidirectionally and the answer causally. |
| """ |
|
|
| device = tokens.device |
| batch_size, seq_len = tokens.shape |
| problem_len, n_slots, answer_start, answer_end = regions.unbind(dim=1) |
| positions = torch.arange(seq_len, device=device) |
|
|
| think_sel = ( |
| torch.rand(batch_size, device=device, generator=generator) < think_probability |
| ) |
| if bool(think_sel.all()): |
| think_sel[-1] = False |
| if not bool(think_sel.any()): |
| think_sel[0] = True |
|
|
| prefix_end = problem_len + 1 |
| think_end = prefix_end + n_slots * block |
| think_region = ( |
| (positions[None, :] >= prefix_end[:, None]) |
| & (positions[None, :] < think_end[:, None]) |
| & think_sel[:, None] |
| ) |
| max_slots = int(n_slots.max()) |
| slot_noise = mask_eps + (1.0 - mask_eps) * torch.rand( |
| batch_size, max_slots, device=device, generator=generator |
| ) |
| slot_index = torch.clamp( |
| (positions[None, :] - prefix_end[:, None]) // block, min=0, max=max_slots - 1 |
| ) |
| token_noise = slot_noise.gather(1, slot_index) |
| mask = ( |
| torch.rand(tokens.shape, device=device, generator=generator) < token_noise |
| ) & think_region |
| noisy_tokens = torch.where(mask, model.config.mask_token_id, tokens) |
|
|
| think_blocked = slot_causal_blocked(problem_len, n_slots, block, seq_len) |
| answer_blocked = prefix_causal_blocked(answer_start, seq_len) |
| blocked = torch.where(think_sel[:, None, None], think_blocked, answer_blocked) |
|
|
| predict_positions = ( |
| (positions[None, :] >= answer_start[:, None] - 1) |
| & (positions[None, :] < answer_end[:, None] - 1) |
| & ~think_sel[:, None] |
| ) |
| output_positions = mask | predict_positions |
| logits = model(noisy_tokens, output_positions=output_positions, attn_mask=blocked) |
|
|
| think_rows = mask[output_positions] |
| |
| |
| zero = logits.sum().clamp(-1.0, 1.0) * 0.0 |
|
|
| think_loss = zero |
| think_accuracy = 0.0 |
| if bool(mask.any()): |
| think_logits = logits[think_rows] |
| think_targets = tokens[mask] |
| per_token = F.cross_entropy(think_logits.float(), think_targets, reduction='none') |
| weights = token_noise[mask] |
| normalizer = think_region.sum().clamp_min(1) |
| think_loss = (per_token / weights).sum() / normalizer |
| think_accuracy = float( |
| (think_logits.argmax(dim=-1) == think_targets).float().mean() |
| ) |
|
|
| answer_loss = zero |
| answer_accuracy = 0.0 |
| if bool(predict_positions.any()): |
| answer_logits = logits[~think_rows] |
| target_positions = torch.zeros_like(predict_positions) |
| target_positions[:, 1:] = predict_positions[:, :-1] |
| answer_targets = tokens[target_positions] |
| answer_loss = F.cross_entropy(answer_logits.float(), answer_targets) |
| answer_accuracy = float( |
| (answer_logits.argmax(dim=-1) == answer_targets).float().mean() |
| ) |
|
|
| return HybridBatchLoss( |
| loss=think_loss + answer_loss, |
| think_loss=float(think_loss), |
| answer_loss=float(answer_loss), |
| think_accuracy=think_accuracy, |
| answer_accuracy=answer_accuracy, |
| think_samples=int(think_sel.sum()), |
| answer_samples=int((~think_sel).sum()), |
| ) |
|
|
|
|
| def adaptive_hybrid_objective( |
| model: DiffusionTransformer, |
| tokens: Tensor, |
| regions: Tensor, |
| *, |
| size_ids: Tensor, |
| end_think_id: int, |
| think_probability: float, |
| mask_eps: float, |
| causal_prefix: bool = False, |
| control_context_noise: float = 0.0, |
| generator: torch.Generator | None = None, |
| ) -> HybridBatchLoss: |
| """Mixed objective for the adaptive layout. |
| |
| Think samples denoise every thought block at an independent noise level under |
| the variable-boundary block-causal mask. The remaining samples run a causal |
| next-token objective over the whole reasoning-and-answer stream, which is where |
| the model learns the ``<szN>`` block-size and ``</think>`` termination decisions. |
| |
| Those causal samples read a pristine think region, while at inference the controller |
| reads thoughts the model just wrote, complete with sampling damage. |
| ``control_context_noise`` closes that gap by swapping a fraction of their think tokens |
| for other tokens drawn from the batch: inputs degrade, targets stay clean. |
| """ |
|
|
| device = tokens.device |
| batch_size, seq_len = tokens.shape |
| problem_len, _, answer_start, answer_end = regions.unbind(dim=1) |
| positions = torch.arange(seq_len, device=device) |
|
|
| think_sel = ( |
| torch.rand(batch_size, device=device, generator=generator) < think_probability |
| ) |
| if bool(think_sel.all()): |
| think_sel[-1] = False |
| if not bool(think_sel.any()): |
| think_sel[0] = True |
|
|
| prefix_end = problem_len + 1 |
| in_think = (positions[None, :] >= prefix_end[:, None]) & ( |
| positions[None, :] < answer_start[:, None] |
| ) |
| is_size = (tokens.unsqueeze(-1) == size_ids).any(dim=-1) |
| boundary = (is_size | (tokens == end_think_id)) & in_think |
| think_content = in_think & ~boundary & think_sel[:, None] |
|
|
| block_id = torch.cumsum((is_size & in_think).long(), dim=1) |
| max_blocks = int(block_id.max().clamp(min=1)) |
| slot_noise = mask_eps + (1.0 - mask_eps) * torch.rand( |
| batch_size, max_blocks, device=device, generator=generator |
| ) |
| gather_index = (block_id - 1).clamp(min=0, max=max_blocks - 1) |
| token_noise = slot_noise.gather(1, gather_index) |
| mask = ( |
| torch.rand(tokens.shape, device=device, generator=generator) < token_noise |
| ) & think_content |
| noisy_tokens = torch.where(mask, model.config.mask_token_id, tokens) |
|
|
| if control_context_noise > 0.0: |
| |
| |
| control_content = in_think & ~boundary & ~think_sel[:, None] |
| corrupt = ( |
| torch.rand(tokens.shape, device=device, generator=generator) < control_context_noise |
| ) & control_content |
| flat = tokens.reshape(-1) |
| picks = torch.randint( |
| 0, flat.numel(), tokens.shape, device=device, generator=generator |
| ) |
| noisy_tokens = torch.where(corrupt, flat[picks], noisy_tokens) |
|
|
| think_blocked = adaptive_block_mask( |
| tokens, problem_len, answer_start, size_ids, end_think_id, |
| causal_prefix=causal_prefix, |
| ) |
| ar_blocked = prefix_causal_blocked(prefix_end, seq_len, causal_prefix=causal_prefix) |
| blocked = torch.where(think_sel[:, None, None], think_blocked, ar_blocked) |
|
|
| predict_positions = ( |
| (positions[None, :] >= problem_len[:, None]) |
| & (positions[None, :] < answer_end[:, None] - 1) |
| & ~think_sel[:, None] |
| ) |
| output_positions = mask | predict_positions |
| logits = model(noisy_tokens, output_positions=output_positions, attn_mask=blocked) |
|
|
| think_rows = mask[output_positions] |
| |
| |
| zero = logits.sum().clamp(-1.0, 1.0) * 0.0 |
|
|
| think_loss = zero |
| think_accuracy = 0.0 |
| if bool(mask.any()): |
| think_logits = logits[think_rows] |
| think_targets = tokens[mask] |
| per_token = F.cross_entropy(think_logits.float(), think_targets, reduction='none') |
| weights = token_noise[mask] |
| normalizer = think_content.sum().clamp_min(1) |
| think_loss = (per_token / weights).sum() / normalizer |
| think_accuracy = float( |
| (think_logits.argmax(dim=-1) == think_targets).float().mean() |
| ) |
|
|
| answer_loss = zero |
| answer_accuracy = 0.0 |
| control_accuracy = 0.0 |
| stop_accuracy = 0.0 |
| if bool(predict_positions.any()): |
| answer_logits = logits[~think_rows] |
| target_positions = torch.zeros_like(predict_positions) |
| target_positions[:, 1:] = predict_positions[:, :-1] |
| answer_targets = tokens[target_positions] |
| answer_loss = F.cross_entropy(answer_logits.float(), answer_targets) |
| control_ids = torch.cat( |
| [size_ids, torch.tensor([end_think_id], device=device, dtype=size_ids.dtype)] |
| ) |
| is_control = (answer_targets.unsqueeze(-1) == control_ids).any(dim=-1) |
| answer_accuracy = float( |
| (answer_logits.argmax(dim=-1) == answer_targets).float().mean() |
| ) |
| if bool(is_control.any()): |
| |
| |
| menu = answer_logits[is_control].index_select(-1, control_ids) |
| chosen = control_ids[menu.argmax(dim=-1)] |
| targets = answer_targets[is_control] |
| control_accuracy = float((chosen == targets).float().mean()) |
| stop_accuracy = float( |
| ((chosen == end_think_id) == (targets == end_think_id)).float().mean() |
| ) |
|
|
| return HybridBatchLoss( |
| loss=think_loss + answer_loss, |
| think_loss=float(think_loss), |
| answer_loss=float(answer_loss), |
| think_accuracy=think_accuracy, |
| answer_accuracy=answer_accuracy, |
| think_samples=int(think_sel.sum()), |
| answer_samples=int((~think_sel).sum()), |
| control_accuracy=control_accuracy, |
| stop_accuracy=stop_accuracy, |
| ) |
|
|
|
|
| def block_size_curriculum( |
| step: int | None, *, n_sizes: int, curriculum_steps: int |
| ) -> Tensor: |
| """Segment-size sampling weights: smallest-size-only ramping linearly to uniform. |
| |
| ``sizes`` are assumed ascending. ``step=None`` (evaluation) and a zero-length |
| curriculum both return the uniform end state so losses stay comparable across |
| checkpoints. |
| """ |
|
|
| uniform = torch.full((n_sizes,), 1.0 / n_sizes) |
| if step is None or curriculum_steps <= 0: |
| return uniform |
| progress = min(1.0, step / curriculum_steps) |
| smallest_only = torch.zeros(n_sizes) |
| smallest_only[0] = 1.0 |
| return smallest_only * (1.0 - progress) + uniform * progress |
|
|
|
|
| def block_diffusion_objective( |
| model: DiffusionTransformer, |
| tokens: Tensor, |
| *, |
| sizes: tuple[int, ...], |
| size_weights: Tensor, |
| mask_eps: float, |
| ar_probability: float = 0.0, |
| generator: torch.Generator | None = None, |
| ) -> HybridBatchLoss: |
| """Variable-block denoising over plain packed text (the conversion objective). |
| |
| Each sample is tiled with segments whose lengths are drawn from ``sizes`` under |
| ``size_weights``; every segment is corrupted at an independent noise level and |
| denoised in one forward under the block-causal geometry (own segment bidirectional, |
| all preceding segments visible). No control tokens exist in the stream — pretraining |
| teaches variable-block denoising only; control decisions are learned in SFT. With |
| probability ``ar_probability`` a sample instead runs plain causal next-token loss, |
| retaining the autoregressive ability that control and answer decoding rely on. |
| Reported as ``think_*`` (denoising) and ``answer_*`` (causal retention) metrics. |
| """ |
|
|
| device = tokens.device |
| batch_size, seq_len = tokens.shape |
| positions = torch.arange(seq_len, device=device) |
|
|
| ar_sel = ( |
| torch.rand(batch_size, device=device, generator=generator) < ar_probability |
| ) |
| if bool(ar_sel.all()): |
| ar_sel[0] = False |
| diff_sel = ~ar_sel |
|
|
| size_tensor = torch.tensor(sizes, device=device, dtype=torch.long) |
| max_segments = -(-seq_len // int(min(sizes))) |
| drawn_index = torch.multinomial( |
| size_weights.to(device).expand(batch_size, -1), |
| max_segments, |
| replacement=True, |
| generator=generator, |
| ) |
| drawn = size_tensor[drawn_index] |
| starts = torch.cumsum(drawn, dim=1) - drawn |
| valid = starts < seq_len |
| boundary_hits = torch.zeros(batch_size, seq_len, dtype=torch.long, device=device) |
| boundary_hits.scatter_add_(1, starts.clamp(max=seq_len - 1), valid.long()) |
| boundary = boundary_hits > 0 |
|
|
| segment_id = torch.cumsum(boundary.long(), dim=1) - 1 |
| segment_noise = mask_eps + (1.0 - mask_eps) * torch.rand( |
| batch_size, max_segments, device=device, generator=generator |
| ) |
| token_noise = segment_noise.gather(1, segment_id.clamp(max=max_segments - 1)) |
| mask = ( |
| torch.rand(tokens.shape, device=device, generator=generator) < token_noise |
| ) & diff_sel[:, None] |
| noisy_tokens = torch.where(mask, model.config.mask_token_id, tokens) |
|
|
| zeros = torch.zeros(batch_size, dtype=torch.long, device=device) |
| full = torch.full((batch_size,), seq_len, dtype=torch.long, device=device) |
| block_blocked = block_mask_from_boundaries(boundary, zeros, full) |
| causal_blocked = prefix_causal_blocked(zeros, seq_len, causal_prefix=True) |
| blocked = torch.where(diff_sel[:, None, None], block_blocked, causal_blocked) |
|
|
| predict_positions = (positions[None, :] < seq_len - 1) & ar_sel[:, None] |
| output_positions = mask | predict_positions |
| logits = model(noisy_tokens, output_positions=output_positions, attn_mask=blocked) |
|
|
| think_rows = mask[output_positions] |
| |
| |
| zero = logits.sum().clamp(-1.0, 1.0) * 0.0 |
|
|
| think_loss = zero |
| think_accuracy = 0.0 |
| if bool(mask.any()): |
| think_logits = logits[think_rows] |
| think_targets = tokens[mask] |
| per_token = F.cross_entropy(think_logits.float(), think_targets, reduction='none') |
| weights = token_noise[mask] |
| normalizer = (diff_sel.sum() * seq_len).clamp_min(1) |
| think_loss = (per_token / weights).sum() / normalizer |
| think_accuracy = float( |
| (think_logits.argmax(dim=-1) == think_targets).float().mean() |
| ) |
|
|
| answer_loss = zero |
| answer_accuracy = 0.0 |
| if bool(predict_positions.any()): |
| answer_logits = logits[~think_rows] |
| target_positions = torch.zeros_like(predict_positions) |
| target_positions[:, 1:] = predict_positions[:, :-1] |
| answer_targets = tokens[target_positions] |
| answer_loss = F.cross_entropy(answer_logits.float(), answer_targets) |
| answer_accuracy = float( |
| (answer_logits.argmax(dim=-1) == answer_targets).float().mean() |
| ) |
|
|
| return HybridBatchLoss( |
| loss=think_loss + answer_loss, |
| think_loss=float(think_loss), |
| answer_loss=float(answer_loss), |
| think_accuracy=think_accuracy, |
| answer_accuracy=answer_accuracy, |
| think_samples=int(diff_sel.sum()), |
| answer_samples=int(ar_sel.sum()), |
| ) |
|
|
|
|
| @dataclass(frozen=True) |
| class ARBatchLoss: |
| loss: Tensor |
| accuracy: float |
| token_count: int |
|
|
|
|
| def ar_objective( |
| model: DiffusionTransformer, tokens: Tensor, regions: Tensor |
| ) -> ARBatchLoss: |
| """Prefix-LM next-token objective over the think and answer regions.""" |
|
|
| device = tokens.device |
| _, seq_len = tokens.shape |
| problem_len, _, _, answer_end = regions.unbind(dim=1) |
| positions = torch.arange(seq_len, device=device) |
|
|
| blocked = prefix_causal_blocked(problem_len + 1, seq_len) |
| predict_positions = (positions[None, :] >= problem_len[:, None]) & ( |
| positions[None, :] < answer_end[:, None] - 1 |
| ) |
| logits = model(tokens, output_positions=predict_positions, attn_mask=blocked) |
|
|
| target_positions = torch.zeros_like(predict_positions) |
| target_positions[:, 1:] = predict_positions[:, :-1] |
| targets = tokens[target_positions] |
| loss = F.cross_entropy(logits.float(), targets) |
| accuracy = float((logits.argmax(dim=-1) == targets).float().mean()) |
| return ARBatchLoss(loss=loss, accuracy=accuracy, token_count=int(targets.numel())) |
|
|
|
|
| def diffusion_objective( |
| model: DiffusionTransformer, |
| tokens: Tensor, |
| regions: Tensor, |
| *, |
| mask_eps: float, |
| mask_probability: Tensor | None = None, |
| generator: torch.Generator | None = None, |
| ) -> tuple[Tensor, CorruptionBatch, Tensor]: |
| """Whole-response denoising: corrupt everything after the problem, pads included.""" |
|
|
| device = tokens.device |
| _, seq_len = tokens.shape |
| problem_len = regions[:, 0] |
| positions = torch.arange(seq_len, device=device) |
| response_mask = positions[None, :] >= (problem_len[:, None] + 1) |
| corruption = corrupt_tokens( |
| tokens, |
| model.config.mask_token_id, |
| valid_mask=response_mask, |
| mask_probability=mask_probability, |
| eps=mask_eps, |
| generator=generator, |
| ) |
| logits = model(corruption.noisy_tokens, output_positions=corruption.mask) |
| return logits, corruption, response_mask |
|
|
|
|
| @dataclass |
| class GenerationResult: |
| tokens: list[int] |
| think_tokens: list[int] |
| answer_tokens: list[int] |
| think_seconds: float = 0.0 |
| answer_seconds: float = 0.0 |
| forward_passes: int = 0 |
| slots_used: int = 0 |
| block_sizes: tuple[int, ...] = () |
| terminated: bool = False |
|
|
| @property |
| def total_seconds(self) -> float: |
| return self.think_seconds + self.answer_seconds |
|
|
|
|
| def _apply_repetition_penalty( |
| logits: Tensor, token_ids: list[int], penalty: float |
| ) -> Tensor: |
| """Divide logits of already-emitted tokens by ``penalty`` (CTRL convention).""" |
|
|
| if penalty == 1.0 or not token_ids: |
| return logits |
| index = torch.tensor(sorted(set(token_ids)), device=logits.device) |
| selected = logits.index_select(-1, index) |
| adjusted = torch.where(selected > 0, selected / penalty, selected * penalty) |
| return logits.index_copy(-1, index, adjusted) |
|
|
|
|
| def _apply_top_p(logits: Tensor, top_p: float) -> Tensor: |
| """Restrict sampling to the smallest set of tokens whose mass reaches ``top_p``.""" |
|
|
| if top_p >= 1.0: |
| return logits |
| ordered, indices = torch.sort(logits, descending=True, dim=-1) |
| cumulative = ordered.softmax(dim=-1).cumsum(dim=-1) |
| remove = cumulative - ordered.softmax(dim=-1) >= top_p |
| ordered = ordered.masked_fill(remove, torch.finfo(logits.dtype).min) |
| return ordered.gather(-1, indices.argsort(dim=-1)) |
|
|
|
|
| def _kv_cache_enabled(part: str) -> bool: |
| """Key/value caching per part, selected by ``MDLM_KV_CACHE``: ar, block, all or off. |
| |
| Defaults to ``ar``, which measured 101s to 12.6s on the same prompt's answer: one query |
| token against an all-visible mask has no downside. The denoising prefix does — caching it |
| hands the backbone a dense mask, dropping flex attention onto its score_mod path and losing |
| the block skipping the uncached call gets, 10.6s per block against 5.3s on an L40S |
| (2026-07-27). It stays off until that path builds a rectangular BlockMask instead. |
| """ |
|
|
| setting = os.environ.get('MDLM_KV_CACHE', 'ar') |
| return setting in ('all', '1') or setting == part |
|
|
|
|
| def _cached_block_logits(model, blocked: Tensor, prefix_len: int): |
| """Score a denoising block against a cached prefix, or ``None`` without cache support. |
| |
| The prefix is encoded once per block; every denoising step then feeds only the block's |
| own positions. Its keys and values are dropped between steps because the block's tokens |
| keep changing as they are revealed, while the prefix behind them does not. |
| """ |
|
|
| if not hasattr(model, 'forward_cached') or not _kv_cache_enabled('block'): |
| return None |
|
|
| cache = model.new_cache() |
|
|
| def logits_fn(tokens: Tensor, masked: Tensor) -> Tensor: |
| if cache.get_seq_length() == 0: |
| with torch.inference_mode(): |
| model.forward_cached( |
| tokens[:, :prefix_len], |
| attn_mask=blocked[:, :prefix_len, :prefix_len], |
| past_key_values=cache, |
| ) |
| cache.crop(prefix_len) |
| with torch.inference_mode(): |
| logits, _ = model.forward_cached( |
| tokens[:, prefix_len:], |
| attn_mask=blocked[:, prefix_len:, :], |
| past_key_values=cache, |
| output_positions=masked[:, prefix_len:], |
| ) |
| return logits |
|
|
| return logits_fn |
|
|
|
|
| def _ar_decode_cached( |
| model: DiffusionTransformer, |
| sequence: list[int], |
| prefix_len: int, |
| *, |
| causal_prefix: bool, |
| eos_id: int, |
| max_new_tokens: int, |
| temperature: float, |
| repetition_penalty: float, |
| top_p: float, |
| device: torch.device, |
| generator: torch.Generator | None, |
| ) -> tuple[list[int], int]: |
| """Same decoding as :func:`_ar_decode` with the prefix kept in a key/value cache. |
| |
| Masks come from :func:`prefix_causal_blocked` exactly as in the uncached path, sliced to |
| the rows of the queries being fed. Priming with an all-visible mask instead would look |
| right — the final row is identical, so a single generated token matches — while silently |
| computing every earlier position bidirectionally and poisoning the cached keys. |
| """ |
|
|
| generated: list[int] = [] |
| cache = model.new_cache() |
| step_in = torch.tensor([sequence], dtype=torch.long, device=device) |
| forwards = 0 |
| prefix = torch.tensor([prefix_len], device=device) |
| for _ in range(max_new_tokens): |
| cached = cache.get_seq_length() |
| length = cached + step_in.shape[1] |
| visible = prefix_causal_blocked(prefix, length, causal_prefix=causal_prefix)[:, cached:, :] |
| output_positions = torch.zeros_like(step_in, dtype=torch.bool) |
| output_positions[0, -1] = True |
| with torch.inference_mode(): |
| logits, cache = model.forward_cached( |
| step_in, attn_mask=visible, past_key_values=cache, |
| output_positions=output_positions, |
| ) |
| logits = _apply_repetition_penalty(logits, generated, repetition_penalty) |
| logits = _apply_top_p(logits, top_p) |
| token, _ = _sample_categorical(logits, temperature, generator) |
| forwards += 1 |
| token_id = int(token.item()) |
| generated.append(token_id) |
| if token_id == eos_id: |
| break |
| step_in = torch.tensor([[token_id]], dtype=torch.long, device=device) |
| return generated, forwards |
|
|
|
|
| def _ar_decode( |
| model: DiffusionTransformer, |
| sequence: list[int], |
| prefix_len: int, |
| *, |
| eos_id: int, |
| max_new_tokens: int, |
| temperature: float, |
| repetition_penalty: float, |
| top_p: float, |
| device: torch.device, |
| generator: torch.Generator | None, |
| causal_prefix: bool = False, |
| ) -> tuple[list[int], int]: |
| """Greedy/temperature decoding under the bidirectional-prefix causal mask. |
| |
| ``repetition_penalty`` (>1 discourages repeats) and ``top_p`` nucleus truncation |
| curb the degenerate loops small models fall into under plain temperature sampling. |
| """ |
|
|
| generated: list[int] = [] |
| forwards = 0 |
| max_new_tokens = min(max_new_tokens, model.config.max_seq_len - len(sequence)) |
| if hasattr(model, 'forward_cached') and _kv_cache_enabled('ar'): |
| return _ar_decode_cached( |
| model, sequence, prefix_len, causal_prefix=causal_prefix, eos_id=eos_id, |
| max_new_tokens=max_new_tokens, temperature=temperature, |
| repetition_penalty=repetition_penalty, top_p=top_p, device=device, |
| generator=generator, |
| ) |
| for _ in range(max_new_tokens): |
| current = torch.tensor([sequence + generated], dtype=torch.long, device=device) |
| seq_len = current.shape[1] |
| prefix = torch.tensor([prefix_len], device=device) |
| blocked = prefix_causal_blocked(prefix, seq_len, causal_prefix=causal_prefix) |
| output_positions = torch.zeros_like(current, dtype=torch.bool) |
| output_positions[0, -1] = True |
| with torch.inference_mode(): |
| logits = model(current, output_positions=output_positions, attn_mask=blocked) |
| logits = _apply_repetition_penalty(logits, generated, repetition_penalty) |
| logits = _apply_top_p(logits, top_p) |
| token, _ = _sample_categorical(logits, temperature, generator) |
| forwards += 1 |
| token_id = int(token.item()) |
| generated.append(token_id) |
| if token_id == eos_id: |
| break |
| return generated, forwards |
|
|
|
|
| @torch.no_grad() |
| def generate_hybrid( |
| model: DiffusionTransformer, |
| prompt_ids: list[int], |
| *, |
| think_id: int, |
| end_think_id: int, |
| thought_pad_id: int, |
| eos_id: int, |
| block: int, |
| max_slots: int, |
| steps_per_block: int, |
| max_answer_tokens: int = 64, |
| temperature: float = 0.7, |
| repetition_penalty: float = 1.0, |
| top_p: float = 1.0, |
| strategy: str = 'confidence', |
| device: torch.device | str = 'cpu', |
| generator: torch.Generator | None = None, |
| ) -> GenerationResult: |
| """Denoise thought slots sequentially, then decode the answer autoregressively.""" |
|
|
| device = torch.device(device) |
| mask_id = model.config.mask_token_id |
| sequence = [*prompt_ids, think_id] |
| forwards = 0 |
| slots_used = 0 |
| |
| slot_budget = model.config.max_seq_len - block - 8 |
|
|
| think_started = time.perf_counter() |
| problem_tensor = torch.tensor([len(prompt_ids)], device=device) |
| for slot_index in range(max_slots): |
| if len(sequence) > slot_budget: |
| break |
| window = torch.tensor( |
| [sequence + [mask_id] * block], dtype=torch.long, device=device |
| ) |
| blocked = slot_causal_blocked( |
| problem_tensor, |
| torch.tensor([slot_index + 1], device=device), |
| block, |
| window.shape[1], |
| ) |
| filled = iterative_unmask( |
| model, |
| window, |
| mask_id, |
| steps=steps_per_block, |
| temperature=temperature, |
| strategy=strategy, |
| attn_mask=blocked, |
| generator=generator, |
| ) |
| slot = [int(token) for token in filled[0, len(sequence):]] |
| forwards += steps_per_block |
| slots_used += 1 |
| sequence.extend(slot) |
| if end_think_id in slot: |
| break |
| if end_think_id not in sequence[len(prompt_ids):]: |
| |
| terminal = [end_think_id] + [thought_pad_id] * (block - 1) |
| sequence.extend(terminal[: max(1, model.config.max_seq_len - 8 - len(sequence))]) |
| think_seconds = time.perf_counter() - think_started |
| think_tokens = sequence[len(prompt_ids):] |
|
|
| answer_started = time.perf_counter() |
| answer, answer_forwards = _ar_decode( |
| model, |
| sequence, |
| prefix_len=len(sequence), |
| eos_id=eos_id, |
| max_new_tokens=max_answer_tokens, |
| temperature=temperature, |
| repetition_penalty=repetition_penalty, |
| top_p=top_p, |
| device=device, |
| generator=generator, |
| ) |
| answer_seconds = time.perf_counter() - answer_started |
| return GenerationResult( |
| tokens=sequence + answer, |
| think_tokens=think_tokens, |
| answer_tokens=answer, |
| think_seconds=think_seconds, |
| answer_seconds=answer_seconds, |
| forward_passes=forwards + answer_forwards, |
| slots_used=slots_used, |
| ) |
|
|
|
|
| def _ar_predict_control( |
| model: DiffusionTransformer, |
| sequence: list[int], |
| allowed_ids: list[int], |
| *, |
| prefix_len: int, |
| temperature: float, |
| device: torch.device, |
| generator: torch.Generator | None, |
| causal_prefix: bool = False, |
| ) -> int: |
| """Predict the next control token, restricted to the allowed size/stop ids.""" |
|
|
| current = torch.tensor([sequence], dtype=torch.long, device=device) |
| seq_len = current.shape[1] |
| blocked = prefix_causal_blocked( |
| torch.tensor([prefix_len], device=device), seq_len, causal_prefix=causal_prefix |
| ) |
| output_positions = torch.zeros_like(current, dtype=torch.bool) |
| output_positions[0, -1] = True |
| with torch.inference_mode(): |
| logits = model(current, output_positions=output_positions, attn_mask=blocked) |
| restricted = torch.full_like(logits, torch.finfo(logits.dtype).min) |
| index = torch.tensor(allowed_ids, device=logits.device) |
| restricted.index_copy_(-1, index, logits.index_select(-1, index)) |
| token, _ = _sample_categorical(restricted, temperature, generator) |
| return int(token.item()) |
|
|
|
|
| @torch.no_grad() |
| def generate_hybrid_adaptive( |
| model: DiffusionTransformer, |
| prompt_ids: list[int], |
| *, |
| think_id: int, |
| end_think_id: int, |
| thought_pad_id: int, |
| eos_id: int, |
| size_ids: dict[int, int], |
| steps_per_block: int, |
| max_blocks: int, |
| max_answer_tokens: int = 96, |
| temperature: float = 0.7, |
| control_temperature: float = 0.0, |
| repetition_penalty: float = 1.0, |
| top_p: float = 1.0, |
| strategy: str = 'confidence', |
| causal_prefix: bool = False, |
| device: torch.device | str = 'cpu', |
| generator: torch.Generator | None = None, |
| ) -> GenerationResult: |
| """Interleave AR block-size decisions with in-block diffusion, then decode. |
| |
| At each boundary the model predicts a ``<szN>`` control token or ``</think>``. |
| A size token allocates that many masked positions denoised in parallel under the |
| variable-boundary block-causal mask; ``</think>`` ends thinking. The answer is |
| then decoded autoregressively with the same repetition and nucleus controls. |
| """ |
|
|
| device = torch.device(device) |
| mask_id = model.config.mask_token_id |
| size_by_id = {token_id: size for size, token_id in size_ids.items()} |
| size_ids_tensor = torch.tensor(sorted(size_ids.values()), device=device) |
| control_ids = [*size_by_id.keys(), end_think_id] |
| prefix_len = len(prompt_ids) + 1 |
| problem_tensor = torch.tensor([len(prompt_ids)], device=device) |
|
|
| sequence = [*prompt_ids, think_id] |
| forwards = 0 |
| blocks_used = 0 |
| chosen: list[int] = [] |
| terminated = False |
|
|
| think_started = time.perf_counter() |
| for _ in range(max_blocks): |
| control = _ar_predict_control( |
| model, |
| sequence, |
| control_ids, |
| prefix_len=prefix_len, |
| temperature=control_temperature, |
| device=device, |
| generator=generator, |
| causal_prefix=causal_prefix, |
| ) |
| forwards += 1 |
| if control == end_think_id: |
| terminated = True |
| break |
| size = size_by_id[control] |
| if len(sequence) + 1 + size > model.config.max_seq_len - 8: |
| break |
| sequence.append(control) |
| window_prefix = len(sequence) |
| window = torch.tensor( |
| [sequence + [mask_id] * size], dtype=torch.long, device=device |
| ) |
| blocked = adaptive_block_mask( |
| window, |
| problem_tensor, |
| torch.tensor([window.shape[1]], device=device), |
| size_ids_tensor, |
| end_think_id, |
| causal_prefix=causal_prefix, |
| ) |
| filled = iterative_unmask( |
| model, |
| window, |
| mask_id, |
| steps=steps_per_block, |
| temperature=temperature, |
| strategy=strategy, |
| attn_mask=blocked, |
| generator=generator, |
| logits_fn=_cached_block_logits(model, blocked, window_prefix), |
| ) |
| sequence.extend(int(token) for token in filled[0, window_prefix:]) |
| forwards += steps_per_block |
| blocks_used += 1 |
| chosen.append(size) |
|
|
| sequence.append(end_think_id) |
| think_seconds = time.perf_counter() - think_started |
| think_tokens = sequence[len(prompt_ids):] |
|
|
| answer_started = time.perf_counter() |
| answer, answer_forwards = _ar_decode( |
| model, |
| sequence, |
| prefix_len=prefix_len, |
| eos_id=eos_id, |
| max_new_tokens=max_answer_tokens, |
| temperature=temperature, |
| repetition_penalty=repetition_penalty, |
| top_p=top_p, |
| device=device, |
| generator=generator, |
| causal_prefix=causal_prefix, |
| ) |
| answer_seconds = time.perf_counter() - answer_started |
| return GenerationResult( |
| tokens=sequence + answer, |
| think_tokens=think_tokens, |
| answer_tokens=answer, |
| think_seconds=think_seconds, |
| answer_seconds=answer_seconds, |
| forward_passes=forwards + answer_forwards, |
| slots_used=blocks_used, |
| block_sizes=tuple(chosen), |
| terminated=terminated, |
| ) |
|
|
|
|
| @torch.no_grad() |
| def generate_ar( |
| model: DiffusionTransformer, |
| prompt_ids: list[int], |
| *, |
| think_id: int, |
| end_think_id: int, |
| eos_id: int, |
| max_new_tokens: int = 384, |
| temperature: float = 0.7, |
| device: torch.device | str = 'cpu', |
| generator: torch.Generator | None = None, |
| ) -> GenerationResult: |
| """Classic sequential CoT baseline under the prefix-LM mask.""" |
|
|
| device = torch.device(device) |
| sequence = [*prompt_ids, think_id] |
| started = time.perf_counter() |
| generated, forwards = _ar_decode( |
| model, |
| sequence, |
| prefix_len=len(sequence), |
| eos_id=eos_id, |
| max_new_tokens=max_new_tokens, |
| temperature=temperature, |
| device=device, |
| generator=generator, |
| ) |
| elapsed = time.perf_counter() - started |
| if end_think_id in generated: |
| split = generated.index(end_think_id) + 1 |
| else: |
| split = len(generated) |
| return GenerationResult( |
| tokens=sequence + generated, |
| think_tokens=generated[:split], |
| answer_tokens=generated[split:], |
| think_seconds=elapsed, |
| answer_seconds=0.0, |
| forward_passes=forwards, |
| ) |
|
|
|
|
| @torch.no_grad() |
| def generate_diffusion( |
| model: DiffusionTransformer, |
| prompt_ids: list[int], |
| *, |
| think_id: int, |
| end_think_id: int, |
| eos_id: int, |
| response_budget: int, |
| steps: int, |
| temperature: float = 0.7, |
| blocked_token_ids: tuple[int, ...] = (), |
| device: torch.device | str = 'cpu', |
| generator: torch.Generator | None = None, |
| ) -> GenerationResult: |
| """Pure-diffusion baseline: denoise the entire response region at once. |
| |
| Blocking the pad token here counters confidence-ordered pad collapse: pads are |
| the easiest predictions, so left unblocked they win every early reveal and |
| squeeze out the actual response text. |
| """ |
|
|
| device = torch.device(device) |
| mask_id = model.config.mask_token_id |
| budget = min(response_budget, model.config.max_seq_len - len(prompt_ids) - 1) |
| sequence = torch.tensor( |
| [[*prompt_ids, think_id] + [mask_id] * budget], dtype=torch.long, device=device |
| ) |
| started = time.perf_counter() |
| filled = iterative_unmask( |
| model, |
| sequence, |
| mask_id, |
| steps=steps, |
| temperature=temperature, |
| strategy='confidence', |
| blocked_token_ids=blocked_token_ids, |
| generator=generator, |
| ) |
| elapsed = time.perf_counter() - started |
| response = [int(token) for token in filled[0, len(prompt_ids) + 1:]] |
| if eos_id in response: |
| response = response[: response.index(eos_id) + 1] |
| if end_think_id in response: |
| split = response.index(end_think_id) + 1 |
| else: |
| split = len(response) |
| return GenerationResult( |
| tokens=[*prompt_ids, think_id] + response, |
| think_tokens=response[:split], |
| answer_tokens=response[split:], |
| think_seconds=elapsed, |
| answer_seconds=0.0, |
| forward_passes=steps, |
| ) |
|
|