RetNet-0.5B-v21 / modeling_retnet.py
Ayaka157's picture
Upload folder using huggingface_hub
a9e39bc verified
Raw History Blame Contribute Delete
32.2 kB
"""HuggingFace-style RetNet implementation — algorithm identical to the
handwritten retnet/model.py, wrapped for the mature ecosystem (Trainer,
Accelerate, safetensors, PreTrainedModel).
Parameter names are kept byte-for-byte identical to the handwritten model so
that ``load_state_dict(old_checkpoint["model_state"], strict=True)`` works
without any renaming.
model = RetNetForCausalLM(config)
model.load_state_dict(strip_optimized_prefix(old["model_state"]))
"""
from __future__ import annotations
import os
from typing import List, Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import PreTrainedModel, PretrainedConfig
#: Opt-in batched-chunk chunkwise retention (see ``retention_fast.py``).
#: Mathematically identical to the reference per-chunk loop but replaces the
#: Python loop's per-chunk slicing + small matmuls with 3 batched matmuls and a
#: tiny state scan. Verified equivalent by ``scripts/test_fast_chunkwise.py``.
FAST_CHUNKWISE = os.environ.get("RETNET_FAST_CHUNKWISE", "0") == "1"
# NOTE(2026-10-10): 试过给 ``MultiScaleRetention._rotations`` 加一层
# ``(length, offset, device, dtype)`` 键的 sin/cos 表缓存,理由是 profile 里
# ``triton_poi_fused__to_copy*`` 合计 42.9 ms(占一个优化步 5.9%),而这 20 层
# 每次前向算的都是同一张表。**实测没有收益**:缓存开 1080 ms / 关 1084 ms
# (同一供电状态下背靠背),逐字符还原成原始实现也仍是 1078 ms。
# 结论是 inductor 已经把这部分处理掉了,Python 侧的缓存只是在编译区域里
# 多插一层间接。**不要再加这个缓存。**
class RetNetHFConfig(PretrainedConfig):
model_type = "retnet_hf"
def __init__(self, vocab_size=8192, d_model=512, num_layers=6, num_heads=8,
value_dim=None, ffn_dim=None, dropout=0.1, norm_eps=1e-5,
tie_weights=True, embed_scale=None, prompt_loss_weight=0.1,
use_gla=False, use_swiglu=False, use_growth_ln=False,
growth_old_dim=None, checkpoint_layers=-1, **kwargs):
super().__init__(**kwargs)
self.vocab_size = vocab_size
self.d_model = d_model
self.num_layers = num_layers
self.num_heads = num_heads
self.value_dim = value_dim if value_dim is not None else 2 * d_model
self.ffn_dim = ffn_dim if ffn_dim is not None else 2 * d_model
self.dropout = dropout
self.norm_eps = norm_eps
self.tie_weights = tie_weights
self.embed_scale = embed_scale
self.prompt_loss_weight = prompt_loss_weight
self.use_gla = use_gla
self.use_swiglu = use_swiglu
self.use_growth_ln = use_growth_ln
self.growth_old_dim = growth_old_dim
#: 只对前 N 层做梯度检查点;-1 = 全部(沿用 HF 的全局开关)。
#: 分层取舍的依据:重算开销与显存占用都随被检查点的层数线性变化,
#: 而激活值总量也是。取中间值可以在同样的 8 GiB 里换到更大的物理 batch,
#: 而更大的 batch 又能摊薄内核启动数(见 docs/15 §0.2)。
self.checkpoint_layers = checkpoint_layers
@property
def key_dim(self) -> int:
return self.d_model // self.num_heads
def rotate_every_two(x: torch.Tensor) -> torch.Tensor:
x1 = x[..., 0::2]
x2 = x[..., 1::2]
x_rot = torch.stack((-x2, x1), dim=-1)
return x_rot.flatten(-2)
def theta_shift(x: torch.Tensor, sin: torch.Tensor, cos: torch.Tensor) -> torch.Tensor:
return x * cos + rotate_every_two(x) * sin
class MultiScaleRetention(nn.Module):
def __init__(self, config: RetNetHFConfig):
super().__init__()
d_model, num_heads, value_dim = config.d_model, config.num_heads, config.value_dim
if d_model % num_heads != 0:
raise ValueError("d_model must be divisible by num_heads")
if value_dim % num_heads != 0:
raise ValueError("value_dim must be divisible by num_heads")
self.d_model = d_model
self.num_heads = num_heads
self.key_dim = d_model // num_heads
self.value_dim = value_dim
self.value_head_dim = value_dim // num_heads
self.scale = self.key_dim ** -0.5
gamma = 1.0 - torch.pow(2.0, -5.0 - torch.arange(num_heads, dtype=torch.float32))
if config.use_gla:
# GLA: 可学习逐头门控(初始值 = RetNet 固定公式,训练中自适应)
self.gamma = nn.Parameter(gamma.clone())
else:
self.register_buffer("gamma", gamma, persistent=True)
inv_freq = 1.0 / (10000.0 ** torch.linspace(0.0, 1.0, steps=self.key_dim // 2,
dtype=torch.float32))
self.register_buffer("inv_freq", inv_freq.repeat_interleave(2), persistent=False)
self.q_proj = nn.Linear(d_model, d_model, bias=False)
self.k_proj = nn.Linear(d_model, d_model, bias=False)
self.v_proj = nn.Linear(d_model, value_dim, bias=False)
self.g_proj = nn.Linear(d_model, value_dim, bias=False)
self.out_proj = nn.Linear(value_dim, d_model, bias=False)
self.dropout = nn.Dropout(config.dropout)
self.group_norm = nn.GroupNorm(num_heads, value_dim, eps=config.norm_eps)
def _init_weights(self):
for name, p in self.named_parameters():
if name.endswith("_proj.weight"):
if name.split(".")[-1].startswith(("q_", "k_", "v_", "g_")):
nn.init.xavier_uniform_(p, gain=2 ** -2.5)
else:
nn.init.xavier_uniform_(p, gain=1.0)
elif name == "gamma":
# GLA 的逐头门控,初值必须是 __init__ 里按 RetNet 固定公式算出的
# 1 - 2^(-5-h),训练中再自适应。**绝不能置零**:
# 之前这里写的是「1 维 Parameter 一律 zeros_」兜底 bias,可本模块
# 所有 _proj 都是 bias=False,于是唯一的 1 维 Parameter 就是 gamma,
# 门控被置零 → log(0) = -inf → 掩码对角线 exp(-inf*0) = NaN,
# 批量 chunkwise 前向整层 NaN(use_gla=True 时每一条路径都中招)。
# 参考实现用 gamma ** delta,0**0 = 1 良定义,所以两条路径在
# gamma=0 处还**不等价**。详见 tests/test_gla_gamma_init.py。
continue
elif name.endswith("bias"):
nn.init.zeros_(p)
def _rotations(self, length, offset, device, dtype):
position = torch.arange(offset, offset + length, device=device, dtype=torch.float32)
freqs = torch.outer(position, self.inv_freq.to(device=device, dtype=torch.float32))
return torch.sin(freqs).to(dtype=dtype), torch.cos(freqs).to(dtype=dtype)
def _project_and_rotate(self, x, offset):
bsz, tgt_len, _ = x.shape
q = self.q_proj(x).view(bsz, tgt_len, self.num_heads, self.key_dim).transpose(1, 2)
k = self.k_proj(x).view(bsz, tgt_len, self.num_heads, self.key_dim).transpose(1, 2)
v = self.v_proj(x).view(bsz, tgt_len, self.num_heads, self.value_head_dim).transpose(1, 2)
g = self.g_proj(x)
k = k * self.scale
sin, cos = self._rotations(tgt_len, offset, x.device, q.dtype)
q = theta_shift(q, sin, cos)
k = theta_shift(k, sin, cos)
return q, k, v, g
@staticmethod
def _head_group_norm(x, num_heads, group_norm, bsz, tgt_len):
"""GroupNorm over ``[B,H,L,vhd]``,输出 ``[B,L,H*vhd]``。
NOTE(2026-10-10): 试过改写成「先在 ``[B,H,L,vhd]`` 上手工算 mean/var
归一化、最后再 permute 一次」,理由是省掉参考实现里那次物化拷贝。
**实测反而慢 9.2%**(190 vs 174 ms/微步,真实 Trainer 路径,两轮一致):
参考实现把 permute 放在**归一化之前**,那次拷贝随即被逐元素归一化核吸收;
而放到**之后**,它直接成为 ``out_proj`` 这个外部 cuBLAS 调用的输入,
再也融不进任何东西。等价性没问题(fp32 相对差 1.6e-7,bf16 5.1e-3 均
在 1 ULP 量级,见 ``tests/test_head_group_norm.py``),但收益是负的。
**不要再动这个布局。**
"""
x = x.transpose(1, 2).reshape(bsz * tgt_len, num_heads * x.shape[-1])
x = group_norm(x.unsqueeze(-1)).squeeze(-1)
return x.reshape(bsz, tgt_len, -1)
def _decay_mask(self, length, device, dtype):
idx = torch.arange(length, device=device)
delta = idx[:, None] - idx[None, :]
causal = delta >= 0
exp = delta.masked_fill(~causal, 0)
mask = self.gamma.to(device=device, dtype=torch.float32)[:, None, None] ** exp
mask = mask.masked_fill(~causal, 0.0)
mask = mask / mask.sum(dim=-1, keepdim=True).sqrt().clamp_min(1e-8)
return mask.to(dtype=dtype)
def _parallel_retention(self, q, k, v):
bsz, _, tgt_len, _ = q.shape
mask = self._decay_mask(tgt_len, q.device, q.dtype)
scores = torch.matmul(q, k.transpose(-1, -2)) * mask
scores = scores / scores.detach().abs().sum(dim=-1, keepdim=True).clamp_min(1.0)
return torch.matmul(scores, v)
def _parallel_final_state(self, k, v, state):
bsz, _, tgt_len, key_dim = k.shape
idx = torch.arange(tgt_len, device=k.device)
weights = self.gamma.to(device=k.device, dtype=torch.float32)[:, None] ** (tgt_len - 1 - idx)
new_state = torch.einsum("bhtk,bhtv,ht->bhkv", k, v, weights.to(k.dtype))
if state is not None:
gamma_pow = self.gamma.to(device=k.device, dtype=k.dtype) ** tgt_len
new_state = gamma_pow.view(1, -1, 1, 1) * state + new_state
return new_state
def _recurrent_retention(self, q, k, v, state):
bsz, _, tgt_len, key_dim = q.shape
value_head_dim = v.shape[-1]
if state is None:
state = torch.zeros(bsz, self.num_heads, key_dim, value_head_dim,
device=q.device, dtype=q.dtype)
outputs = []
gamma = self.gamma.to(device=q.device, dtype=q.dtype)
for t in range(tgt_len):
state = gamma.view(1, -1, 1, 1) * state + \
k[:, :, t, :].unsqueeze(-1) * v[:, :, t, :].unsqueeze(-2)
out_t = torch.sum(q[:, :, t, :].unsqueeze(-1) * state, dim=-2)
outputs.append(out_t)
output = torch.stack(outputs, dim=2)
return output, state
def _chunkwise_retention(self, q, k, v, state, chunk_size):
"""Equation (7): inner-chunk parallel + cross-chunk recurrent state."""
if FAST_CHUNKWISE:
from .retention_fast import chunkwise_batched
return chunkwise_batched(q, k, v, self.gamma, state, chunk_size)
bsz, _, tgt_len, key_dim = q.shape
if tgt_len % chunk_size != 0:
raise ValueError(f"chunkwise mode requires seq_len ({tgt_len}) to be a "
f"multiple of chunk_size ({chunk_size})")
if state is None:
state = torch.zeros(bsz, self.num_heads, key_dim, v.shape[-1],
device=q.device, dtype=q.dtype)
gamma = self.gamma.to(device=q.device, dtype=q.dtype)
outputs = []
rel = torch.arange(chunk_size, device=q.device)
delta = rel[:, None] - rel[None, :]
causal = delta >= 0
inner_mask = torch.where(causal, gamma[:, None, None] ** delta.masked_fill(~causal, 0),
torch.zeros_like(delta, dtype=torch.float32))
cross_decay = gamma[:, None] ** (rel + 1)[None, :]
value_decay = gamma[:, None] ** (chunk_size - 1 - rel)[None, :]
chunk_decay = gamma ** chunk_size
for i in range(tgt_len // chunk_size):
sl = slice(i * chunk_size, (i + 1) * chunk_size)
qi, ki, vi = q[:, :, sl], k[:, :, sl], v[:, :, sl]
inner_scores = torch.matmul(qi, ki.transpose(-1, -2)) * inner_mask
inner = torch.matmul(inner_scores, vi)
cross = torch.matmul(qi * cross_decay[None, :, :, None], state)
outputs.append(inner + cross)
weighted_v = vi * value_decay[None, :, :, None]
state = chunk_decay.view(1, -1, 1, 1) * state + \
torch.matmul(ki.transpose(-1, -2), weighted_v)
output = torch.cat(outputs, dim=2)
return output, state
def forward(self, x, mode="parallel", state=None, offset=0, return_state=False,
chunk_size=64):
bsz, tgt_len, _ = x.shape
q, k, v, g = self._project_and_rotate(x, offset)
if mode == "parallel":
output, new_state = self._parallel_retention(q, k, v), None
if return_state:
new_state = self._parallel_final_state(k, v, state)
elif mode == "recurrent":
output, new_state = self._recurrent_retention(q, k, v, state)
elif mode == "chunkwise":
output, new_state = self._chunkwise_retention(q, k, v, state, chunk_size)
else:
raise ValueError(f"unknown mode: {mode}")
output = self._head_group_norm(output, self.num_heads, self.group_norm, bsz, tgt_len)
output = F.silu(g) * output
output = self.dropout(output)
output = self.out_proj(output)
if return_state:
return output, new_state
return output
class GrowthLayerNorm(nn.Module):
"""Two-segment LayerNorm for function-preserving width growth.
The old ``old_dim`` channels are normalized exactly as before (statistics
unaffected by new channels); the new channels are scaled independently with
``gamma_new``/``beta_new`` and stay a zero side branch while
``gamma_new=0, beta_new=0``. This is what makes pre-LN width expansion
strictly function-preserving (BERT2BERT-style FPI for LayerNorm).
state_dict keys: ``ln_old.weight``, ``ln_old.bias``, ``gamma_new``, ``beta_new``.
"""
def __init__(self, old_dim: int, new_dim: int, eps: float = 1e-5,
gamma_new: float = 1.0, beta_new: float = 0.0):
super().__init__()
self.ln_old = nn.LayerNorm(old_dim, eps=eps)
self.gamma_new = nn.Parameter(torch.full((new_dim - old_dim,), gamma_new))
self.beta_new = nn.Parameter(torch.full((new_dim - old_dim,), beta_new))
def forward(self, x: torch.Tensor) -> torch.Tensor:
old = self.ln_old(x[..., : self.ln_old.normalized_shape[0]])
new = x[..., self.ln_old.normalized_shape[0]:] * self.gamma_new + self.beta_new
return torch.cat([old, new], dim=-1)
class RetNetBlock(nn.Module):
def _norm(self, config: RetNetHFConfig, dim: int):
if config.use_growth_ln:
return GrowthLayerNorm(config.growth_old_dim, dim, eps=config.norm_eps,
gamma_new=1.0, beta_new=0.0)
return nn.LayerNorm(dim, eps=config.norm_eps)
def __init__(self, config: RetNetHFConfig, layer_idx: int = 0):
super().__init__()
self.layer_idx = layer_idx
#: 该层是否做梯度检查点。-1 表示「跟随 HF 的全局开关」。
self.checkpoint_layers = getattr(config, "checkpoint_layers", -1)
self.msr = MultiScaleRetention(config)
self.norm1 = self._norm(config, config.d_model)
self.gradient_checkpointing = False
if config.use_swiglu:
# SwiGLU FFN:y = silu(xW_gate) * (xW_up) -> W_down
self.ffn = nn.Sequential() # placeholder; real modules below
self.gate_proj = nn.Linear(config.d_model, config.ffn_dim, bias=False)
self.up_proj = nn.Linear(config.d_model, config.ffn_dim, bias=False)
self.down_proj = nn.Linear(config.ffn_dim, config.d_model, bias=False)
self.swiglu = True
else:
self.ffn = nn.Sequential(
nn.Linear(config.d_model, config.ffn_dim, bias=False),
nn.GELU(),
nn.Linear(config.ffn_dim, config.d_model, bias=False),
)
self.swiglu = False
self.norm2 = self._norm(config, config.d_model)
self.dropout = nn.Dropout(config.dropout)
def _block_forward(self, x, mode, state, offset, return_state, chunk_size=64):
msr_out = self.msr(self.norm1(x), mode=mode, state=state, offset=offset,
return_state=return_state, chunk_size=chunk_size)
if return_state:
y, new_state = msr_out
else:
y, new_state = msr_out, None
y = x + y
if self.swiglu:
ffn_out = F.silu(self.gate_proj(self.norm2(y))) * self.up_proj(self.norm2(y))
ffn_out = self.down_proj(ffn_out)
else:
ffn_out = self.ffn(self.norm2(y))
y = y + self.dropout(ffn_out)
if return_state:
return y, new_state
return y
def forward(self, x, mode="parallel", state=None, offset=0, return_state=False,
chunk_size=64):
do_ckpt = self.gradient_checkpointing and self.training
if do_ckpt and 0 <= self.checkpoint_layers <= self.layer_idx:
do_ckpt = False # 分层策略:只检查点前 checkpoint_layers 层
if do_ckpt:
return torch.utils.checkpoint.checkpoint(
self._block_forward, x, mode, state, offset, return_state, chunk_size,
use_reentrant=False)
return self._block_forward(x, mode, state, offset, return_state, chunk_size)
class RetNetForCausalLM(PreTrainedModel):
"""RetNet language model with a from-scratch token embedding.
Forward signature is Trainer-compatible:
forward(input_ids, labels=None, wcodes=None, ...) -> CausalLMOutput
When labels+wcodes are given the loss is the wcode-weighted cross entropy
(wcode: 0=pad, 1=reply, 2=prompt, 3=plain LM) exactly like train.py.
"""
config_class = RetNetHFConfig
base_model_prefix = "retnet"
supports_gradient_checkpointing = True
#: 本模型**不使用** ``num_items_in_batch`` 做损失归一化(loss 已按权重码
#: ``w.sum()`` 归一),但 ``forward`` 签名末尾的 ``**_kwargs`` 会让 HF 的启发式
#: 判定「模型自己会处理归一化」,从而**跳过** ``loss = loss / grad_accum_steps``。
#: 后果是梯度被放大 accum 倍、等效学习率成倍上升 —— 静默,且只在 accum>1 时发作。
#: HF 按 **``accepts_loss_kwargs``**(不是 ``model_accepts_loss_kwargs``)查找;
#: 找不到时它会去猜 forward 签名,本项目会被误判为 True。
accepts_loss_kwargs = False
model_accepts_loss_kwargs = False
def __init__(self, config: RetNetHFConfig):
super().__init__(config)
self.embedding = nn.Embedding(config.vocab_size, config.d_model)
self.embed_dropout = nn.Dropout(config.dropout)
self.embed_scale = config.embed_scale if config.embed_scale is not None \
else config.d_model ** 0.5
self.blocks = nn.ModuleList([RetNetBlock(config, i)
for i in range(config.num_layers)])
if config.use_growth_ln:
self.final_norm = GrowthLayerNorm(config.growth_old_dim, config.d_model,
eps=config.norm_eps,
gamma_new=1.0, beta_new=0.0)
else:
self.final_norm = nn.LayerNorm(config.d_model, eps=config.norm_eps)
self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False)
if config.tie_weights:
self.lm_head.weight = self.embedding.weight
self._tied_weights_keys = ["lm_head.weight"]
self._init_weights_local()
def _init_weights_local(self):
nn.init.normal_(self.embedding.weight, mean=0.0, std=0.02)
for block in self.blocks:
if block.swiglu:
nn.init.xavier_uniform_(block.gate_proj.weight)
nn.init.xavier_uniform_(block.up_proj.weight)
nn.init.xavier_uniform_(block.down_proj.weight)
else:
for name, p in block.ffn.named_parameters():
if name.endswith("weight"):
nn.init.xavier_uniform_(p)
block.msr._init_weights()
if not self.config.tie_weights:
nn.init.xavier_uniform_(self.lm_head.weight)
def tie_weights(self):
"""共享 lm_head 与 embedding(from_pretrained 后恢复 tie)。"""
if self.config.tie_weights:
self.lm_head.weight = self.embedding.weight
def set_checkpoint_layers(self, n: int):
"""只对前 ``n`` 层做梯度检查点(n<0 = 全部)。
必须在 HF 启用梯度检查点**之后**调用,因为它会把所有层的
``gradient_checkpointing`` 置 True。
"""
self.config.checkpoint_layers = n
for b in self.blocks:
b.checkpoint_layers = n
return n
def _forward_hidden(self, input_ids, mode="parallel", states=None, offset=0,
return_state=False, chunk_size=64, return_hidden=False):
if input_ids.ndim != 2:
raise ValueError("input_ids must be [batch, seq]")
x = self.embedding(input_ids) * self.embed_scale
x = self.embed_dropout(x)
if states is None:
states = [None] * len(self.blocks)
if len(states) != len(self.blocks):
raise ValueError("states must contain one entry per block")
new_states: List[Optional[torch.Tensor]] = []
for block, block_state in zip(self.blocks, states):
if return_state or (mode == "recurrent" and block_state is not None):
x, ns = block(x, mode=mode, state=block_state, offset=offset,
return_state=True, chunk_size=chunk_size)
new_states.append(ns)
else:
x = block(x, mode=mode, state=block_state, offset=offset,
chunk_size=chunk_size)
new_states.append(block_state)
x = self.final_norm(x)
if return_hidden:
# 融合交叉熵需要的是 LM head 的**输入**(hidden),不是输出。
# 这里提前返回可以完全不物化 [B,L,V]。
if return_state or mode == "recurrent":
return x, new_states
return x
logits = self.lm_head(x)
if return_state or mode == "recurrent":
return logits, new_states
return logits
def forward(self, input_ids, labels=None, wcodes=None, mode=None,
states=None, offset=0, return_state=False, chunk_size=None, **_kwargs):
# NOTE(2026-10-10): 试过在这里调 torch.compiler.cudagraph_mark_step_begin()
# 以启用 torch.compile(mode="reduce-overhead") 的 CUDA Graphs。**无效**——
# 错误从损失移到 _forward_hidden,仍是 "accessing tensor output of
# CUDAGraphs that has been overwritten / Gradient addition node due to
# multiple use of tensor":这是 CUDA Graphs 与梯度累积(同一张图跑 4 次、
# 各自反向)的硬冲突,不是漏调一个 API 能解决的。已回滚。
# NOTE(2026-10-10): 上面这个默认值以前写的是 64,而 HF Trainer 调用模型时
# **不会**传这个关键字,于是 ``config.chunk_size`` 和任务 JSON 里的
# ``chunk_size`` 一直是**死字段**——项目以为在跑 128,实际永远 64。
# 实测:``config.chunk_size`` 设 64/128/256 的 loss **逐位相同**,而显式
# 传 ``chunk_size=`` 会改变 loss。``bench_step.py`` 的
# ``--sweep chunk_size=...`` 因此扫的是空变量,``docs/15`` §6 的
# 「chunk 512/1024 反而更慢,最优是 128」不成立。
# 现改成 None 再回落到 config,任务配置才真正生效。
# ⚠️ 对已经在跑的任务,这会让 chunk 从 64 跳到配置值 —— 数值会跳变,
# 只能在**新开一轮**时启用,不要热切。
if chunk_size is None:
chunk_size = int(getattr(self.config, "chunk_size", 64) or 64)
if mode is None:
mode = getattr(self.config, "mode", "parallel")
_fused = (os.environ.get("RETNET_FUSED_CE") == "1"
and labels is not None and wcodes is not None)
logits = None
# `loss` 必须在所有分支之前初始化:不传 labels 时(普通推理、prefill、
# 以及 HF 的 generate 路径)上面几个分支都不会赋值,line 536 的
# CausalLMOutput(loss=loss, ...) 会直接 UnboundLocalError。
loss = None
if _fused:
# 融合路径:LM head 的输入直接进 fused CE,完全不物化 [B,L,V]
hidden, new_states = self._forward_hidden(
input_ids, mode=mode, states=states, offset=offset,
return_state=True, chunk_size=chunk_size, return_hidden=True)
B_, L_, D_ = hidden.shape
w = torch.zeros_like(wcodes, dtype=torch.float32)
w = torch.where(wcodes == 1, torch.ones_like(w), w)
w = torch.where(wcodes == 2,
torch.full_like(w, self.config.prompt_loss_weight), w)
w = torch.where(wcodes == 3, torch.ones_like(w), w)
from .fused_ce import fused_linear_cross_entropy
loss = fused_linear_cross_entropy(
hidden.reshape(B_ * L_, D_), self.lm_head.weight,
labels.reshape(-1), w.reshape(-1))
elif return_state or mode == "recurrent" or mode == "chunkwise":
logits, new_states = self._forward_hidden(input_ids, mode=mode, states=states,
offset=offset, return_state=True,
chunk_size=chunk_size)
else:
logits = self._forward_hidden(input_ids, mode=mode, states=states, offset=offset)
new_states = None
if not _fused:
# 非融合路径:沿用原来的显式 fp32 上采样。
# NOTE(2026-10-10): 试过去掉 ``logits.float()``(让 ``F.cross_entropy``
# 直接吃 bf16)。loss 逐位一致但**步时 0% 变化**——profile 把这两个
# 融合核记成 3.9%,是归因假象。故保留原状。
if labels is not None and wcodes is not None:
logits_f = logits.float()
token_loss = F.cross_entropy(
logits_f.reshape(-1, logits_f.shape[-1]), labels.reshape(-1),
reduction="none")
w = torch.zeros_like(wcodes, dtype=torch.float32)
w = torch.where(wcodes == 1, torch.ones_like(w), w)
w = torch.where(wcodes == 2,
torch.full_like(w, self.config.prompt_loss_weight), w)
w = torch.where(wcodes == 3, torch.ones_like(w), w)
loss = (token_loss * w.reshape(-1)).sum() / w.sum().clamp_min(1.0)
elif labels is not None:
logits_f = logits.float()
loss = F.cross_entropy(logits_f.reshape(-1, logits_f.shape[-1]),
labels.reshape(-1))
from transformers.modeling_outputs import CausalLMOutput
return CausalLMOutput(loss=loss, logits=logits)
@torch.inference_mode()
def generate(self, input_ids: torch.Tensor, max_new_tokens: int = 32,
temperature: float = 1.0, top_k: Optional[int] = None,
top_p: Optional[float] = None, repetition_penalty: float = 1.0,
eos_id: Optional[int] = None, device: Optional[torch.device] = None,
penalize_prompt: bool = False, no_repeat_ngram_size: int = 0,
on_token=None) -> torch.Tensor:
"""Autoregressive generation (same algorithm/sampling as handwritten).
``on_token(token_id)`` is called for every accepted token, in order, as
soon as it is sampled. Returning a truthy value stops generation
immediately (the token is still part of the returned sequence). Used by
the interactive play script to stream output and to cut a reply at the
first ``用户:`` / ``助手:`` turn boundary.
"""
device = device or next(self.parameters()).device
ids = input_ids.to(device)
states = [None] * len(self.blocks)
if ids.shape[1] > 1:
logits, states = self._forward_hidden(ids, mode="parallel", return_state=True)
next_logits = logits[:, -1]
else:
next_logits, states = self._forward_hidden(ids, mode="recurrent", states=states,
offset=0, return_state=True)
next_logits = next_logits[:, -1]
generated = list(ids[0].tolist())
prompt_len = ids.shape[1]
offset = ids.shape[1]
for _ in range(max_new_tokens):
penalty_source = generated if penalize_prompt else generated[prompt_len:]
banned = []
if no_repeat_ngram_size and len(generated) >= no_repeat_ngram_size:
n = no_repeat_ngram_size
suffix = tuple(generated[-(n - 1):]) if n > 1 else ()
for i in range(len(generated) - n + 1):
if tuple(generated[i:i + n - 1]) == suffix:
banned.append(generated[i + n - 1])
if temperature > 0:
logits = next_logits / temperature
if repetition_penalty and repetition_penalty != 1.0:
for token_id in set(penalty_source):
score = logits[0, token_id]
logits[0, token_id] = score / repetition_penalty if score >= 0 \
else score * repetition_penalty
for token_id in banned:
logits[0, token_id] = -torch.inf
if top_k is not None:
k = min(top_k, logits.shape[-1])
top_values, _ = torch.topk(logits, k)
logits[logits < top_values[:, -1:]] = -torch.inf
probs = F.softmax(logits, dim=-1)
if top_p is not None and top_p < 1.0:
sorted_probs, sorted_indices = torch.sort(probs, descending=True, dim=-1)
cumsum = sorted_probs.cumsum(dim=-1)
remove = cumsum > top_p
remove[..., 1:] = remove[..., :-1].clone()
remove[..., 0] = False
filtered = torch.zeros_like(probs)
filtered.scatter_(1, sorted_indices[~remove].unsqueeze(0),
sorted_probs[~remove].unsqueeze(0))
probs = filtered / filtered.sum(dim=-1, keepdim=True)
next_id = torch.multinomial(probs, num_samples=1).item()
else:
logits = next_logits.clone()
for token_id in banned:
logits[0, token_id] = -torch.inf
next_id = logits.argmax(dim=-1).item()
if eos_id is not None and next_id == eos_id:
break
generated.append(next_id)
if on_token is not None and on_token(next_id):
break
next_token = torch.tensor([[next_id]], device=device, dtype=torch.long)
next_logits, states = self._forward_hidden(next_token, mode="recurrent",
states=states, offset=offset,
return_state=True)
next_logits = next_logits[:, -1]
offset += 1
return torch.tensor([generated], device=device, dtype=torch.long)