bear-240m-cpt / engine /transformer.py
Dummy9898's picture
Release Mesosfer Bear AI Model checkpoint (bear_cpt)
b2140f4 verified
Raw
History Blame Contribute Delete
11.1 kB
"""
Mesosfer Bear AI - Transformer Language Model
Llama-style decoder-only transformer with:
- RMSNorm (pre-norm)
- Rotary Position Embeddings (RoPE)
- Grouped Query Attention (GQA) with Flash Attention
- SwiGLU FFN
- Weight-tied embedding ↔ LM head
Default config targets ~250M params at depth 16.
"""
import math
from dataclasses import dataclass
from typing import Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from engine.flashattion import bear_attention
# -- Model Config -----------------------------------------------------------
@dataclass
class BearConfig:
"""
~250M param config (weight-tied):
embed 60K*1024 = 61M
16 layers * ~11M = 179M
total ≈ 240M
"""
vocab_size: int = 60000
d_model: int = 1024
n_layers: int = 16
n_heads: int = 16 # query heads
n_kv_heads: int = 4 # key/value heads (GQA ratio 4:1)
ffn_hidden: int = 2816 # SwiGLU hidden dim (≈ 4*d_model*2/3 rounded to 256)
max_seq_len: int = 4096
norm_eps: float = 1e-5
rope_theta: float = 10000.0
dropout: float = 0.0 # ponytail: 0 for pre-training, set >0 for fine-tuning
tie_weights: bool = True
@classmethod
def from_dict(cls, d: dict) -> "BearConfig":
import dataclasses
valid_keys = {f.name for f in dataclasses.fields(cls)}
return cls(**{k: v for k, v in d.items() if k in valid_keys})
def to_dict(self) -> dict:
import dataclasses
return dataclasses.asdict(self)
# -- RMSNorm ----------------------------------------------------------------
class RMSNorm(nn.Module):
"""Root Mean Square Layer Normalization (Zhang & Sennrich 2019)."""
def __init__(self, dim: int, eps: float = 1e-5):
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x: torch.Tensor) -> torch.Tensor:
norm = x.float().pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt()
return (x.float() * norm).type_as(x) * self.weight
# -- Rotary Position Embedding (RoPE) --------------------------------------
def precompute_rope_freqs(
head_dim: int,
max_seq_len: int,
theta: float = 10000.0,
device: Optional[torch.device] = None,
) -> torch.Tensor:
"""Precompute complex-valued RoPE frequencies: shape (max_seq_len, head_dim//2)."""
freqs = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim))
positions = torch.arange(max_seq_len, device=device).float()
angles = torch.outer(positions, freqs) # (seq_len, head_dim//2)
return torch.polar(torch.ones_like(angles), angles) # complex64
def apply_rope(
x: torch.Tensor,
freqs: torch.Tensor,
) -> torch.Tensor:
"""
Apply rotary embeddings to x.
x: (batch, n_heads, seq_len, head_dim)
freqs: (seq_len, head_dim//2) complex
"""
# Reshape to pairs of floats -> complex
x_complex = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
freqs = freqs.unsqueeze(0).unsqueeze(0) # (1, 1, seq_len, head_dim//2)
x_rotated = x_complex * freqs
return torch.view_as_real(x_rotated).flatten(-2).type_as(x)
# -- Grouped Query Attention ------------------------------------------------
class GQAttention(nn.Module):
"""
Multi-head attention with Grouped Query Attention (GQA).
Delegates to bear_attention() from engine.flashattion for backend selection.
"""
def __init__(self, config: BearConfig):
super().__init__()
self.n_heads = config.n_heads
self.n_kv_heads = config.n_kv_heads
self.head_dim = config.d_model // config.n_heads
self.n_rep = self.n_heads // self.n_kv_heads
self.dropout_p = config.dropout
self.wq = nn.Linear(config.d_model, config.n_heads * self.head_dim, bias=False)
self.wk = nn.Linear(config.d_model, config.n_kv_heads * self.head_dim, bias=False)
self.wv = nn.Linear(config.d_model, config.n_kv_heads * self.head_dim, bias=False)
self.wo = nn.Linear(config.n_heads * self.head_dim, config.d_model, bias=False)
def forward(
self,
x: torch.Tensor,
rope_freqs: torch.Tensor,
mask: Optional[torch.Tensor] = None,
) -> torch.Tensor:
B, T, _ = x.shape
q = self.wq(x).view(B, T, self.n_heads, self.head_dim)
k = self.wk(x).view(B, T, self.n_kv_heads, self.head_dim)
v = self.wv(x).view(B, T, self.n_kv_heads, self.head_dim)
# Apply RoPE (needs B, H, T, D layout)
q = apply_rope(q.transpose(1, 2), rope_freqs).transpose(1, 2) # -> (B, T, H, D)
k = apply_rope(k.transpose(1, 2), rope_freqs).transpose(1, 2)
drop_p = self.dropout_p if self.training else 0.0
attn_out = bear_attention(q, k, v, n_rep=self.n_rep, dropout_p=drop_p, causal=True, mask=mask)
attn_out = attn_out.contiguous().view(B, T, -1)
return self.wo(attn_out)
# -- SwiGLU Feed-Forward ---------------------------------------------------
class SwiGLU(nn.Module):
"""
SwiGLU FFN (Shazeer 2020): gate(x) * up(x), then down projection.
More parameter-efficient than standard FFN at same quality.
"""
def __init__(self, config: BearConfig):
super().__init__()
self.w_gate = nn.Linear(config.d_model, config.ffn_hidden, bias=False)
self.w_up = nn.Linear(config.d_model, config.ffn_hidden, bias=False)
self.w_down = nn.Linear(config.ffn_hidden, config.d_model, bias=False)
self.dropout = nn.Dropout(config.dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.dropout(self.w_down(F.silu(self.w_gate(x)) * self.w_up(x)))
# -- Transformer Block -----------------------------------------------------
class TransformerBlock(nn.Module):
"""Pre-norm transformer block: RMSNorm → Attn → residual → RMSNorm → FFN → residual."""
def __init__(self, config: BearConfig):
super().__init__()
self.attn_norm = RMSNorm(config.d_model, config.norm_eps)
self.attn = GQAttention(config)
self.ffn_norm = RMSNorm(config.d_model, config.norm_eps)
self.ffn = SwiGLU(config)
def forward(
self,
x: torch.Tensor,
rope_freqs: torch.Tensor,
mask: Optional[torch.Tensor] = None,
) -> torch.Tensor:
x = x + self.attn(self.attn_norm(x), rope_freqs, mask)
x = x + self.ffn(self.ffn_norm(x))
return x
# -- Full Model -------------------------------------------------------------
class BearTransformer(nn.Module):
"""
Mesosfer Bear AI decoder-only transformer.
Architecture: Llama-style with RoPE + GQA + SwiGLU + RMSNorm.
Outputs raw logits (no softmax) for use with CrossEntropyLoss.
"""
def __init__(self, config: BearConfig):
super().__init__()
self.config = config
self.tok_emb = nn.Embedding(config.vocab_size, config.d_model)
self.drop = nn.Dropout(config.dropout)
self.layers = nn.ModuleList([
TransformerBlock(config) for _ in range(config.n_layers)
])
self.norm = RMSNorm(config.d_model, 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.tok_emb.weight
# Precompute RoPE frequencies (registered as buffer, moves with .to(device))
head_dim = config.d_model // config.n_heads
rope_freqs = precompute_rope_freqs(head_dim, config.max_seq_len, config.rope_theta)
self.register_buffer("rope_freqs", rope_freqs, persistent=False)
# Init weights
self._init_weights()
def _init_weights(self):
"""Small normal init + scaled residual init for deep networks."""
std = 0.02
for module in self.modules():
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)
# Scale down residual projections by 1/sqrt(2*n_layers) for training stability
# ponytail: only apply to output projections (wo, w_down)
residual_std = std / math.sqrt(2 * self.config.n_layers)
for layer in self.layers:
nn.init.normal_(layer.attn.wo.weight, mean=0.0, std=residual_std)
nn.init.normal_(layer.ffn.w_down.weight, mean=0.0, std=residual_std)
def forward(
self,
input_ids: torch.Tensor,
labels: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
"""
Args:
input_ids: (batch, seq_len) token IDs
labels: (batch, seq_len) target IDs for loss computation (optional)
Returns:
(logits, loss) — loss is None if labels not provided.
"""
B, T = input_ids.shape
assert T <= self.config.max_seq_len, f"Sequence length {T} exceeds max {self.config.max_seq_len}"
x = self.drop(self.tok_emb(input_ids))
rope_freqs = self.rope_freqs[:T]
for layer in self.layers:
x = layer(x, rope_freqs)
x = self.norm(x)
logits = self.lm_head(x)
loss = None
if labels is not None:
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), labels.view(-1))
return logits, loss
def param_count(self) -> int:
"""Total trainable parameters (excluding tied duplicates)."""
seen = set()
total = 0
for p in self.parameters():
if p.data_ptr() not in seen:
seen.add(p.data_ptr())
total += p.numel()
return total
@torch.no_grad()
def generate(
self,
input_ids: torch.Tensor,
max_new_tokens: int = 256,
temperature: float = 0.8,
top_k: int = 50,
) -> torch.Tensor:
"""
Simple autoregressive generation (no KV cache — good enough for <4K tokens).
ponytail: KV cache adds ~150 lines of complexity. Add when inference speed matters.
"""
for _ in range(max_new_tokens):
# Crop to max context if needed
idx_cond = input_ids if input_ids.size(1) <= self.config.max_seq_len else input_ids[:, -self.config.max_seq_len:]
logits, _ = self(idx_cond)
logits = logits[:, -1, :] # last position
if temperature > 0:
logits = logits / temperature
if top_k > 0:
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float("-inf")
probs = F.softmax(logits, dim=-1)
next_id = torch.multinomial(probs, num_samples=1)
else:
next_id = logits.argmax(dim=-1, keepdim=True)
input_ids = torch.cat([input_ids, next_id], dim=1)
return input_ids