ThoxMicroprocessor-8M-Base-TinyStories / modeling_thox_microprocessor.py
tommytracx's picture
Upload folder using huggingface_hub
7496ed2 verified
Raw History Blame Contribute Delete
15.3 kB
"""Hugging Face remote-code implementation of THOX Microprocessor.
This file is uploaded verbatim to the model repository, so it must be
self-contained: it may not import the ``thox_microprocessor`` package. That
duplication is deliberate but dangerous — v0.1 shipped a copy whose submodules
were named ``n1``/``post``/``n2`` while the training package used
``attn_norm``/``post_attn_norm``/``mixer_norm``, so a published checkpoint would
have loaded with three randomly-initialised norms per layer and no error.
``tests/test_hub_parity.py`` now asserts strict state-dict compatibility and
logit equality between the two implementations.
Scope note: this reference path recomputes the full prefix for each decoding
step rather than maintaining a KV cache. It is portable across transformers
versions, which matters more for remote code than decode throughput; the
``thox_microprocessor`` package implements cached decoding for training and
deployment.
"""
from __future__ import annotations
import math
from typing import List, Optional, Tuple, Union
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import PreTrainedModel
from transformers.modeling_outputs import CausalLMOutputWithPast
try: # transformers >= 4.50 stopped mixing GenerationMixin into PreTrainedModel
from transformers import GenerationMixin
except ImportError: # pragma: no cover - older transformers
from transformers.generation import GenerationMixin
from .configuration_thox_microprocessor import ThoxMicroprocessorConfig
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6) -> None:
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x: torch.Tensor) -> torch.Tensor:
variance = x.float().pow(2).mean(dim=-1, keepdim=True)
return x * torch.rsqrt(variance + self.eps).to(dtype=x.dtype) * self.weight
class QKRMSNorm(RMSNorm):
"""Per-head Q/K RMS normalization applied before RoPE."""
def _rope_frequencies(seq_len: int, head_dim: int, theta: float, device) -> Tuple[torch.Tensor, torch.Tensor]:
inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim))
positions = torch.arange(seq_len, device=device, dtype=torch.float32)
angles = torch.outer(positions, inv_freq)
return angles.cos(), angles.sin()
def _apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
even = x[..., 0::2]
odd = x[..., 1::2]
cos = cos[None, None, :, :].to(dtype=x.dtype)
sin = sin[None, None, :, :].to(dtype=x.dtype)
return torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2)
def _fwht_block(x: torch.Tensor) -> torch.Tensor:
n = x.shape[-1]
if n <= 0 or n & (n - 1):
raise ValueError(f"FWHT input width must be a power of two, got {n}")
y = x
h = 1
while h < n:
shape = y.shape
y = y.reshape(*shape[:-1], -1, 2, h)
a = y[..., 0, :]
b = y[..., 1, :]
y = torch.cat((a + b, a - b), dim=-1).reshape(*shape)
h *= 2
return y * (1.0 / math.sqrt(n))
def _stride_shuffle(x: torch.Tensor, block_size: int) -> torch.Tensor:
nblocks = x.shape[-1] // block_size
return x.reshape(*x.shape[:-1], block_size, nblocks).transpose(-1, -2).reshape(x.shape)
def _stride_unshuffle(x: torch.Tensor, block_size: int) -> torch.Tensor:
nblocks = x.shape[-1] // block_size
return x.reshape(*x.shape[:-1], nblocks, block_size).transpose(-1, -2).reshape(x.shape)
class BlockHadamardMixer(nn.Module):
def __init__(self, dim: int, block_size: int = 64, center: bool = True, shuffle: bool = False) -> None:
super().__init__()
if dim % block_size != 0:
raise ValueError(f"dim ({dim}) must be divisible by block_size ({block_size})")
self.dim = dim
self.block_size = block_size
self.num_blocks = dim // block_size
self.center = center
self.shuffle = shuffle and self.num_blocks > 1
self.d1 = nn.Parameter(torch.ones(dim))
self.d2 = nn.Parameter(torch.ones(dim))
self.d3 = nn.Parameter(torch.full((dim,), 0.02))
def forward(self, x: torch.Tensor) -> torch.Tensor:
shape = x.shape
y = x * self.d1
if self.shuffle:
y = _stride_shuffle(y, self.block_size)
y = y.reshape(*shape[:-1], self.num_blocks, self.block_size)
y = _fwht_block(y)
y = F.silu(y * self.d2.reshape(self.num_blocks, self.block_size))
if self.center:
y = y - y.mean(dim=-1, keepdim=True)
y = _fwht_block(y).reshape(shape)
if self.shuffle:
y = _stride_unshuffle(y, self.block_size)
return y * self.d3
class GroupedQueryAttention(nn.Module):
def __init__(self, config: ThoxMicroprocessorConfig) -> None:
super().__init__()
self.config = config
self.head_dim = config.d_model // config.num_heads
kv_dim = config.num_kv_heads * self.head_dim
self.q_proj = nn.Linear(config.d_model, config.d_model, bias=False)
self.k_proj = nn.Linear(config.d_model, kv_dim, bias=False)
self.v_proj = nn.Linear(config.d_model, kv_dim, bias=False)
self.out_proj = nn.Linear(config.d_model, config.d_model, bias=False)
self.head_gate = nn.Linear(config.d_model, config.num_heads, bias=False)
self.q_norm = QKRMSNorm(self.head_dim, config.rms_eps) if config.use_qk_norm else nn.Identity()
self.k_norm = QKRMSNorm(self.head_dim, config.rms_eps) if config.use_qk_norm else nn.Identity()
def forward(
self,
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
attn_bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
cfg = self.config
batch, seq_len, _ = x.shape
q = self.q_proj(x).view(batch, seq_len, cfg.num_heads, self.head_dim).transpose(1, 2)
k = self.k_proj(x).view(batch, seq_len, cfg.num_kv_heads, self.head_dim).transpose(1, 2)
v = self.v_proj(x).view(batch, seq_len, cfg.num_kv_heads, self.head_dim).transpose(1, 2)
q = _apply_rope(self.q_norm(q), cos, sin)
k = _apply_rope(self.k_norm(k), cos, sin)
repeats = cfg.num_heads // cfg.num_kv_heads
if repeats > 1:
k = k.repeat_interleave(repeats, dim=1)
v = v.repeat_interleave(repeats, dim=1)
scores = torch.matmul(q, k.transpose(-2, -1)) * (1.0 / math.sqrt(self.head_dim))
if attn_bias is not None:
scores = scores + attn_bias
probs = F.softmax(scores.float(), dim=-1).to(dtype=q.dtype)
probs = F.dropout(probs, p=cfg.attention_dropout, training=self.training)
context = torch.matmul(probs, v)
gate = torch.sigmoid(self.head_gate(x)).transpose(1, 2).unsqueeze(-1)
context = (context * gate).transpose(1, 2).contiguous().view(batch, seq_len, cfg.d_model)
return self.out_proj(context)
class ThoxMicroBlock(nn.Module):
def __init__(self, config: ThoxMicroprocessorConfig, layer_index: int = 0) -> None:
super().__init__()
self.config = config
self.attn_norm = RMSNorm(config.d_model, config.rms_eps)
self.attn = GroupedQueryAttention(config)
self.post_attn_norm = (
RMSNorm(config.d_model, config.rms_eps) if config.use_sandwich_norm else nn.Identity()
)
if config.use_hadamard_mixer:
self.mixer_norm = RMSNorm(config.d_model, config.rms_eps)
self.mixer = BlockHadamardMixer(
config.d_model,
config.hadamard_block_size,
center=config.mixer_center,
shuffle=config.mixer_block_shuffle and (layer_index % 2 == 1),
)
else:
self.mixer_norm = None
self.mixer = None
if config.use_residual_gate:
self.attn_gate = nn.Parameter(torch.tensor(0.0))
self.mixer_gate = nn.Parameter(torch.tensor(0.0))
else:
self.register_parameter("attn_gate", None)
self.register_parameter("mixer_gate", None)
def forward(
self,
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
attn_bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
update = self.post_attn_norm(self.attn(self.attn_norm(x), cos, sin, attn_bias))
if self.attn_gate is not None:
update = torch.sigmoid(self.attn_gate) * update
x = x + update
if self.mixer is not None:
update = self.mixer(self.mixer_norm(x))
if self.mixer_gate is not None:
update = torch.sigmoid(self.mixer_gate) * update
x = x + update
return x
class ThoxMicroprocessorPreTrainedModel(PreTrainedModel):
config_class = ThoxMicroprocessorConfig
base_model_prefix = "thox_microprocessor"
supports_gradient_checkpointing = False
_supports_cache_class = False
_supports_sdpa = False
_supports_flash_attn_2 = False
# With tied embeddings there is no second tensor to tie, so nothing is
# shared and this stays empty. An empty dict is iterated identically by the
# transformers 4.x (list) and 5.x (mapping) code paths.
_tied_weights_keys: dict = {}
def _init_weights(self, module: nn.Module) -> None:
std = self.config.initializer_range
if isinstance(module, (nn.Linear, nn.Embedding)):
nn.init.normal_(module.weight, mean=0.0, std=std)
if isinstance(module, nn.Linear) and module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, RMSNorm):
nn.init.ones_(module.weight)
elif isinstance(module, BlockHadamardMixer):
nn.init.ones_(module.d1)
nn.init.ones_(module.d2)
nn.init.constant_(module.d3, 0.02)
class ThoxMicroprocessorForCausalLM(ThoxMicroprocessorPreTrainedModel, GenerationMixin):
def __init__(self, config: ThoxMicroprocessorConfig) -> None:
super().__init__(config)
self.embed_tokens = nn.Embedding(config.vocab_size, config.d_model)
self.embed_dropout = nn.Dropout(config.embedding_dropout)
self.layers = nn.ModuleList(
[ThoxMicroBlock(config, layer_index=i) for i in range(config.num_layers)]
)
self.final_norm = RMSNorm(config.d_model, config.rms_eps)
self.lm_head = (
None
if config.tie_word_embeddings
else nn.Linear(config.d_model, config.vocab_size, bias=False)
)
self.post_init()
# Depth-scaled residual init, applied after post_init so it is not
# overwritten. This must match the training package exactly.
residual_std = config.initializer_range / math.sqrt(2 * config.num_layers)
for layer in self.layers:
nn.init.normal_(layer.attn.out_proj.weight, mean=0.0, std=residual_std)
def get_input_embeddings(self) -> nn.Module:
return self.embed_tokens
def set_input_embeddings(self, value: nn.Module) -> None:
self.embed_tokens = value
def get_output_embeddings(self):
return self.lm_head
def set_output_embeddings(self, value) -> None:
self.lm_head = value
def _output_weight(self) -> torch.Tensor:
return self.embed_tokens.weight if self.lm_head is None else self.lm_head.weight
def _attention_bias(self, batch, seq_len, attention_mask, dtype, device) -> torch.Tensor:
neg = torch.finfo(dtype).min
causal = torch.ones(seq_len, seq_len, dtype=torch.bool, device=device).tril()
bias = torch.where(
causal,
torch.zeros((), dtype=dtype, device=device),
torch.full((), neg, dtype=dtype, device=device),
).expand(batch, 1, seq_len, seq_len)
if attention_mask is not None:
if attention_mask.ndim != 2 or attention_mask.shape != (batch, seq_len):
raise ValueError(
f"attention_mask must have shape [{batch}, {seq_len}], "
f"got {tuple(attention_mask.shape)}"
)
pad = torch.where(
attention_mask.to(torch.bool),
torch.zeros((), dtype=dtype, device=device),
torch.full((), neg, dtype=dtype, device=device),
)
bias = bias + pad[:, None, None, :]
return bias
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
labels: Optional[torch.LongTensor] = None,
past_key_values: Optional[List[torch.FloatTensor]] = None,
use_cache: Optional[bool] = None,
return_dict: Optional[bool] = None,
**kwargs,
) -> Union[Tuple, CausalLMOutputWithPast]:
if (input_ids is None) == (inputs_embeds is None):
raise ValueError("provide exactly one of input_ids or inputs_embeds")
return_dict = True if return_dict is None else return_dict
if inputs_embeds is None:
if input_ids.ndim != 2:
raise ValueError(
f"input_ids must have shape [batch, seq_len], got {tuple(input_ids.shape)}"
)
inputs_embeds = self.embed_tokens(input_ids)
batch, seq_len, _ = inputs_embeds.shape
if seq_len > self.config.max_seq_len:
raise ValueError(
f"sequence length {seq_len} exceeds max_seq_len={self.config.max_seq_len}"
)
x = self.embed_dropout(inputs_embeds * math.sqrt(self.config.d_model))
device = x.device
cos, sin = _rope_frequencies(seq_len, self.config.head_dim, self.config.rope_theta, device)
attn_bias = self._attention_bias(batch, seq_len, attention_mask, x.dtype, device)
for layer in self.layers:
x = layer(x, cos, sin, attn_bias)
x = self.final_norm(x)
logits = F.linear(x, self._output_weight())
loss = None
if labels is not None:
if labels.shape[:2] != (batch, seq_len):
raise ValueError("labels must have shape [batch, seq_len]")
loss = F.cross_entropy(
logits[:, :-1].contiguous().view(-1, self.config.vocab_size).float(),
labels[:, 1:].contiguous().view(-1),
ignore_index=-100,
)
if not return_dict:
return (loss, logits) if loss is not None else (logits,)
return CausalLMOutputWithPast(loss=loss, logits=logits, past_key_values=None)
def prepare_inputs_for_generation(self, input_ids, attention_mask=None, **kwargs):
# No KV cache: always re-encode the (truncated) prefix.
window = self.config.max_seq_len
input_ids = input_ids[:, -window:]
if attention_mask is not None:
attention_mask = attention_mask[:, -window:]
return {"input_ids": input_ids, "attention_mask": attention_mask, "use_cache": False}
__all__ = [
"ThoxMicroprocessorConfig",
"ThoxMicroprocessorPreTrainedModel",
"ThoxMicroprocessorForCausalLM",
]