Aurora-Proelia-ChatML / aurora_model.py
arthu1's picture
Fix normal Transformers inference loading
01cfd4f verified
Raw
History Blame Contribute Delete
17.7 kB
from __future__ import annotations
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from .aurora_config import AuroraConfig
try:
from cut_cross_entropy import linear_cross_entropy
except ImportError:
linear_cross_entropy = None
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float) -> None:
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x: torch.Tensor) -> torch.Tensor:
scale = torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
return self.weight * x * scale
def precompute_rope_frequencies(
seq_len: int, head_dim: int, theta: float, device: torch.device, dtype: torch.dtype
) -> tuple[torch.Tensor, torch.Tensor]:
inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim))
positions = torch.arange(seq_len, device=device).float()
freqs = torch.outer(positions, inv_freq)
return freqs.cos().to(dtype=dtype), freqs.sin().to(dtype=dtype)
def apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
cos = cos[None, :, None, :]
sin = sin[None, :, None, :]
x_even = x[..., 0::2]
x_odd = x[..., 1::2]
out = torch.empty_like(x)
out[..., 0::2] = x_even * cos - x_odd * sin
out[..., 1::2] = x_even * sin + x_odd * cos
return out
class CausalSelfAttention(nn.Module):
def __init__(self, cfg: AuroraConfig) -> None:
super().__init__()
self.cfg = cfg
self.num_heads = cfg.num_attention_heads
self.num_kv_heads = cfg.num_key_value_heads
self.head_dim = cfg.head_dim
self.kv_repeat = self.num_heads // self.num_kv_heads
self.q_proj = nn.Linear(cfg.hidden_size, cfg.num_attention_heads * self.head_dim, bias=cfg.attention_bias)
self.k_proj = nn.Linear(cfg.hidden_size, cfg.num_key_value_heads * self.head_dim, bias=cfg.attention_bias)
self.v_proj = nn.Linear(cfg.hidden_size, cfg.num_key_value_heads * self.head_dim, bias=cfg.attention_bias)
self.o_proj = nn.Linear(cfg.hidden_size, cfg.hidden_size, bias=cfg.attention_bias)
self.q_norm = RMSNorm(self.head_dim, cfg.rms_norm_eps) if cfg.qk_norm else nn.Identity()
self.k_norm = RMSNorm(self.head_dim, cfg.rms_norm_eps) if cfg.qk_norm else nn.Identity()
self.dropout_p = cfg.dropout
def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
batch, seq_len, _ = x.shape
q = self.q_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
k = self.k_proj(x).view(batch, seq_len, self.num_kv_heads, self.head_dim)
v = self.v_proj(x).view(batch, seq_len, self.num_kv_heads, self.head_dim)
q = self.q_norm(q)
k = self.k_norm(k)
q = apply_rope(q, cos, sin).transpose(1, 2)
k = apply_rope(k, cos, sin).transpose(1, 2)
v = v.transpose(1, 2)
# Explicit K/V expansion is mathematically equivalent to GQA and works
# across CUDA, Apple MPS, and CPU PyTorch backends.
if self.kv_repeat > 1:
k = k.repeat_interleave(self.kv_repeat, dim=1)
v = v.repeat_interleave(self.kv_repeat, dim=1)
y = F.scaled_dot_product_attention(
q,
k,
v,
attn_mask=None,
dropout_p=self.dropout_p if self.training else 0.0,
is_causal=True,
)
y = y.transpose(1, 2).contiguous().view(batch, seq_len, self.cfg.hidden_size)
return self.o_proj(y)
class SwiGLU(nn.Module):
def __init__(self, cfg: AuroraConfig, intermediate_size: int | None = None) -> None:
super().__init__()
intermediate_size = intermediate_size or cfg.intermediate_size
self.gate_proj = nn.Linear(cfg.hidden_size, intermediate_size, bias=cfg.mlp_bias)
self.up_proj = nn.Linear(cfg.hidden_size, intermediate_size, bias=cfg.mlp_bias)
self.down_proj = nn.Linear(intermediate_size, cfg.hidden_size, bias=cfg.mlp_bias)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
class Top1MoE(nn.Module):
"""Top-1 routed SwiGLU experts with Switch-style router regularization."""
def __init__(self, cfg: AuroraConfig) -> None:
super().__init__()
self.num_experts = cfg.num_experts
self.router_aux_loss_coef = cfg.router_aux_loss_coef
self.router_z_loss_coef = cfg.router_z_loss_coef
self.router_noise_scale = cfg.router_noise_scale
self.capacity_factor = cfg.moe_capacity_factor
self.use_gate_weight = cfg.router_use_gate_weight
# Keep routing in BF16/FP32 rather than quantizing its logits to FP8.
self.router = nn.Linear(cfg.hidden_size, cfg.num_experts, bias=False)
self.experts = nn.ModuleList([SwiGLU(cfg) for _ in range(cfg.num_experts)])
# Detached summaries from the latest batch, for collapse detection in
# the trainer. They are intentionally not persistent model state.
self.last_expert_fraction: torch.Tensor | None = None
self.last_preferred_expert_fraction: torch.Tensor | None = None
self.last_forced_fraction: torch.Tensor | None = None
self.last_selected_gate_probability: torch.Tensor | None = None
def _capacity_constrained_route(
self, scores: torch.Tensor, preferred_index: torch.Tensor
) -> torch.Tensor:
"""Assign exactly one expert/token while bounding every expert load.
Experts keep their highest-scoring first-choice tokens. Overflow is
deterministically retried against each token's next preference. With
five experts this small eager-only matching pass is far cheaper than
an expert MLP and prevents a collapsed router from starving experts.
"""
token_count = scores.size(0)
capacity = max(
math.ceil(token_count / self.num_experts),
math.ceil(token_count * self.capacity_factor / self.num_experts),
)
rankings = torch.argsort(scores, dim=-1, descending=True)
assigned = torch.full_like(preferred_index, -1)
remaining = [capacity for _ in range(self.num_experts)]
for rank in range(self.num_experts):
for expert_index in range(self.num_experts):
slots = remaining[expert_index]
if slots <= 0:
continue
candidates = torch.nonzero(
(assigned < 0) & (rankings[:, rank] == expert_index), as_tuple=False
).flatten()
candidate_count = candidates.numel()
if candidate_count == 0:
continue
if candidate_count > slots:
candidate_scores = scores.index_select(0, candidates)[:, expert_index]
best_positions = torch.topk(candidate_scores, k=slots, sorted=False).indices
candidates = candidates.index_select(0, best_positions)
candidate_count = slots
assigned.index_fill_(0, candidates, expert_index)
remaining[expert_index] -= candidate_count
# The combined capacity is at least the token count and every token
# ranks every expert, so this is a logic invariant rather than an
# expected fallback.
if bool(torch.any(assigned < 0)):
raise RuntimeError("capacity-constrained MoE routing left tokens unassigned")
return assigned
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
original_shape = x.shape
flat_x = x.reshape(-1, original_shape[-1])
router_logits = self.router(flat_x).float()
router_probs = torch.softmax(router_logits, dim=-1)
# Routing indices are discrete. Keep this bookkeeping out of the
# autograd graph; language gradients still reach the selected gate
# probability when gate weighting is enabled, while router losses use
# the clean differentiable probabilities below.
with torch.no_grad():
routing_logits = router_logits
if self.training and self.router_noise_scale > 0:
# Noisy top-1 routing keeps early training exploratory. Only
# the discrete expert choice is noisy; probability weights and
# regularization remain based on clean BF16/FP32 logits.
gumbel_noise = -torch.empty_like(router_logits).exponential_().log()
routing_logits = router_logits + self.router_noise_scale * gumbel_noise
preferred_index = torch.argmax(routing_logits, dim=-1)
route_index = (
self._capacity_constrained_route(routing_logits, preferred_index)
if self.capacity_factor
else preferred_index
)
selected_router_probability = router_probs.gather(1, route_index.unsqueeze(1)).squeeze(1)
route_weight = (
selected_router_probability
if self.use_gate_weight
else torch.ones_like(selected_router_probability)
)
output = torch.zeros_like(flat_x)
for expert_index, expert in enumerate(self.experts):
token_indices = torch.nonzero(route_index == expert_index, as_tuple=False).flatten()
if token_indices.numel() == 0:
continue
expert_input = flat_x.index_select(0, token_indices)
real_token_count = expert_input.size(0)
# TorchAO's FP8 GEMMs require their M dimension to be divisible
# by 16. Sparse routing gives every expert a variable number of
# tokens, so pad only this temporary dispatch buffer and discard
# the corresponding outputs. This changes no real-token math.
fp8_padding = (-real_token_count) % 16
if fp8_padding:
expert_input = torch.cat(
(expert_input, expert_input.new_zeros((fp8_padding, expert_input.size(-1)))), dim=0
)
expert_output = expert(expert_input)[:real_token_count]
routed_output = expert_output * route_weight.index_select(0, token_indices).to(expert_output.dtype).unsqueeze(-1)
# RMSNorm can promote the residual stream to FP32, while the FP8
# expert projections return BF16 under autocast. Restore the
# residual dtype before scattering selected expert outputs.
output = output.index_copy(0, token_indices, routed_output.to(output.dtype))
expert_fraction = F.one_hot(route_index, num_classes=self.num_experts).to(router_probs.dtype).mean(dim=0)
preferred_fraction = F.one_hot(preferred_index, num_classes=self.num_experts).to(router_probs.dtype).mean(dim=0)
mean_router_prob = router_probs.mean(dim=0)
self.last_expert_fraction = expert_fraction.detach()
self.last_preferred_expert_fraction = preferred_fraction.detach()
self.last_forced_fraction = (route_index != preferred_index).float().mean().detach()
self.last_selected_gate_probability = selected_router_probability.mean().detach()
# Balance the router's *clean preference* rather than the capacity-
# constrained dispatch, which is intentionally already near-uniform.
aux_loss = self.router_aux_loss_coef * self.num_experts * torch.sum(
preferred_fraction * mean_router_prob
)
z_loss = self.router_z_loss_coef * torch.logsumexp(router_logits, dim=-1).square().mean()
return output.reshape(original_shape), aux_loss + z_loss
class DecoderBlock(nn.Module):
def __init__(self, cfg: AuroraConfig) -> None:
super().__init__()
self.input_layernorm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
self.self_attn = CausalSelfAttention(cfg)
self.post_attention_layernorm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
self.mlp: nn.Module = Top1MoE(cfg) if cfg.num_experts > 1 else SwiGLU(cfg)
def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
x = x + self.self_attn(self.input_layernorm(x), cos, sin)
mlp_input = self.post_attention_layernorm(x)
if isinstance(self.mlp, Top1MoE):
mlp_output, router_loss = self.mlp(mlp_input)
else:
mlp_output = self.mlp(mlp_input)
router_loss = x.new_zeros((), dtype=torch.float32)
return x + mlp_output, router_loss
class AuroraForCausalLM(nn.Module):
def __init__(self, cfg: AuroraConfig) -> None:
super().__init__()
cfg.validate()
self.cfg = cfg
self.embed_tokens = nn.Embedding(cfg.vocab_size, cfg.hidden_size)
self.layers = nn.ModuleList([DecoderBlock(cfg) for _ in range(cfg.num_layers)])
self.norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
self.lm_head = nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
self.last_router_loss: torch.Tensor | None = None
self.register_buffer("rope_cos_cached", torch.empty(0), persistent=False)
self.register_buffer("rope_sin_cached", torch.empty(0), persistent=False)
if cfg.tie_word_embeddings:
self.lm_head.weight = self.embed_tokens.weight
self.apply(self._init_weights)
def _init_weights(self, module: nn.Module) -> None:
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
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=0.02)
def _rope_cache(
self, seq_len: int, device: torch.device, dtype: torch.dtype
) -> tuple[torch.Tensor, torch.Tensor]:
cache_miss = (
self.rope_cos_cached.numel() == 0
or self.rope_cos_cached.size(0) < seq_len
or self.rope_cos_cached.device != device
or self.rope_cos_cached.dtype != dtype
)
if cache_miss:
cos, sin = precompute_rope_frequencies(
self.cfg.context_length,
self.cfg.head_dim,
self.cfg.rope_theta,
device,
dtype,
)
self.rope_cos_cached = cos
self.rope_sin_cached = sin
return self.rope_cos_cached[:seq_len], self.rope_sin_cached[:seq_len]
def _rope_dtype(self, x: torch.Tensor) -> torch.dtype:
if x.device.type == "cuda" and torch.is_autocast_enabled("cuda"):
return torch.get_autocast_dtype("cuda")
if x.device.type == "cpu" and torch.is_autocast_enabled("cpu"):
return torch.get_autocast_dtype("cpu")
return x.dtype
def forward(
self, input_ids: torch.Tensor, labels: torch.Tensor | None = None
) -> tuple[torch.Tensor, torch.Tensor | None]:
x = self.embed_tokens(input_ids)
cos, sin = self._rope_cache(x.size(1), x.device, self._rope_dtype(x))
router_loss = torch.zeros((), device=x.device, dtype=torch.float32)
for layer in self.layers:
x, layer_router_loss = layer(x, cos, sin)
router_loss = router_loss + layer_router_loss
# Each layer produces a regularizer of the same scale. Average them
# so the configured coefficient has the same meaning regardless of
# depth (instead of becoming 16x stronger in this MoE model).
router_loss = router_loss / max(1, len(self.layers))
self.last_router_loss = router_loss.detach()
x = self.norm(x)
if labels is not None and linear_cross_entropy is not None:
# RMSNorm may promote activations to FP32, but Cut Cross Entropy's
# backward kernel requires BF16/FP16 hidden states.
loss = linear_cross_entropy(x.to(self.lm_head.weight.dtype), self.lm_head.weight, labels, shift=True)
logits = x.new_empty(0)
else:
logits = self.lm_head(x)
loss = None
if labels is not None:
loss = F.cross_entropy(
logits[:, :-1].contiguous().view(-1, logits.size(-1)),
labels[:, 1:].contiguous().view(-1),
)
# Keep evaluation perplexity comparable to dense models: router regularization
# shapes gradients only during training and is not language-model loss.
if loss is not None and self.training:
loss = loss + router_loss
return logits, loss
def count_parameters(model: nn.Module) -> int:
seen: set[int] = set()
total = 0
for param in model.parameters():
ident = id(param)
if ident not in seen:
seen.add(ident)
total += param.numel()
return total
def count_active_parameters(model: nn.Module) -> int:
"""Count parameters used by one top-1 path, without double-counting ties."""
seen: set[int] = set()
total = 0
for name, param in model.named_parameters():
if ".mlp.experts." in name:
expert_index = name.split(".mlp.experts.", 1)[1].split(".", 1)[0]
if expert_index != "0":
continue
ident = id(param)
if ident not in seen:
seen.add(ident)
total += param.numel()
return total
def estimate_parameter_count(cfg: AuroraConfig) -> int:
model = AuroraForCausalLM(cfg)
return count_parameters(model)