Fables / modeling_fwkv.py
FlameF0X's picture
Upload 2 files
dd08deb verified
Raw History Blame Contribute Delete
14.7 kB
# modeling_fwkv.py
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import PreTrainedModel, AutoModel, AutoConfig
from transformers.modeling_outputs import CausalLMOutputWithPast
from transformers.generation import GenerationMixin
from transformers.cache_utils import Cache # needed to detect Cache objects
from .configuration_fwkv import FWKVConfig
# ════════════════════════════════════════════════════════════════════════════
# utility functions
# ════════════════════════════════════════════════════════════════════════════
def parallel_scan_decay(a: torch.Tensor, W: torch.Tensor) -> torch.Tensor:
"""REFERENCE implementation (Hillis-Steele). y_t = a_t + W * y_{t-1}.
Kept for parity/testing with decay_scan; not used at inference time."""
W = W.to(dtype=a.dtype)
val = a
T = a.shape[1]
d = 1
while d < T:
shifted = F.pad(val[:, :-d, :], (0, 0, d, 0))
val = val + (W ** d) * shifted
d *= 2
return val
def decay_scan(a: torch.Tensor, W: torch.Tensor, state: torch.Tensor | None = None) -> torch.Tensor:
"""y_t = a_t + W * y_{t-1}, y_{-1} = state, as a causal FFT convolution in fp32.
a: (B, T, d), W: (d,) with 0 < W < 1, state: (B, d) or None.
Matches the training-time implementation exactly (same math as
parallel_scan_decay, but computed as an FFT convolution for speed).
"""
with torch.autocast(device_type=a.device.type, enabled=False):
a = a.float()
W = W.float()
B, T, d = a.shape
if state is not None:
first = a[:, :1] + W.view(1, 1, d) * state.float().unsqueeze(1)
a = torch.cat([first, a[:, 1:]], dim=1)
if T == 1:
return a
n = 1 << (2 * T - 1).bit_length() # >= 2T-1, avoids circular wrap
t = torch.arange(T, device=a.device, dtype=a.dtype)
K = torch.exp(t.unsqueeze(1) * torch.log(W).unsqueeze(0)) # (T, d) = W^t
Fa = torch.fft.rfft(a, n=n, dim=1)
Fk = torch.fft.rfft(K, n=n, dim=0)
y = torch.fft.irfft(Fa * Fk.unsqueeze(0), n=n, dim=1)[:, :T]
return y.contiguous()
def chunked_cross_entropy(x, weight, labels, chunk_size=512, ignore_index=-100):
x_2d = x.reshape(-1, x.size(-1))
labels_1d = labels.reshape(-1)
N = x_2d.size(0)
total_loss = x_2d.new_zeros((), dtype=torch.float32)
total_count = labels_1d.new_zeros(())
for start in range(0, N, chunk_size):
end = min(start + chunk_size, N)
label_chunk = labels_1d[start:end]
count = (label_chunk != ignore_index).sum()
if count == 0:
continue
logits_chunk = F.linear(x_2d[start:end], weight)
loss_chunk = F.cross_entropy(logits_chunk.float(), label_chunk, ignore_index=ignore_index, reduction="sum")
total_loss = total_loss + loss_chunk
total_count = total_count + count
return total_loss / total_count.clamp(min=1)
# ════════════════════════════════════════════════════════════════════════════
# modules β€” mirror the trained Myosotis v2 / FWKVBlock exactly
# ════════════════════════════════════════════════════════════════════════════
class FactorizedTiedHead(nn.Module):
def __init__(self, vocab_size, d_model, d_emb):
super().__init__()
self.d_model = d_model
self.d_emb = d_emb
self.weight = nn.Parameter(torch.empty(vocab_size, d_emb))
self.proj = nn.Linear(d_emb, d_model, bias=False)
nn.init.normal_(self.weight, std=0.02)
nn.init.normal_(self.proj.weight, std=0.02)
def embed(self, input_ids):
return self.proj(F.embedding(input_ids, self.weight))
# kept as an alias so either calling convention works
def forward(self, input_ids):
return self.embed(input_ids)
def to_emb_space(self, x):
return F.linear(x, self.proj.weight.t())
def logits(self, x_emb):
return F.linear(x_emb, self.weight)
def resize_vocab(self, new_vocab_size):
old = self.weight.shape[0]
if new_vocab_size == old:
return
new_w = torch.empty(new_vocab_size, self.d_emb, device=self.weight.device, dtype=self.weight.dtype)
nn.init.normal_(new_w, std=0.02)
new_w[:old] = self.weight.data
self.weight = nn.Parameter(new_w)
class FWKVBlock(nn.Module):
"""Pre-norm block: token-shifted WKV mixer + token-shifted FFN.
Per-layer recurrent state = (wkv_state, shift_state_timemix, shift_state_chanmix),
each (B, d). Must match the training-time block bit-for-bit (module names and
math), since checkpoints are loaded by state_dict key.
"""
def __init__(self, d, layer_id, n_layers, ffn_mult=4, floor=0.1, ceil=0.999,
head_size=64, tau_min=2.0, tau_max=1000.0):
super().__init__()
assert d % head_size == 0, "d_model must be divisible by head_size"
self.d = d
self.floor = floor
self.ceil = ceil
self.ln1 = nn.LayerNorm(d)
self.ln2 = nn.LayerNorm(d)
self.proj_k = nn.Linear(d, d, bias=False)
self.proj_v = nn.Linear(d, d, bias=False)
self.proj_r = nn.Linear(d, d, bias=False)
self.proj_out = nn.Linear(d, d, bias=False)
self.proj_out._is_out_proj = True
self.ln_x = nn.GroupNorm(d // head_size, d) # per-head norm on mixer output
self.ffn = nn.Sequential(
nn.Linear(d, ffn_mult * d, bias=False),
nn.GELU(),
nn.Linear(ffn_mult * d, d, bias=False),
)
self.ffn[2]._is_out_proj = True
# Decay: channels span timescales tau in [tau_min, tau_max] (W = 1 - 1/tau).
tau = torch.exp(torch.linspace(math.log(tau_min), math.log(tau_max), d))
W0 = (1.0 - 1.0 / tau).clamp(floor + 1e-3, ceil - 1e-4)
p = (W0 - floor) / (ceil - floor)
self.w = nn.Parameter(torch.log(p / (1.0 - p)))
# Token-shift mix coefficients (RWKV-4 style init).
ratio_1_to_almost0 = 1.0 - (layer_id / n_layers)
ratio_0_to_1 = layer_id / max(1, n_layers - 1)
ramp = torch.arange(d, dtype=torch.float32) / d
self.mu_k = nn.Parameter(ramp ** ratio_1_to_almost0)
self.mu_v = nn.Parameter(ramp ** ratio_1_to_almost0 + 0.3 * ratio_0_to_1)
self.mu_r = nn.Parameter(ramp ** (0.5 * ratio_1_to_almost0))
self.mu_c = nn.Parameter(ramp ** ratio_1_to_almost0)
@property
def W(self):
return self.floor + (self.ceil - self.floor) * torch.sigmoid(self.w)
def forward(self, x, state=None):
B, T, d = x.shape
if state is None:
wkv_s = None
sh_tm = x.new_zeros(B, d)
sh_cm = x.new_zeros(B, d)
else:
wkv_s, sh_tm, sh_cm = state
# ── time mix (WKV) ────────────────────────────────────────────────
h = self.ln1(x)
hp = torch.cat([sh_tm.unsqueeze(1).to(h.dtype), h[:, :-1]], dim=1) # x_{t-1}
xk = h * self.mu_k + hp * (1 - self.mu_k)
xv = h * self.mu_v + hp * (1 - self.mu_v)
xr = h * self.mu_r + hp * (1 - self.mu_r)
k = self.proj_k(xk)
v = self.proj_v(xv)
r = torch.sigmoid(self.proj_r(xr))
W = self.W
gain = torch.sqrt(1.0 - W * W) # unit stationary variance per channel
a = k.float() * v.float() * gain
y = decay_scan(a, W, wkv_s) # (B, T, d) fp32
new_wkv = y[:, -1].detach()
yn = self.ln_x(y.reshape(B * T, d)).reshape(B, T, d)
x = x + self.proj_out(r * yn.to(r.dtype))
# ── channel mix (FFN) ────────────────────────────────────────────
h2 = self.ln2(x)
h2p = torch.cat([sh_cm.unsqueeze(1).to(h2.dtype), h2[:, :-1]], dim=1)
xc = h2 * self.mu_c + h2p * (1 - self.mu_c)
x = x + self.ffn(xc)
new_state = (new_wkv, h[:, -1].detach(), h2[:, -1].detach())
return x, new_state
# ════════════════════════════════════════════════════════════════════════════
# HF wrapper
# ════════════════════════════════════════════════════════════════════════════
class FWKVPreTrainedModel(PreTrainedModel):
config_class = FWKVConfig
base_model_prefix = "model"
supports_gradient_checkpointing = False
def _init_weights(self, module):
if isinstance(module, nn.Linear):
std = 0.02
if getattr(module, "_is_out_proj", False):
std = 0.02 / math.sqrt(2 * self.config.n_layers)
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.LayerNorm, nn.GroupNorm)):
nn.init.ones_(module.weight)
nn.init.zeros_(module.bias)
elif isinstance(module, FactorizedTiedHead):
pass
class FWKVLanguageModel(FWKVPreTrainedModel, GenerationMixin):
# This model does NOT use the standard KV-Cache machinery (DynamicCache/
# StaticCache). Without this, newer transformers versions (4.45+/5.x)
# auto-wrap past_key_values in a Cache object during generate(), which this
# custom recurrent-state architecture can't consume.
_supports_cache_class = False
_supports_static_cache = False
_supports_quantized_cache = False
def __init__(self, config):
super().__init__(config)
self.config = config
self.amp_dtype = None # optionally set by a training/inference driver
self.shared = FactorizedTiedHead(config.vocab_size, config.d_model, config.d_emb)
self.ln0 = nn.LayerNorm(config.d_model)
self.blocks = nn.ModuleList([
FWKVBlock(config.d_model, i, config.n_layers, config.ffn_mult,
config.wkv_floor, config.wkv_ceil, config.head_size,
config.tau_min, config.tau_max)
for i in range(config.n_layers)
])
self.norm = nn.LayerNorm(config.d_model)
self.post_init()
def get_input_embeddings(self):
return self.shared
def set_input_embeddings(self, value):
self.shared = value
def resize_vocab(self, new_vocab_size):
self.shared.resize_vocab(new_vocab_size)
self.config.vocab_size = new_vocab_size
def count_params(self):
return sum(p.numel() for p in self.parameters())
def forward(
self,
input_ids=None,
past_key_values=None,
attention_mask=None,
labels=None,
use_cache=True,
**kwargs
):
if input_ids is None:
raise ValueError("input_ids must be provided")
if not isinstance(input_ids, torch.Tensor):
input_ids = torch.tensor(input_ids)
if input_ids.dtype != torch.long:
input_ids = input_ids.long()
if input_ids.dim() == 1:
input_ids = input_ids.unsqueeze(0)
x = self.ln0(self.shared.embed(input_ids))
# If transformers handed us a Cache object (DynamicCache, etc.) instead
# of our custom list/tuple of per-layer state tensors, treat it as
# "no state yet" rather than trying to iterate/index into it.
if past_key_values is None or isinstance(past_key_values, Cache):
states_in = [None] * len(self.blocks)
else:
states_in = past_key_values
states_out = []
for block, state in zip(self.blocks, states_in):
x, new_state = block(x, state)
states_out.append(new_state)
x = self.norm(x)
x_emb = self.shared.to_emb_space(x)
loss = None
logits = None
if labels is not None:
if not isinstance(labels, torch.Tensor):
labels = torch.tensor(labels)
if labels.dtype != torch.long:
labels = labels.long()
loss = chunked_cross_entropy(
x_emb, self.shared.weight, labels,
chunk_size=self.config.ce_chunk, ignore_index=-100
)
else:
logits = self.shared.logits(x_emb)
past_key_values_out = tuple(states_out) if use_cache else None
return CausalLMOutputWithPast(
loss=loss,
logits=logits,
past_key_values=past_key_values_out,
)
def prepare_inputs_for_generation(self, input_ids, past_key_values=None, **kwargs):
if past_key_values is not None:
input_ids = input_ids[:, -1:] if input_ids.dim() > 1 else input_ids[-1:]
# Don't coerce a Cache object into a tuple β€” pass it through as-is,
# forward() knows how to treat it as "empty" state.
if past_key_values is not None and not isinstance(past_key_values, (tuple, Cache)):
past_key_values = tuple(past_key_values)
return {
"input_ids": input_ids,
"past_key_values": past_key_values,
"use_cache": True,
}
def _reorder_cache(self, past_key_values, beam_idx):
if past_key_values is None or isinstance(past_key_values, Cache):
return past_key_values
reordered = []
for state in past_key_values:
if state is None:
reordered.append(None)
continue
# each per-layer state is itself a 3-tuple (wkv_state, sh_tm, sh_cm)
reordered.append(tuple(s.index_select(0, beam_idx) for s in state))
return tuple(reordered)
# Register the model with AutoModel
AutoConfig.register("fwkv", FWKVConfig)
AutoModel.register(FWKVConfig, FWKVLanguageModel)