Download Model/model.py from Montlok/DoL-1.5-Flash: direct link, hf CLI and curl.
- Browser
- Download file 55.8 kB
-
https://huggingface.co/Montlok/DoL-1.5-Flash/resolve/main/Model/model.py
- Command line
-
hf download hf://Montlok/DoL-1.5-Flash/Model/model.py
-
curl -L -o model.py https://huggingface.co/Montlok/DoL-1.5-Flash/resolve/main/Model/model.py
55.8 kB
# -*- coding: utf-8 -*-
from __future__ import annotations
from collections.abc import Mapping
from typing import Callable, Sequence
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.checkpoint import checkpoint
from Model.blocks import StandardBlock
from Model.config import RDTConfig
from Model.layers.rmsnorm import RMSNorm, GroupedRMSNorm
from Model.layers.vision_cross_attention import RaggedVisionCrossAttention
from Model.ocr.position_contract import (
BOUNDARY_V1,
LEGACY_SEQUENTIAL_V0,
validate_ocr_position_contract,
)
from Model.recurrent import RecurrentCore
from Model.two_stage import TwoStageCore
from Model.vision import VisionInjector
class RDTForCausalLM(nn.Module):
def __init__(self, cfg: RDTConfig, patch_pixels: int = 14 * 14 * 3):
super().__init__()
self.cfg = cfg
self.patch_pixels = patch_pixels
self.embed = nn.Embedding(
cfg.vocab_size,
cfg.d_model,
padding_idx=cfg.pad_id,
)
self.vision = VisionInjector(cfg, patch_pixels)
self.prelude = nn.ModuleList(
StandardBlock(cfg, layer_idx=i) for i in range(cfg.n_prelude)
)
if cfg.core_type == "two_stage":
self.recurrent = TwoStageCore(cfg)
else:
self.recurrent = RecurrentCore(cfg)
self.coda = nn.ModuleList(
StandardBlock(cfg, layer_idx=cfg.n_prelude + i) for i in range(cfg.n_coda)
)
self.final_norm = RMSNorm(cfg.d_model, eps=cfg.rmsnorm_eps)
self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
self.bidirectional = cfg.bidirectional
if self.bidirectional:
self.reverse_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
else:
self.reverse_head = None
# Runtime toggle: OCR alignment and GRPO disable the
# bidirectional reverse-LM auxiliary loss so it does not pollute the
# supervised/preference gradient. Pretraining keeps it enabled.
self.reverse_loss_enabled = True
# OMVT-v2 detail memory is opt-in. Keeping this as ``None`` means a
# v1 model creates no bridge parameters and executes no bridge path.
self.vision_cross_attention: RaggedVisionCrossAttention | None = None
self.apply(self._init_weights)
self._scale_residual_projections()
for module in self.modules():
if isinstance(module, (RMSNorm, GroupedRMSNorm)):
module.token_chunk_size = cfg.norm_token_chunk_size
if cfg.tie_word_embeddings:
self.lm_head.weight = self.embed.weight
if self.reverse_head is not None:
self.reverse_head.weight = self.embed.weight
def install_vision_cross_attention(
self,
*,
memory_dim: int,
n_heads: int | None = None,
dropout: float = 0.0,
) -> RaggedVisionCrossAttention:
"""Explicitly install the teacher-forced OMVT-v2 detail bridge.
Generation and incremental cache behavior are deliberately not
changed here. A second install is rejected so a trained bridge cannot
be silently reinitialized.
"""
if self.vision_cross_attention is not None:
raise RuntimeError("vision cross-attention is already installed")
bridge = RaggedVisionCrossAttention(
d_model=self.cfg.d_model,
memory_dim=memory_dim,
n_heads=self.cfg.n_heads if n_heads is None else n_heads,
dropout=dropout,
)
bridge.apply(self._init_weights)
bridge.zero_init_output_projection()
bridge.to(device=self.embed.weight.device, dtype=self.embed.weight.dtype)
bridge.train(self.training)
self.vision_cross_attention = bridge
return bridge
def _forward_decode(
self,
input_ids: torch.Tensor,
word_pos: torch.Tensor,
morph_depth: torch.Tensor,
cache,
pixel_values: torch.Tensor | Mapping[str, torch.Tensor] | None = None,
visual_features: torch.Tensor | None = None,
detail_memory: torch.Tensor | None = None,
detail_cu_seqlens: torch.Tensor | None = None,
steps: int | None = None,
) -> torch.Tensor:
"""Incremental forward over the new tokens ``input_ids`` (``[B, m]``).
``word_pos`` / ``morph_depth`` are the *absolute* per-position values of
the new tokens (computed by the caller from the full running sequence).
Reuses ``cache`` so the result is bit-exact with a full
:meth:`forward` over the whole prefix. Returns logits ``[B, m, vocab]``;
callers typically read the last position. Generation is pad-free, so
``attn_mask`` is omitted throughout.
"""
if self.cfg.core_type != "two_stage":
raise NotImplementedError(
"incremental KV/state cache is implemented for core_type="
"'two_stage' only"
)
bsz, seq_len = input_ids.shape
pos_offset = cache.seq_len
h = self._embed_with_activation_dtype(input_ids)
if pixel_values is not None or visual_features is not None:
h = self.vision(
h,
input_ids,
pixel_values,
visual_features=visual_features,
)
for i, block in enumerate(self.prelude):
h = block(
h,
word_pos=word_pos,
morph_depth=morph_depth,
attn_mask=None,
causal=True,
cache=cache.mla_cache(f"prelude.{i}"),
pos_offset=pos_offset,
)
if (detail_memory is None) != (detail_cu_seqlens is None):
raise ValueError(
"detail_memory and detail_cu_seqlens must be supplied together"
)
if detail_memory is not None:
if self.vision_cross_attention is None:
raise RuntimeError(
"detail memory requires install_vision_cross_attention()"
)
h = self.vision_cross_attention(
h,
detail_memory,
detail_cu_seqlens,
)
h, _rec_info = self.recurrent(
h,
word_pos=word_pos,
morph_depth=morph_depth,
attn_mask=None,
causal=True,
cache=cache,
pos_offset=pos_offset,
steps=steps,
)
for i, block in enumerate(self.coda):
h = block(
h,
word_pos=word_pos,
morph_depth=morph_depth,
attn_mask=None,
causal=True,
cache=cache.mla_cache(f"coda.{i}"),
pos_offset=pos_offset,
)
h = self.final_norm(h)
logits = self.lm_head(h)
cache.seq_len = pos_offset + seq_len
return logits
def forward(
self,
input_ids: torch.Tensor,
attention_mask: torch.Tensor | None = None,
labels: torch.Tensor | None = None,
pixel_values: torch.Tensor | Mapping[str, torch.Tensor] | None = None,
visual_features: torch.Tensor | None = None,
word_pos: torch.Tensor | None = None,
morph_depth: torch.Tensor | None = None,
morphology_track_table: torch.Tensor | None = None,
steps: int | None = None,
bptt_window: int | None = None,
return_logits: bool = True,
logits_to_keep: int = 0,
loss_chunk_size: int | None = None,
pixel_repeats: int = 1,
position_contract: str | None = None,
detail_memory: torch.Tensor | None = None,
detail_cu_seqlens: torch.Tensor | None = None,
all_tokens_valid: bool = False,
) -> dict[str, torch.Tensor | dict | None]:
self._check_inputs(input_ids, attention_mask, labels)
if type(logits_to_keep) is not int or logits_to_keep < 0:
raise ValueError("logits_to_keep must be a non-negative integer")
if logits_to_keep and (labels is not None or not return_logits):
raise ValueError("logits_to_keep is only available for inference without labels")
if loss_chunk_size is None:
loss_chunk_size = self.cfg.loss_chunk_size
elif loss_chunk_size <= 0:
raise ValueError("loss_chunk_size must be positive")
bsz, seq_len = input_ids.shape
if attention_mask is None:
attention_mask = (input_ids != self.cfg.pad_id).long()
if all_tokens_valid and (pixel_values is not None or visual_features is not None):
raise ValueError("the unpadded long-context path is text-only")
layer_mask = None if all_tokens_valid else attention_mask
if position_contract is not None:
position_contract = validate_ocr_position_contract(position_contract)
if morphology_track_table is not None:
raise ValueError(
"morphology_track_table cannot be combined with an explicit "
"OCR position contract"
)
if (word_pos is None) != (morph_depth is None):
raise ValueError("word_pos and morph_depth must be supplied together")
if word_pos is None:
if position_contract is not None:
word_pos, morph_depth = self._morph_info_for_position_contract(
input_ids,
attention_mask,
position_contract,
)
elif morphology_track_table is not None:
word_pos, morph_depth = self._morph_info_from_track_table(
input_ids,
morphology_track_table,
)
else:
word_pos, morph_depth = self._default_morph_info(
input_ids=input_ids,
attention_mask=attention_mask,
)
if (detail_memory is None) != (detail_cu_seqlens is None):
raise ValueError(
"detail_memory and detail_cu_seqlens must be supplied together"
)
if detail_memory is not None and self.vision_cross_attention is None:
raise RuntimeError(
"detail memory requires install_vision_cross_attention()"
)
h = self._embed_with_activation_dtype(input_ids)
if pixel_values is not None or visual_features is not None:
h = self.vision(
h,
input_ids,
pixel_values,
visual_features=visual_features,
pixel_repeats=pixel_repeats,
)
for block in self.prelude:
h = self._maybe_ckpt(
block,
h,
word_pos=word_pos,
morph_depth=morph_depth,
attn_mask=layer_mask,
causal=True,
)
if detail_memory is not None:
assert self.vision_cross_attention is not None
h = self.vision_cross_attention(
h,
detail_memory,
detail_cu_seqlens,
query_mask=attention_mask,
)
e0 = h
h, rec_info = self.recurrent(
e0,
word_pos=word_pos,
morph_depth=morph_depth,
attn_mask=layer_mask,
causal=True,
steps=steps,
bptt_window=bptt_window,
)
for block in self.coda:
h = self._maybe_ckpt(
block,
h,
word_pos=word_pos,
morph_depth=morph_depth,
attn_mask=layer_mask,
causal=True,
)
h = self.final_norm(h)
logits = None
loss = None
loss_parts: dict[str, float] = {}
if labels is not None:
if not return_logits and loss_chunk_size is not None:
loss, loss_parts = self._losses_chunked(
h,
labels,
loss_chunk_size,
)
else:
logits = self.lm_head(h)
loss, loss_parts = self._losses(h, logits, labels)
elif return_logits:
logits = self.lm_head(h[:, -logits_to_keep:] if logits_to_keep else h)
return {
"loss": loss,
"logits": logits if return_logits else None,
"loss_parts": loss_parts,
"rec_info": rec_info,
}
def _embed_with_activation_dtype(self, input_ids):
hidden = self.embed(input_ids)
if self.cfg.low_precision_residual and torch.is_autocast_enabled(hidden.device.type):
hidden = hidden.to(torch.get_autocast_dtype(hidden.device.type))
return hidden
def _losses(
self,
h: torch.Tensor,
logits: torch.Tensor,
labels: torch.Tensor,
) -> tuple[torch.Tensor, dict[str, float]]:
forward = self._causal_loss(logits, labels)
loss = forward
parts = {"forward": float(forward.detach())}
if self.reverse_head is not None and self.reverse_loss_enabled:
rev_logits = self.reverse_head(h)
reverse = self._reverse_loss(rev_logits, labels)
loss = loss + self.cfg.reverse_loss_weight * reverse
parts["reverse"] = float(reverse.detach())
return loss, parts
def _losses_chunked(
self,
h: torch.Tensor,
labels: torch.Tensor,
chunk_size: int,
) -> tuple[torch.Tensor, dict[str, float]]:
forward = self._chunked_causal_loss(h, labels, self.lm_head, chunk_size)
loss = forward
parts = {"forward": float(forward.detach())}
if self.reverse_head is not None and self.reverse_loss_enabled:
reverse = self._chunked_reverse_loss(
h,
labels,
self.reverse_head,
chunk_size,
)
loss = loss + self.cfg.reverse_loss_weight * reverse
parts["reverse"] = float(reverse.detach())
return loss, parts
def _causal_loss(self, logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
return self._token_loss(
logits[:, :-1].reshape(-1, logits.size(-1)),
labels[:, 1:].reshape(-1),
)
def _reverse_loss(
self,
rev_logits: torch.Tensor,
labels: torch.Tensor,
) -> torch.Tensor:
return self._token_loss(
rev_logits[:, 1:].reshape(-1, rev_logits.size(-1)),
labels[:, :-1].reshape(-1),
)
def _token_loss(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
valid = targets != self.cfg.ignore_index
if logits.numel() == 0 or not bool(valid.any()):
return logits.sum() * 0.0
return F.cross_entropy(
logits,
targets,
ignore_index=self.cfg.ignore_index,
)
def _chunked_causal_loss(
self,
h: torch.Tensor,
labels: torch.Tensor,
head: nn.Linear,
chunk_size: int,
) -> torch.Tensor:
return self._chunked_token_loss(
h[:, :-1].reshape(-1, h.size(-1)),
labels[:, 1:].reshape(-1),
head,
chunk_size,
)
def _chunked_reverse_loss(
self,
h: torch.Tensor,
labels: torch.Tensor,
head: nn.Linear,
chunk_size: int,
) -> torch.Tensor:
return self._chunked_token_loss(
h[:, 1:].reshape(-1, h.size(-1)),
labels[:, :-1].reshape(-1),
head,
chunk_size,
)
def _chunked_token_loss(
self,
hidden: torch.Tensor,
targets: torch.Tensor,
head: nn.Linear,
chunk_size: int,
) -> torch.Tensor:
valid = targets != self.cfg.ignore_index
if hidden.numel() == 0 or not bool(valid.any()):
return hidden.sum() * 0.0
hidden = hidden[valid]
targets = targets[valid]
loss_sum = hidden.new_zeros(())
def loss_piece(states, expected):
logits = F.linear(states, head.weight, head.bias)
return F.cross_entropy(logits, expected, reduction="sum")
for start in range(0, hidden.size(0), chunk_size):
end = min(start + chunk_size, hidden.size(0))
if self.cfg.grad_ckpt_loss and torch.is_grad_enabled():
part = checkpoint(loss_piece, hidden[start:end], targets[start:end], use_reentrant=False)
else:
part = loss_piece(hidden[start:end], targets[start:end])
loss_sum = loss_sum + part
return loss_sum / targets.numel()
def _maybe_ckpt(
self,
block: nn.Module,
h: torch.Tensor,
word_pos: torch.Tensor | None,
morph_depth: torch.Tensor | None,
attn_mask: torch.Tensor | None,
causal: bool,
) -> torch.Tensor:
if (
getattr(self.cfg, "grad_ckpt_prelude_coda", False)
and self.training
and h.requires_grad
):
def _fn(x):
return block(
x,
word_pos=word_pos,
morph_depth=morph_depth,
attn_mask=attn_mask,
causal=causal,
)
return checkpoint(_fn, h, use_reentrant=False)
return block(
h,
word_pos=word_pos,
morph_depth=morph_depth,
attn_mask=attn_mask,
causal=causal,
)
def _default_morph_info(
self,
input_ids: torch.Tensor,
attention_mask: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Derive ``(word_pos, morph_depth)`` from boundary token IDs.
This is a vectorized counterpart of
:func:`Tokenizer.pretraining.derive_morph_info_from_boundary_ids`.
Callers that already supply ``word_pos`` / ``morph_depth`` skip this.
Semantics (see tokenizer-side reference for details):
* ``word_boundary_id`` opens a new word at depth 0.
* ``morpheme_boundary_id`` keeps the current word and bumps depth.
* Other special tokens (ids in ``[0, 256)``) reset depth to 0 and
stay on the current ``word_pos``; the next non-special content
token will inherit ``word_pos`` until a new ``word_boundary``.
* Padding positions (``attention_mask == 0``) keep their derived
``word_pos`` so downstream RoPE never sees ``-1``; the mask
itself is what zeros out padded contributions.
"""
device = input_ids.device
bsz, seq_len = input_ids.shape
cfg = self.cfg
wb = int(cfg.word_boundary_id)
mb = int(cfg.morpheme_boundary_id)
special_hi = 256
is_wb = input_ids == wb
is_mb = input_ids == mb
is_special = (input_ids >= 0) & (input_ids < special_hi)
is_content = ~is_special
is_other_special = is_special & ~is_wb & ~is_mb
# word_pos: cumulative count of word_boundary occurrences with a
# per-row shift. If the very first word_boundary appears before
# any content token, the first wb anchors word 0 (shift = -1).
# Otherwise content tokens implicitly open word 0 and the first
# wb opens word 1 (shift = 0).
wb_cum = is_wb.long().cumsum(dim=1)
inf = seq_len + 1
any_wb = is_wb.any(dim=1)
any_content = is_content.any(dim=1)
first_wb = torch.where(
any_wb,
is_wb.long().argmax(dim=1),
torch.full((bsz,), inf, device=device, dtype=torch.long),
)
first_content = torch.where(
any_content,
is_content.long().argmax(dim=1),
torch.full((bsz,), inf, device=device, dtype=torch.long),
)
shift = torch.where(
first_wb < first_content,
torch.full((bsz,), -1, device=device, dtype=torch.long),
torch.zeros(bsz, device=device, dtype=torch.long),
)
word_pos = (wb_cum + shift.unsqueeze(-1)).clamp(min=0)
# morph_depth: cumulative morpheme_boundary count since the most
# recent reset (word_boundary or other special). The reset
# position itself reads depth 0.
cum_inc = is_mb.long().cumsum(dim=1)
reset_positions = is_wb | is_other_special
reset_value = torch.where(
reset_positions, cum_inc, torch.full_like(cum_inc, -1)
)
last_reset, _ = reset_value.cummax(dim=1)
depth = cum_inc - last_reset.clamp(min=0)
depth = torch.where(reset_positions, torch.zeros_like(depth), depth)
return word_pos.to(torch.long), depth.to(torch.long)
def _morph_info_for_position_contract(
self,
input_ids: torch.Tensor,
attention_mask: torch.Tensor,
position_contract: str,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Derive positions for one declared OCR train/inference contract."""
contract = validate_ocr_position_contract(position_contract)
if contract == BOUNDARY_V1:
word_pos, morph_depth = self._default_morph_info(
input_ids,
attention_mask,
)
if self.cfg.max_morph_depth > 0:
morph_depth = morph_depth.clamp(
max=self.cfg.max_morph_depth - 1
)
return word_pos, morph_depth
if contract == LEGACY_SEQUENTIAL_V0:
seq_len = input_ids.shape[1]
word_pos = torch.arange(
seq_len,
device=input_ids.device,
dtype=torch.long,
).unsqueeze(0).expand_as(input_ids)
word_pos = torch.where(
attention_mask.to(torch.bool),
word_pos,
torch.zeros_like(word_pos),
)
return word_pos, torch.zeros_like(word_pos)
raise AssertionError(f"unreachable position contract: {contract}")
def _morph_info_from_track_table(
self,
input_ids: torch.Tensor,
track_table: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Vectorized span-equivalent morphology for tokenizer-routed ids.
Track ``0`` resets the current word, ``1`` is a Mongolian word piece,
and ``2`` is a general-script word piece. Strict OCR data admits only
text for which this id-only representation exactly matches the
tokenizer's span-aware pretraining representation.
"""
if track_table.ndim != 1:
raise ValueError("morphology_track_table must be 1-D")
if track_table.device != input_ids.device:
raise ValueError(
"morphology_track_table must be on the same device as input_ids"
)
tracks = track_table[input_ids].to(torch.long)
is_word = tracks > 0
previous_word = torch.zeros_like(is_word)
previous_word[:, 1:] = is_word[:, :-1]
same_track = torch.zeros_like(is_word)
same_track[:, 1:] = tracks[:, 1:] == tracks[:, :-1]
word_start = is_word & ~(previous_word & same_track)
starts_seen = word_start.long().cumsum(dim=1)
word_pos = (starts_seen - 1).clamp(min=0)
positions = torch.arange(
input_ids.shape[1],
device=input_ids.device,
dtype=torch.long,
).unsqueeze(0).expand_as(input_ids)
start_positions = torch.where(
word_start,
positions,
torch.full_like(positions, -1),
)
latest_start = start_positions.cummax(dim=1).values
morph_depth = torch.where(
is_word,
positions - latest_start.clamp(min=0),
torch.zeros_like(positions),
)
return word_pos, morph_depth
def _check_inputs(
self,
input_ids: torch.Tensor,
attention_mask: torch.Tensor | None,
labels: torch.Tensor | None,
) -> None:
if input_ids.ndim != 2:
raise ValueError("input_ids must have shape [B, L]")
if input_ids.numel() == 0:
raise ValueError("input_ids cannot be empty")
# Dtype + shape checks only; value-range checks are skipped here
# to avoid host syncs on the hot path. The embedding lookup will
# raise an out-of-range error if vocab bounds are violated, and
# we rely on the data pipeline to keep ids well-formed.
if input_ids.dtype not in (torch.int32, torch.int64):
raise TypeError("input_ids must be int32 or int64")
if attention_mask is not None and attention_mask.shape != input_ids.shape:
raise ValueError("attention_mask must have shape [B, L]")
if labels is not None and labels.shape != input_ids.shape:
raise ValueError("labels must have shape [B, L]")
if input_ids.shape[1] > self.cfg.max_seq_len:
raise ValueError("sequence length exceeds max_seq_len")
def _init_weights(self, module: nn.Module) -> None:
std = self.cfg.init_std
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, mean=0.0, std=std)
if module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
nn.init.normal_(module.weight, mean=0.0, std=std)
if module.padding_idx is not None:
with torch.no_grad():
module.weight[module.padding_idx].zero_()
# Last linear of every residual write: attention output, SwiGLU down
# projection, and the SSM output projection (both NaiveSSM and the
# official Mamba3 expose it as ``out_proj``).
_RESIDUAL_PROJ_SUFFIXES = ("attn.o_proj", "ffn.w_down", "mamba.out_proj")
@torch.no_grad()
def _scale_residual_projections(self) -> None:
"""GPT-2-style depth-scaled init, extended to the unrolled loop.
Every sublayer writes ``x + f(x)`` into the residual stream. With a
weight-shared recurrent core the stream sees its core-aware
``effective_depth`` (including one-pass encoder/local stages and the
repeated refinement stage), so leaving the output projections at
``init_std`` makes the hidden-state norm
grow linearly across recurrent steps (and costs bf16 mantissa
precision late in the loop). Scaling them by
``1/sqrt(2 * effective_depth)`` keeps the residual variance roughly
depth-invariant at init.
"""
scale = (2.0 * self.cfg.effective_depth) ** -0.5
for container in (self.prelude, self.recurrent, self.coda):
for name, module in container.named_modules():
if isinstance(module, nn.Linear) and name.endswith(
self._RESIDUAL_PROJ_SUFFIXES
):
module.weight.mul_(scale)
def _kl_exit_active(self, recurrent_steps: int | None) -> bool:
"""Whether zero-shot KL early-exit governs this decode step.
Gated on ``cfg.kl_exit_threshold > 0`` and only on the cache-free path
with no explicit ``recurrent_steps`` override; when disabled the decode
is bit-exact with a single fixed-depth forward.
"""
return (
self.cfg.kl_exit_threshold > 0.0
and recurrent_steps is None
and self.cfg.core_type == "two_stage"
)
@torch.no_grad()
def _adaptive_depth_logits(
self,
window: torch.Tensor,
pixel_values: torch.Tensor | Mapping[str, torch.Tensor] | None,
visual_features: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
morphology_track_table: torch.Tensor | None = None,
word_pos: torch.Tensor | None = None,
morph_depth: torch.Tensor | None = None,
detail_memory: torch.Tensor | None = None,
detail_cu_seqlens: torch.Tensor | None = None,
) -> torch.Tensor:
"""Last-position logits with per-token adaptive recurrent depth.
Runs the refinement at increasing depth ``d = 1 .. recurrent_steps`` and
stops as soon as the symmetric-free KL between the depth-``d`` and
depth-``d-1`` last-token distributions drops below
``cfg.kl_exit_threshold`` for every row (Huginn arXiv:2502.05171 §6.1,
zero-shot convergence of the latent recurrence). Saves test-time compute
on "easy" tokens while spending full depth on hard / rare ones. Always
returns the deepest distribution actually computed.
"""
max_steps = self.cfg.recurrent_steps
threshold = self.cfg.kl_exit_threshold
prev_logp = None
logits = None
if (word_pos is None) != (morph_depth is None):
raise ValueError("word_pos and morph_depth must be supplied together")
morphology_kwargs = (
{"word_pos": word_pos, "morph_depth": morph_depth}
if word_pos is not None
else {}
if morphology_track_table is None
else {"morphology_track_table": morphology_track_table}
)
for d in range(1, max_steps + 1):
if pixel_values is not None or visual_features is not None:
out = self.forward(
window,
attention_mask=attention_mask,
steps=d,
return_logits=True,
pixel_values=pixel_values,
visual_features=visual_features,
detail_memory=detail_memory,
detail_cu_seqlens=detail_cu_seqlens,
**morphology_kwargs,
)
else:
out = self.forward(
window,
attention_mask=attention_mask,
steps=d,
return_logits=True,
detail_memory=detail_memory,
detail_cu_seqlens=detail_cu_seqlens,
**morphology_kwargs,
)
logits = out["logits"][:, -1, :].float()
logp = F.log_softmax(logits, dim=-1)
if prev_logp is not None:
# KL(p_d || p_{d-1}) per row; exit when all rows have converged.
kl = (logp.exp() * (logp - prev_logp)).sum(dim=-1)
if bool((kl < threshold).all()):
break
prev_logp = logp
return logits
@torch.no_grad()
def generate(
self,
input_ids: torch.Tensor,
max_new_tokens: int = 64,
temperature: float = 1.0,
top_k: int | None = None,
top_p: float | None = None,
min_p: float | None = None,
greedy: bool = False,
eos_id: int | None = None,
pad_id: int | None = None,
repetition_penalty: float = 1.0,
use_cache: bool = False,
stop_ids: Sequence[int] | None = None,
on_token: Callable[[int, torch.Tensor], None] | None = None,
on_sample: (
Callable[[int, torch.Tensor, torch.Tensor], None] | None
) = None,
recurrent_steps: int | None = None,
pixel_values: torch.Tensor | Mapping[str, torch.Tensor] | None = None,
morphology_track_table: torch.Tensor | None = None,
pixel_repeats: int = 1,
position_contract: str | None = None,
detail_memory: torch.Tensor | None = None,
detail_cu_seqlens: torch.Tensor | None = None,
) -> torch.Tensor:
"""Autoregressively continue ``input_ids`` (``[B, L]``) with sampling.
Sampling controls: ``temperature`` then ``min_p`` (ICLR 2025), ``top_k``
and nucleus ``top_p`` truncation, plus CTRL-style ``repetition_penalty``;
``greedy=True`` takes the argmax.
With ``use_cache=True`` (``core_type='two_stage'`` only) decoding runs an
incremental KV/state cache that is numerically identical to the cache-
free path but processes one token per step instead of re-running the
whole prefix:
* **Stage 1 (Mamba)** keeps a constant-size ``(conv_state, ssm_state)``
per layer and steps the selective SSM once per new token -- O(1) time
and memory per step (Gu & Dao, "Mamba", 2023/2024).
* **Stage 2 (MLA)** appends the new token's ``K``/``V`` to a per-layer
cache and attends over the frozen prefix; because each refinement pass
is causal and earlier positions are frozen once computed, one cache is
kept per ``(refinement step, layer)`` pair.
The cached path keeps the full context (no sliding-window eviction), so
``L + max_new_tokens`` must stay within ``max_seq_len``; the cache-free
path instead slides a ``max_seq_len`` window. ``use_cache`` is only
supported for ``core_type='two_stage'``.
Returns the full sequence ``[B, L + n]`` where ``n <= max_new_tokens``.
Generation stops early for a row once it emits ``eos_id`` (subsequent
positions are filled with ``pad_id``).
``stop_ids`` adds extra stop tokens: a row also finishes once it emits
any id in ``stop_ids`` (the stop token itself is kept in the output, so
a harness can see *which* delimiter — e.g. ``</think>`` or a tool-call
close token — ended the segment, then resume by calling ``generate``
again on the returned sequence). ``eos_id`` is always a stop token.
``on_token`` is an optional callback invoked as ``on_token(step, tok)``
after each decoding step with the step index and the ``[B]`` tensor of
ids that were just appended to the sequence; use it for streaming. Note
rows that have already finished receive ``pad_id`` (not a freshly
sampled id), matching exactly what is written to the output. Both are
backward compatible: when unset, behaviour is identical to before.
``on_sample`` additionally receives
``on_sample(step, token_ids, token_log_probs)``. The log-probabilities
are captured from the exact filtered distribution used by sampling,
before any later policy update. Finished rows receive zero because
their appended PAD is not a sampled action. OCR-GRPO uses this callback
to verify synchronous on-policy rollout/scoring consistency.
``recurrent_steps`` overrides the recurrent-depth refinement count for
this decode call (default ``None`` -> ``cfg.recurrent_steps``). Because
RDT reasons in latent depth, raising this spends more test-time compute
("think harder") per token without emitting any extra tokens; lowering
it trades quality for speed. The override is constant for the whole call
so the incremental KV/state cache stays consistent across positions.
``pixel_values`` optionally supplies image pixels for prompts containing
``<image_patch>`` slots. Cached decoding consumes them only during the
prefill step; cache-free decoding refuses sliding-window truncation with
images because dropping patch slots would desync the visual payload. OMVT
mapping inputs are encoded/projected once per generation call and their
visual features are reused across decode steps.
"""
if input_ids.dim() != 2:
raise ValueError("input_ids must have shape [B, L]")
if max_new_tokens < 0:
raise ValueError("max_new_tokens must be non-negative")
if temperature <= 0:
raise ValueError("temperature must be positive")
if top_k is not None and top_k <= 0:
raise ValueError("top_k must be positive when set")
if top_p is not None and not (0.0 < top_p <= 1.0):
raise ValueError("top_p must be in (0, 1] when set")
if min_p is not None and not (0.0 < min_p <= 1.0):
raise ValueError("min_p must be in (0, 1] when set")
if repetition_penalty <= 0:
raise ValueError("repetition_penalty must be positive")
if recurrent_steps is not None and recurrent_steps <= 0:
raise ValueError("recurrent_steps must be positive when set")
if type(pixel_repeats) is not int or pixel_repeats <= 0:
raise ValueError("pixel_repeats must be a positive integer")
if (detail_memory is None) != (detail_cu_seqlens is None):
raise ValueError(
"detail_memory and detail_cu_seqlens must be supplied together"
)
if detail_memory is not None:
if self.vision_cross_attention is None:
raise RuntimeError(
"detail memory requires install_vision_cross_attention()"
)
if pixel_repeats != 1:
raise ValueError(
"detail memory does not support pixel_repeats; repeat its "
"ragged offsets explicitly"
)
if position_contract is not None:
position_contract = validate_ocr_position_contract(position_contract)
if morphology_track_table is not None:
raise ValueError(
"morphology_track_table cannot be combined with an explicit "
"OCR position contract"
)
if use_cache and self.cfg.core_type != "two_stage":
raise NotImplementedError(
"use_cache=True is only supported for core_type="
"'two_stage'"
)
if use_cache and self.cfg.use_official_mamba:
raise NotImplementedError(
"use_cache=True requires the NaiveSSM fallback backend; "
"official Mamba kernels are not steppable by the decode cache. "
"Pass --mamba=naive for cached decoding or use_cache=False."
)
if morphology_track_table is not None:
if morphology_track_table.ndim != 1:
raise ValueError("morphology_track_table must be 1-D")
if morphology_track_table.shape[0] < self.cfg.vocab_size:
raise ValueError(
"morphology_track_table is shorter than model vocab_size"
)
if morphology_track_table.device != input_ids.device:
raise ValueError(
"morphology_track_table must share input_ids device"
)
if bool(
(
(morphology_track_table < 0)
| (morphology_track_table > 2)
).any()
):
raise ValueError(
"morphology_track_table values must be reset/mn/general"
)
cfg = self.cfg
eos_id = cfg.eos_id if eos_id is None else eos_id
pad_id = cfg.pad_id if pad_id is None else pad_id
was_training = self.training
self.eval()
seq = input_ids
device = seq.device
finished = torch.zeros(seq.shape[0], dtype=torch.bool, device=device)
seq_attention = (
(seq != pad_id).long()
if pad_id is not None
else torch.ones_like(seq, dtype=torch.long)
)
stop_token_ids: list[int] = []
if eos_id is not None:
stop_token_ids.append(int(eos_id))
if stop_ids is not None:
stop_token_ids.extend(int(s) for s in stop_ids)
stop_tensor = (
torch.tensor(sorted(set(stop_token_ids)), device=device, dtype=seq.dtype)
if stop_token_ids
else None
)
decode_cache = None
if use_cache:
from Model.inference.cache import DecodeCache
# The incremental cache prefills every prompt position into the
# MLA/Mamba state with an all-ones mask, so a padded prompt would
# fold pad embeddings into the cache and change logits for shorter
# rows. Reject padded prompts (use the cache-free path for those).
if pad_id is not None and bool((input_ids == pad_id).any()):
raise ValueError(
"use_cache=True requires pad-free prompts; the incremental "
"cache cannot mask padding. Use use_cache=False for padded "
"batches."
)
# The cache has no eviction, so the whole run must fit in context.
# Guard up front (counting the tokens about to be appended) instead
# of generating one token past the limit before raising.
if input_ids.shape[1] + max_new_tokens > cfg.max_seq_len:
raise ValueError(
"cached generation requires L + max_new_tokens <= "
f"max_seq_len ({cfg.max_seq_len}); got L={input_ids.shape[1]}"
f" + max_new_tokens={max_new_tokens}. Reduce max_new_tokens "
"or use use_cache=False."
)
decode_cache = DecodeCache()
try:
# The cache-free official-Mamba path re-runs the text model for every
# generated token. Re-running the unchanged OMVT tower there would
# multiply visual compute by max_new_tokens. Cache only Mapping
# (production OMVT) inputs; retain the legacy Tensor path verbatim.
cached_visual_features = None
runtime_pixel_values = pixel_values
if isinstance(pixel_values, Mapping):
cached_visual_features = self.vision.encode_visual(
pixel_values,
pixel_repeats=pixel_repeats,
)
runtime_pixel_values = None
elif pixel_repeats != 1:
raise ValueError(
"pixel_repeats is only supported for OMVT mapping inputs"
)
generic_morphology_kwargs = (
{}
if morphology_track_table is None
else {"morphology_track_table": morphology_track_table}
)
for step in range(max_new_tokens):
if use_cache:
if decode_cache.seq_len == 0:
step_ids = seq
step_pixels = runtime_pixel_values
step_features = cached_visual_features
else:
step_ids = seq[:, -1:]
# The image was folded into the cache at prefill; later
# steps process only the new token and carry no
# <image_patch> slots, so pixel_values must be dropped.
step_pixels = None
step_features = None
if position_contract is not None:
word_pos, morph_depth = (
self._morph_info_for_position_contract(
seq,
seq_attention,
position_contract,
)
)
elif morphology_track_table is not None:
word_pos, morph_depth = (
self._morph_info_from_track_table(
seq,
morphology_track_table,
)
)
else:
word_pos, morph_depth = self._default_morph_info(
seq, seq_attention
)
m = step_ids.shape[1]
logits = self._forward_decode(
step_ids,
word_pos=word_pos[:, -m:],
morph_depth=morph_depth[:, -m:],
cache=decode_cache,
pixel_values=step_pixels,
visual_features=step_features,
detail_memory=detail_memory,
detail_cu_seqlens=detail_cu_seqlens,
steps=recurrent_steps,
)[:, -1, :].float()
else:
window = seq
window_attention = seq_attention
if window.shape[1] > cfg.max_seq_len:
window = window[:, -cfg.max_seq_len:]
window_attention = window_attention[:, -cfg.max_seq_len:]
if pixel_values is not None:
# Left-truncation could drop <image_patch> slots while
# pixel_values still holds the full visual payload,
# desyncing the injector. Refuse loudly.
raise ValueError(
"cache-free image generation cannot truncate the "
"context (L + generated tokens exceeded "
"max_seq_len); the <image_patch> slots would "
"desync from pixel_values. Use use_cache=True or "
"keep L + max_new_tokens <= max_seq_len."
)
position_kwargs = {}
if position_contract is not None:
word_pos, morph_depth = (
self._morph_info_for_position_contract(
window,
window_attention,
position_contract,
)
)
position_kwargs = {
"word_pos": word_pos,
"morph_depth": morph_depth,
}
if self._kl_exit_active(recurrent_steps):
logits = self._adaptive_depth_logits(
window,
runtime_pixel_values,
visual_features=cached_visual_features,
attention_mask=window_attention,
morphology_track_table=morphology_track_table,
detail_memory=detail_memory,
detail_cu_seqlens=detail_cu_seqlens,
**position_kwargs,
)
elif (
runtime_pixel_values is not None
or cached_visual_features is not None
or detail_memory is not None
):
out = self.forward(
window,
attention_mask=window_attention,
steps=recurrent_steps,
return_logits=True,
pixel_values=runtime_pixel_values,
visual_features=cached_visual_features,
detail_memory=detail_memory,
detail_cu_seqlens=detail_cu_seqlens,
**position_kwargs,
**generic_morphology_kwargs,
)
logits = out["logits"][:, -1, :].float()
else:
out = self.forward(
window,
attention_mask=window_attention,
steps=recurrent_steps,
return_logits=True,
**position_kwargs,
**generic_morphology_kwargs,
)
logits = out["logits"][:, -1, :].float()
if repetition_penalty != 1.0:
logits = self._apply_repetition_penalty(
logits, seq, repetition_penalty
)
if greedy:
next_token = torch.argmax(logits, dim=-1)
sampling_logp = (
F.log_softmax(logits.float(), dim=-1)
.gather(-1, next_token.unsqueeze(-1))
.squeeze(-1)
if on_sample is not None
else None
)
else:
logits = logits / temperature
logits = self._filter_logits(logits, top_k, top_p, min_p)
if on_sample is not None:
sampling_distribution = F.log_softmax(
logits.float(), dim=-1
)
probs = sampling_distribution.exp()
else:
sampling_distribution = None
probs = F.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1).squeeze(-1)
sampling_logp = (
sampling_distribution.gather(
-1, next_token.unsqueeze(-1)
).squeeze(-1)
if sampling_distribution is not None
else None
)
was_finished = finished
next_token = torch.where(
was_finished, torch.full_like(next_token, pad_id), next_token
)
next_token = next_token.to(seq.dtype)
seq = torch.cat([seq, next_token.unsqueeze(1)], dim=1)
next_attention = (~was_finished).to(seq_attention.dtype).unsqueeze(1)
seq_attention = torch.cat([seq_attention, next_attention], dim=1)
if on_token is not None:
on_token(step, next_token)
if on_sample is not None:
assert sampling_logp is not None
on_sample(
step,
next_token,
torch.where(
was_finished,
torch.zeros_like(sampling_logp),
sampling_logp,
),
)
if stop_tensor is not None:
finished = finished | torch.isin(next_token, stop_tensor)
else:
finished = finished | (next_token == eos_id)
if bool(finished.all()):
break
finally:
if was_training:
self.train()
return seq
@staticmethod
def _apply_repetition_penalty(
logits: torch.Tensor, seq: torch.Tensor, penalty: float
) -> torch.Tensor:
"""Divide logits of already-seen tokens by ``penalty`` (CTRL-style)."""
for row in range(seq.shape[0]):
seen = torch.unique(seq[row])
row_logits = logits[row, seen]
logits[row, seen] = torch.where(
row_logits > 0, row_logits / penalty, row_logits * penalty
)
return logits
@staticmethod
def _filter_logits(
logits: torch.Tensor,
top_k: int | None,
top_p: float | None,
min_p: float | None = None,
) -> torch.Tensor:
"""Apply top-k, nucleus (top-p) and min-p masking to ``[B, V]`` logits.
min-p (Nguyen et al., "Turning Up the Heat: Min-p Sampling for
Creative and Coherent LLM Outputs", ICLR 2025) keeps only tokens whose
probability is at least ``min_p * p_max`` where ``p_max`` is the top
token's probability. The candidate pool scales with the model's own
confidence: sharp distributions prune hard, flat ones stay permissive.
It is applied before top-k/top-p so it can act as the primary truncation.
"""
if min_p is not None:
probs = F.softmax(logits, dim=-1)
p_max = probs.max(dim=-1, keepdim=True).values
logits = logits.masked_fill(probs < min_p * p_max, float("-inf"))
if top_k is not None:
k = min(top_k, logits.shape[-1])
kth = torch.topk(logits, k, dim=-1).values[:, -1, None]
logits = logits.masked_fill(logits < kth, float("-inf"))
if top_p is not None:
sorted_logits, sorted_idx = torch.sort(logits, descending=True, dim=-1)
cum_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
remove = cum_probs > top_p
remove[:, 1:] = remove[:, :-1].clone()
remove[:, 0] = False
remove_idx = remove.scatter(1, sorted_idx, remove)
logits = logits.masked_fill(remove_idx, float("-inf"))
return logits
def count_params(self, trainable_only: bool = False) -> int:
seen: set[int] = set()
total = 0
for param in self.parameters():
if trainable_only and not param.requires_grad:
continue
ptr = param.data_ptr()
if ptr in seen:
continue
seen.add(ptr)
total += param.numel()
return total
def _check() -> None:
from Model.config import tiny_config
torch.manual_seed(0)
cfg = tiny_config()
model = RDTForCausalLM(cfg)
print("RDTForCausalLM")
print(f" params: {model.count_params():,}")
print(f" actual_layers: {cfg.actual_layers}")
print(f" effective_depth: {cfg.effective_depth}")
bsz, seq_len = 2, 32
input_ids = torch.randint(256, 24576, (bsz, seq_len))
input_ids[:, 0] = cfg.bos_id
attention_mask = torch.ones(bsz, seq_len, dtype=torch.long)
labels = input_ids.clone()
out = model(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels,
)
print("Text")
print(f" logits: {tuple(out['logits'].shape)}")
print(f" loss: {out['loss'].item():.6f}")
print(f" loss_parts: {out['loss_parts']}")
print(f" steps_used: {out['rec_info'].get('steps_used')}")
out["loss"].backward()
grad_sq = 0.0
for p in model.parameters():
if p.grad is not None:
grad_sq += p.grad.norm().item() ** 2
print(f" grad_norm: {grad_sq**0.5:.6f}")
ids2 = torch.tensor(
[
[
cfg.bos_id,
300,
cfg.image_start_id,
cfg.image_patch_id,
cfg.image_patch_id,
cfg.image_end_id,
301,
cfg.eos_id,
]
]
)
labels2 = ids2.clone()
labels2[ids2 == cfg.image_start_id] = cfg.ignore_index
labels2[ids2 == cfg.image_patch_id] = cfg.ignore_index
labels2[ids2 == cfg.image_end_id] = cfg.ignore_index
pixel_values = torch.randn(2, model.patch_pixels)
out2 = model(
input_ids=ids2,
labels=labels2,
pixel_values=pixel_values,
)
print("ImageText")
print(f" loss: {out2['loss'].item():.6f}")
model.eval()
with torch.no_grad():
for steps in [2, 8]:
out_step = model(input_ids=input_ids, steps=steps)
print(
f" steps={steps}, logits_norm={out_step['logits'].norm().item():.6f}"
)
if __name__ == "__main__":
_check()
# -*- coding: utf-8 -*-
from __future__ import annotations
from collections.abc import Mapping
from typing import Callable, Sequence
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.checkpoint import checkpoint
from Model.blocks import StandardBlock
from Model.config import RDTConfig
from Model.layers.rmsnorm import RMSNorm, GroupedRMSNorm
from Model.layers.vision_cross_attention import RaggedVisionCrossAttention
from Model.ocr.position_contract import (
BOUNDARY_V1,
LEGACY_SEQUENTIAL_V0,
validate_ocr_position_contract,
)
from Model.recurrent import RecurrentCore
from Model.two_stage import TwoStageCore
from Model.vision import VisionInjector
class RDTForCausalLM(nn.Module):
def __init__(self, cfg: RDTConfig, patch_pixels: int = 14 * 14 * 3):
super().__init__()
self.cfg = cfg
self.patch_pixels = patch_pixels
self.embed = nn.Embedding(
cfg.vocab_size,
cfg.d_model,
padding_idx=cfg.pad_id,
)
self.vision = VisionInjector(cfg, patch_pixels)
self.prelude = nn.ModuleList(
StandardBlock(cfg, layer_idx=i) for i in range(cfg.n_prelude)
)
if cfg.core_type == "two_stage":
self.recurrent = TwoStageCore(cfg)
else:
self.recurrent = RecurrentCore(cfg)
self.coda = nn.ModuleList(
StandardBlock(cfg, layer_idx=cfg.n_prelude + i) for i in range(cfg.n_coda)
)
self.final_norm = RMSNorm(cfg.d_model, eps=cfg.rmsnorm_eps)
self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
self.bidirectional = cfg.bidirectional
if self.bidirectional:
self.reverse_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
else:
self.reverse_head = None
# Runtime toggle: OCR alignment and GRPO disable the
# bidirectional reverse-LM auxiliary loss so it does not pollute the
# supervised/preference gradient. Pretraining keeps it enabled.
self.reverse_loss_enabled = True
# OMVT-v2 detail memory is opt-in. Keeping this as ``None`` means a
# v1 model creates no bridge parameters and executes no bridge path.
self.vision_cross_attention: RaggedVisionCrossAttention | None = None
self.apply(self._init_weights)
self._scale_residual_projections()
for module in self.modules():
if isinstance(module, (RMSNorm, GroupedRMSNorm)):
module.token_chunk_size = cfg.norm_token_chunk_size
if cfg.tie_word_embeddings:
self.lm_head.weight = self.embed.weight
if self.reverse_head is not None:
self.reverse_head.weight = self.embed.weight
def install_vision_cross_attention(
self,
*,
memory_dim: int,
n_heads: int | None = None,
dropout: float = 0.0,
) -> RaggedVisionCrossAttention:
"""Explicitly install the teacher-forced OMVT-v2 detail bridge.
Generation and incremental cache behavior are deliberately not
changed here. A second install is rejected so a trained bridge cannot
be silently reinitialized.
"""
if self.vision_cross_attention is not None:
raise RuntimeError("vision cross-attention is already installed")