Can111's picture
Final checkpoints of Table 2
689a869 verified
Raw History Blame Contribute Delete
11.9 kB
"""Reference implementation of the dense decoder-only transformer behind the Standalone and Sampled standalone
models of "ShellElastic: Scalable Elastic Pretraining with Simultaneous Maximal Updates and Hyperparameter
Transfer".
Each released model is an ordinary dense decoder. The released weights already include the constant forward
multipliers of the model (for these models, the FFN output multiplier 1/e, where e is the FFN expansion), so this
file contains no scaling constants.
Architecture (one layer, pre-norm):
h = x + Attention(RMSNorm(x))
out = h + SwiGLU(RMSNorm(h))
* RMSNorm with eps 1e-5; this file computes the statistics in float32 and casts the result back to the input dtype
before the learned gain is applied.
* Attention: separate query/key/value projections without biases; queries and keys are RMS-normalized within
each head (the gains are stored at full width, one per channel); rotary position embeddings in the
rotate-half form with base 10,000; causal softmax attention with scale 1/sqrt(head_dim); the attention output
is multiplied elementwise by sigmoid(x W_gate^T) before the output projection.
* SwiGLU feed-forward network: down_proj(silu(gate_proj(x)) * up_proj(x)).
* Untied input embedding and output head, no biases anywhere, context length 2,048, and the 50,432-token GPT-NeoX
vocabulary (Table 8 of the paper; token ids of the GPT-NeoX-20B tokenizer, Section 4.1).
Rotary tables: ``rope_precision`` is ``"bfloat16"`` (the default) or ``"float32"``. ``"bfloat16"`` rounds the
positions and inverse frequencies to bfloat16, their product to bfloat16, and cos/sin of those angles to bfloat16,
in every compute precision; ``"float32"`` computes the tables in float32.
Dependencies: torch, and safetensors for loading the weights.
Example:
import torch
from modeling_dense import DenseDecoder
model = DenseDecoder.from_pretrained("path/to/model_dir", device="cuda", dtype=torch.bfloat16)
input_ids = torch.tensor([[...]], device="cuda") # GPT-NeoX-20B token ids
logits = model(input_ids) # [batch, tokens, 50432], float32
logits, loss = model(input_ids[:, :-1], labels=input_ids[:, 1:])
"""
from __future__ import annotations
import json
import os
from dataclasses import asdict, dataclass, fields
import torch
import torch.nn.functional as F
from torch import nn
__all__ = ["DenseConfig", "DenseDecoder"]
@dataclass
class DenseConfig:
vocab_size: int = 50432
hidden_size: int = 384
intermediate_size: int = 1536
num_hidden_layers: int = 32
num_attention_heads: int = 6
head_dim: int = 64
max_position_embeddings: int = 2048
rope_theta: float = 10000.0
rope_precision: str = "bfloat16"
rms_norm_eps: float = 1e-5
@classmethod
def from_dict(cls, values: dict) -> "DenseConfig":
"""Build a config from a dict, ignoring keys that only describe the release (method, tier, ...)."""
known = {f.name for f in fields(cls)}
return cls(**{k: v for k, v in values.items() if k in known})
@classmethod
def from_json_file(cls, path: str) -> "DenseConfig":
with open(path) as f:
return cls.from_dict(json.load(f))
def to_dict(self) -> dict:
return asdict(self)
class RMSNorm(nn.Module):
"""RMSNorm over the last dimension, or within consecutive groups of ``group_size`` channels.
The normalization runs in float32; the result is cast back to the input dtype and then multiplied by the
per-channel gain (one gain per channel, also in grouped mode).
"""
def __init__(self, dim: int, eps: float, group_size: int | None = None) -> None:
super().__init__()
self.eps = eps
self.group_size = group_size
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x: torch.Tensor) -> torch.Tensor:
h = x.float()
if self.group_size is not None:
h = h.unflatten(-1, (-1, self.group_size))
h = h * torch.rsqrt(h.pow(2).mean(-1, keepdim=True) + self.eps)
if self.group_size is not None:
h = h.flatten(-2)
return h.to(x.dtype) * self.weight
def rotary_cos_sin(config: DenseConfig, num_positions: int, device: torch.device) -> tuple[torch.Tensor, torch.Tensor]:
"""Rotary tables of shape [num_positions, head_dim] in float32 (rotate-half layout)."""
dim = config.head_dim
frequencies = 1.0 / (config.rope_theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim)) # on CPU
frequencies = frequencies.to(device)
positions = torch.arange(num_positions, device=device, dtype=torch.float32)
if config.rope_precision == "bfloat16":
# Every step rounded to bfloat16 (the product of two bfloat16 numbers is exact in float32, so rounding it
# once gives the bfloat16 product).
def bf16(t: torch.Tensor) -> torch.Tensor:
return t.to(torch.bfloat16).float()
angles = bf16(torch.outer(bf16(positions), bf16(frequencies)))
angles = torch.cat((angles, angles), dim=-1)
return bf16(angles.cos()), bf16(angles.sin())
if config.rope_precision != "float32":
raise ValueError(f"rope_precision must be 'bfloat16' or 'float32', got {config.rope_precision!r}")
angles = torch.outer(positions, frequencies)
angles = torch.cat((angles, angles), dim=-1)
return angles.cos(), angles.sin()
def rotate_half(x: torch.Tensor) -> torch.Tensor:
"""(a, b) -> (-b, a) over the two halves of the last dimension."""
half = x.shape[-1] // 2
return torch.cat((-x[..., half:], x[..., :half]), dim=-1)
def apply_rotary(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
"""x: [batch, tokens, heads, head_dim]; cos, sin: [tokens, head_dim], already in x's dtype."""
cos = cos[None, :, None, :]
sin = sin[None, :, None, :]
return x * cos + rotate_half(x) * sin
class Attention(nn.Module):
def __init__(self, config: DenseConfig) -> None:
super().__init__()
self.num_heads = config.num_attention_heads
self.head_dim = config.head_dim
width = config.num_attention_heads * config.head_dim
self.q_proj = nn.Linear(config.hidden_size, width, bias=False)
self.k_proj = nn.Linear(config.hidden_size, width, bias=False)
self.v_proj = nn.Linear(config.hidden_size, width, bias=False)
self.gate_proj = nn.Linear(config.hidden_size, width, bias=False) # sigmoid output gate
self.o_proj = nn.Linear(width, config.hidden_size, bias=False)
self.q_norm = RMSNorm(width, config.rms_norm_eps, group_size=config.head_dim)
self.k_norm = RMSNorm(width, config.rms_norm_eps, group_size=config.head_dim)
def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
batch, length, _ = x.shape
heads = (batch, length, self.num_heads, self.head_dim)
q = self.q_norm(self.q_proj(x)).view(heads)
k = self.k_norm(self.k_proj(x)).view(heads)
v = self.v_proj(x).view(heads)
q = apply_rotary(q, cos, sin)
k = apply_rotary(k, cos, sin)
out = F.scaled_dot_product_attention(
q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), is_causal=True, scale=self.head_dim**-0.5
)
out = out.transpose(1, 2).reshape(batch, length, self.num_heads * self.head_dim)
out = out * torch.sigmoid(self.gate_proj(x))
return self.o_proj(out)
class MLP(nn.Module):
def __init__(self, config: DenseConfig) -> None:
super().__init__()
self.gate_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
self.up_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
class DecoderLayer(nn.Module):
def __init__(self, config: DenseConfig) -> None:
super().__init__()
self.attn_norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
self.attn = Attention(config)
self.mlp_norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
self.mlp = MLP(config)
def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
h = x + self.attn(self.attn_norm(x), cos, sin)
return h + self.mlp(self.mlp_norm(h))
class DenseDecoder(nn.Module):
"""Decoder-only language model. ``forward`` returns float32 logits (and the mean cross-entropy loss when
``labels`` is given; label -100 is ignored)."""
def __init__(self, config: DenseConfig) -> None:
super().__init__()
self.config = config
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
self.layers = nn.ModuleList(DecoderLayer(config) for _ in range(config.num_hidden_layers))
self.norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
def hidden_states(self, input_ids: torch.Tensor) -> torch.Tensor:
"""Final hidden states after the last norm, [batch, tokens, hidden_size], in the model's dtype."""
length = input_ids.shape[1]
if length > self.config.max_position_embeddings:
raise ValueError(f"{length} tokens exceed the context of {self.config.max_position_embeddings}")
x = self.embed_tokens(input_ids)
cos, sin = rotary_cos_sin(self.config, length, input_ids.device)
cos, sin = cos.to(x.dtype), sin.to(x.dtype)
for layer in self.layers:
x = layer(x, cos, sin)
return self.norm(x)
def forward(self, input_ids: torch.Tensor, labels: torch.Tensor | None = None):
logits = self.lm_head(self.hidden_states(input_ids)).float()
if labels is None:
return logits
loss = F.cross_entropy(logits.flatten(0, 1), labels.flatten(), ignore_index=-100)
return logits, loss
@torch.no_grad()
def generate(self, input_ids: torch.Tensor, max_new_tokens: int, temperature: float = 0.0) -> torch.Tensor:
"""Simple sampling without a key/value cache (greedy when temperature is 0)."""
for _ in range(max_new_tokens):
context = input_ids[:, -self.config.max_position_embeddings:]
next_logits = self(context)[:, -1]
if temperature > 0:
probs = torch.softmax(next_logits / temperature, dim=-1)
next_ids = torch.multinomial(probs, num_samples=1)
else:
next_ids = next_logits.argmax(dim=-1, keepdim=True)
input_ids = torch.cat([input_ids, next_ids], dim=1)
return input_ids
@classmethod
def from_pretrained(cls, directory: str, device="cpu", dtype: torch.dtype = torch.float32) -> "DenseDecoder":
"""Load ``config.json`` and ``model.safetensors`` from a released model directory.
The weights are read and cast to ``dtype`` on the CPU and then moved to ``device``, so the device holds only
one copy of the model, in ``dtype``."""
from safetensors.torch import load_file
config = DenseConfig.from_json_file(os.path.join(directory, "config.json"))
state = load_file(os.path.join(directory, "model.safetensors"), device="cpu")
if dtype is not None:
for name in list(state): # cast on the CPU, one tensor at a time
if state[name].is_floating_point():
state[name] = state[name].to(dtype)
with torch.device("meta"):
model = cls(config)
model.load_state_dict(state, strict=True, assign=True)
return model.to(device).eval()