Evo2-20B-1M / layers.py
Taykhoom's picture
Bit-exact with vortex: checkpoint RoPE frequencies, trained FP8 scales, fp8_recipe option
5ef8dc1 verified
Raw History Blame Contribute Delete
11.8 kB
"""Basic layers for Evo2 (StripedHyena2).
* RMSNorm -- pre/post norm in each block
* ParallelGatedMLP -- GLU(act(l1(x)) * l2(x)) -> l3(...); evo2_style
activations replace `act` with Identity for layer > 0
* VocabParallelEmbedding -- single-process variant of vortex's
VocabParallelEmbedding so checkpoint keys match
* TELinear -- TransformerEngine FP8-capable Linear, with a
pure-PyTorch fallback when TE is not installed.
Used for the input QKV-like projections of
Hyena cascade blocks; required for 1B/20B/40B.
"""
from __future__ import annotations
from typing import Callable
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
try:
from transformer_engine.pytorch import Linear as _TELinearBase
from transformer_engine.common.recipe import Format, DelayedScaling
import transformer_engine.pytorch as te
HAS_TE = True
except ImportError:
HAS_TE = False
def grab_first_if_tuple(x):
if x.__class__.__name__ == "tuple":
return x[0]
return x
class RMSNorm(nn.Module):
def __init__(self, config):
super().__init__()
self.eps = config.eps
self.hidden_size = config.hidden_size
self.scale = nn.Parameter(torch.ones(self.hidden_size))
self.register_parameter("scale", self.scale)
self.use_flash_rmsnorm = config.get("use_flash_rmsnorm", False)
if self.use_flash_rmsnorm:
from flash_attn.ops.rms_norm import rms_norm as rmsnorm_func
self.rmsnorm_func = rmsnorm_func
def forward(self, x):
if self.use_flash_rmsnorm:
return self.rmsnorm_func(x, self.scale, self.eps)
y = x / (x.norm(2, dim=-1, keepdim=True) * self.hidden_size ** (-1.0 / 2) + self.eps)
return self.scale * y
class ParallelGatedMLP(nn.Module):
"""GLU MLP. With evo2_style_activations=True, layer_idx > 0 uses Identity
in place of the gating activation; layer 0 keeps gelu."""
def __init__(self, config, layer_idx: int):
super().__init__()
self.layer_idx = layer_idx
multiple_of = config.get("inner_size_multiple_of", 64)
self.act_type = config.get("mlp_activation", "gelu")
if self.act_type == "gelu":
self.act = F.gelu
elif self.act_type == "silu":
self.act = F.silu
else:
raise NotImplementedError(f"Unknown mlp_activation: {self.act_type}")
if self.layer_idx > 0 and config.get("evo2_style_activations", False):
self.act = nn.Identity()
self.multiple_of = multiple_of * config.model_parallel_size
inner_size = int(2 * config.hidden_size * 4 / 3)
inner_size = self.multiple_of * ((inner_size + self.multiple_of - 1) // self.multiple_of)
if config.get("inner_mlp_size", None) is not None:
inner_size = config.inner_mlp_size
self.l1 = nn.Linear(config.hidden_size, inner_size, bias=False)
self.l2 = nn.Linear(config.hidden_size, inner_size, bias=False)
self.l3 = nn.Linear(inner_size, config.hidden_size, bias=False)
def forward(self, z):
z1, z2 = self.l1(z), self.l2(z)
z1, z2 = grab_first_if_tuple(z1), grab_first_if_tuple(z2)
y = self.l3(self.act(z1) * z2)
return grab_first_if_tuple(y)
class VocabParallelEmbedding(nn.Embedding):
"""Single-process variant of vortex's VocabParallelEmbedding.
Drops distributed sharding so this minimal port runs on a single device
(HF/accelerate handles cross-device placement via device_map="auto").
"""
def __init__(self, config):
vocab_size = config.vocab_size
padding_idx = config.get("padding_idx", None)
super().__init__(vocab_size, embedding_dim=config.hidden_size, padding_idx=padding_idx)
def embed(self, x: Tensor) -> Tensor:
return self.forward(x)
def unembed(self, u: Tensor) -> Tensor:
return u @ self.weight.T
if HAS_TE:
_TE_WORKSPACE_FIXUP_APPLIED = False
def fixup_te_workspace():
"""Patch TE's Linear module to use per-device cuBLAS workspaces.
Vortex helper, ported verbatim. Without this, TE's Linear uses a single
workspace tensor allocated on whatever CUDA device was current at first
call, then fails ("cuBLAS Error: the function failed to launch on the
GPU") when called from layers on other devices -- which happens with
device_map="auto" sharding. Idempotent; safe to call multiple times.
No-op when TE is not installed (the import guard above gates it).
"""
global _TE_WORKSPACE_FIXUP_APPLIED
if _TE_WORKSPACE_FIXUP_APPLIED:
return
from functools import lru_cache
@lru_cache
def te_cublas_get_workspace_per_device(device):
import transformer_engine.pytorch.module.base as tebase
with torch.cuda.device(device):
tebase._cublas_workspace = None # force get_workspace() to reallocate
return tebase.get_workspace()
def get_workspace():
return te_cublas_get_workspace_per_device(torch.cuda.current_device())
import transformer_engine.pytorch.module.linear as telinear
telinear.get_workspace = get_workspace
_TE_WORKSPACE_FIXUP_APPLIED = True
# Trained FP8 scaling state from the original checkpoint. TE's own
# _extra_state is a pickled blob, so we keep the fp32 values as int32 bit
# patterns instead: integer tensors survive dtype casts (bf16 loading,
# .to()) untouched. Shapes follow the HYBRID DelayedScaling recipe.
FP8_STATE_SHAPES = {
"fp8_scale_fwd": (3,),
"fp8_amax_history_fwd": (16, 3),
"fp8_scale_bwd": (2,),
"fp8_amax_history_bwd": (16, 2),
}
def set_format_recipe(kind: str = "delayed"):
"""FP8 format and scaling recipe.
"delayed" is the recipe the original Evo 2 uses. Its scaling state is
shared across layers, so a gradient-enabled forward over layers on
different GPUs fails; "current" keeps no shared state and works there.
"""
fp8_format = Format.HYBRID
if kind == "current":
from transformer_engine.common.recipe import Float8CurrentScaling
return fp8_format, Float8CurrentScaling(fp8_format=fp8_format)
fp8_recipe = DelayedScaling(fp8_format=fp8_format, amax_history_len=16, amax_compute_algo="max")
return fp8_format, fp8_recipe
class TELinear(_TELinearBase):
"""Wrapper for Transformer-Engine's Linear, matching vortex's signature.
Returns (out, None) so callers can grab_first_if_tuple, matching vortex.
"""
def __init__(
self,
input_size: int,
output_size: int,
init_method: Callable | None = None,
bias: bool = True,
skip_bias_add: bool = False,
use_fp8: bool = False,
fp8_recipe: str = "delayed",
**kwargs,
):
params_dtype = torch.bfloat16
self.te_return_bias = skip_bias_add and bias
self.use_fp8_input_projections = use_fp8
if use_fp8:
self.fp8_format, self.fp8_recipe = set_format_recipe(fp8_recipe)
super().__init__(
in_features=input_size,
out_features=output_size,
sequence_parallel=False,
fuse_wgrad_accumulation=False,
tp_group=None,
tp_size=1,
init_method=init_method,
params_dtype=params_dtype,
parallel_mode=None,
bias=bias,
return_bias=self.te_return_bias,
**kwargs,
)
if use_fp8:
# Zeros mean "no trained state": TE then starts from scale 1.
for name, shape in FP8_STATE_SHAPES.items():
self.register_buffer(name, torch.zeros(shape, dtype=torch.int32))
self._fp8_state_restored = False
def _restore_trained_fp8_state(self):
"""Load the checkpoint's FP8 scales into TE, as vortex does.
With the delayed recipe, TE only updates scales when gradients are
on, so inference runs with these trained values, like the original.
"""
self._fp8_state_restored = True
if not self.fp8_recipe.delayed() or not bool(self.fp8_scale_fwd.any()):
return
import pickle
def fp32(t):
return t.detach().cpu().view(torch.float32)
state = {
"scale_fwd": fp32(self.fp8_scale_fwd),
"amax_history_fwd": fp32(self.fp8_amax_history_fwd),
"scale_bwd": fp32(self.fp8_scale_bwd),
"amax_history_bwd": fp32(self.fp8_amax_history_bwd),
"recipe": self.fp8_recipe,
"extra_fp8_variables": {"fp8_checkpoint": True, "num_gemms": 1,
"fp8_max_fwd": 448, "fp8_max_bwd": 57344},
}
blob = torch.frombuffer(bytearray(pickle.dumps(state)), dtype=torch.uint8)
with torch.cuda.device(self.weight.device):
self.set_extra_state(blob)
def forward(self, x):
if self.use_fp8_input_projections:
if not self._fp8_state_restored:
self._restore_trained_fp8_state()
with te.fp8_autocast(enabled=True, fp8_recipe=self.fp8_recipe):
out = super().forward(x)
else:
out = super().forward(x)
if self.te_return_bias:
return out
return out, None
else:
def fixup_te_workspace():
"""No-op when TransformerEngine is not installed."""
return
class TELinear(nn.Module):
"""Pure-PyTorch fallback for TELinear (no FP8). Used by 7B variants
when TransformerEngine isn't installed.
Parameters are registered with names that match TE's state_dict layout
(`weight`, `bias`), so checkpoints saved from either path are
cross-loadable.
"""
def __init__(
self,
input_size: int,
output_size: int,
init_method: Callable | None = None,
bias: bool = True,
skip_bias_add: bool = False,
use_fp8: bool = False,
**kwargs,
):
super().__init__()
if use_fp8:
raise RuntimeError(
"FP8 requires Transformer Engine, which is not installed. "
"Install it with: pip install transformer_engine>=2.3.0"
)
self.te_return_bias = skip_bias_add and bias
self.use_fp8_input_projections = False
self.in_features = input_size
self.out_features = output_size
self.has_bias = bias
self.weight = nn.Parameter(torch.empty(output_size, input_size, dtype=torch.bfloat16))
if bias:
self.bias = nn.Parameter(torch.zeros(output_size, dtype=torch.bfloat16))
else:
self.register_parameter("bias", None)
if init_method is not None:
init_method(self.weight)
else:
nn.init.xavier_uniform_(self.weight)
def forward(self, x):
out = F.linear(x.to(self.weight.dtype), self.weight, self.bias)
if self.te_return_bias:
return out, self.bias
return out, None