ProGen2-base-bf16 / progen2_mlx.py
N8Programs's picture
Upload ProGen2 base BF16 MLX conversion
e8df098 verified
Raw
History Blame Contribute Delete
8.04 kB
# Copyright (c) 2026
"""MLX-LM architecture for ProGen2 causal protein LMs."""
from dataclasses import dataclass
from typing import Any, Optional
import mlx.core as mx
import mlx.nn as nn
from mlx_lm.models.base import (
BaseModelArgs,
create_attention_mask,
scaled_dot_product_attention,
)
@dataclass
class ModelArgs(BaseModelArgs):
model_type: str
vocab_size_emb: int
vocab_size_lm_head: int
n_positions: int
embed_dim: int
n_layer: int
n_head: int
rotary_dim: int = 64
n_inner: Optional[int] = None
activation_function: str = "gelu_new"
layer_norm_epsilon: float = 1e-5
bos_token_id: int = 1
eos_token_id: int = 2
pad_token_id: int = 0
def gelu_new(x: mx.array) -> mx.array:
return 0.5 * x * (
1.0
+ mx.tanh(0.7978845608028654 * (x + 0.044715 * mx.power(x, 3)))
)
def rotate_every_two(x: mx.array) -> mx.array:
x1 = x[..., ::2]
x2 = x[..., 1::2]
stacked = mx.stack((-x2, x1), axis=-1)
return stacked.reshape(*x.shape)
class PartialRotaryEmbedding(nn.Module):
def __init__(self, rotary_dim: int):
super().__init__()
self.rotary_dim = rotary_dim
inv_freq = 1.0 / (
10000
** (mx.arange(0, rotary_dim, 2, dtype=mx.float32) / rotary_dim)
)
self.inv_freq = inv_freq
def __call__(self, x: mx.array, offset: int | mx.array = 0) -> mx.array:
seq_len = x.shape[-2]
offset = mx.array(offset, dtype=mx.float32)
positions = mx.arange(seq_len, dtype=mx.float32)
if offset.ndim == 0:
positions = positions + offset
freqs = positions[:, None] * self.inv_freq[None, :]
emb = mx.repeat(freqs, 2, axis=-1)
cos = mx.cos(emb).astype(x.dtype).reshape(
1,
1,
seq_len,
self.rotary_dim,
)
sin = mx.sin(emb).astype(x.dtype).reshape(
1,
1,
seq_len,
self.rotary_dim,
)
else:
positions = positions[None, :] + offset[:, None]
freqs = positions[:, :, None] * self.inv_freq[None, None, :]
emb = mx.repeat(freqs, 2, axis=-1)
cos = mx.cos(emb).astype(x.dtype).reshape(
x.shape[0],
1,
seq_len,
self.rotary_dim,
)
sin = mx.sin(emb).astype(x.dtype).reshape(
x.shape[0],
1,
seq_len,
self.rotary_dim,
)
return (x * cos) + (rotate_every_two(x) * sin)
class ProGenAttention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
if args.embed_dim % args.n_head != 0:
raise ValueError("embed_dim must be divisible by n_head")
self.embed_dim = args.embed_dim
self.num_heads = args.n_head
self.head_dim = args.embed_dim // args.n_head
self.mp_num = 8
self.mp_part = args.embed_dim // self.mp_num
self.scale = self.head_dim**-0.5
self.rotary_dim = args.rotary_dim
self.qkv_proj = nn.Linear(args.embed_dim, args.embed_dim * 3, bias=False)
self.out_proj = nn.Linear(args.embed_dim, args.embed_dim, bias=False)
self.rotary = PartialRotaryEmbedding(self.rotary_dim)
def _split_heads_from_mp(self, x: mx.array) -> mx.array:
batch_size, seq_len = x.shape[:2]
x = x.reshape(batch_size, seq_len, self.embed_dim)
return x.reshape(
batch_size,
seq_len,
self.num_heads,
self.head_dim,
).transpose(0, 2, 1, 3)
def _apply_partial_rotary(self, x: mx.array, offset: int | mx.array) -> mx.array:
x_rot = x[..., : self.rotary_dim]
x_pass = x[..., self.rotary_dim :]
return mx.concatenate([self.rotary(x_rot, offset=offset), x_pass], axis=-1)
def __call__(
self,
hidden_states: mx.array,
mask: Optional[Any] = None,
cache: Optional[Any] = None,
) -> mx.array:
batch_size, seq_len, _ = hidden_states.shape
qkv = self.qkv_proj(hidden_states)
qkv = qkv.reshape(batch_size, seq_len, self.mp_num, -1)
query, value, key = mx.split(qkv, 3, axis=-1)
query = self._split_heads_from_mp(query)
key = self._split_heads_from_mp(key)
value = self._split_heads_from_mp(value)
offset = 0 if cache is None else cache.offset
query = self._apply_partial_rotary(query, offset=offset)
key = self._apply_partial_rotary(key, offset=offset)
if cache is not None:
key, value = cache.update_and_fetch(key, value)
attn_output = scaled_dot_product_attention(
query,
key,
value,
cache=cache,
scale=self.scale,
mask=mask,
)
attn_output = attn_output.transpose(0, 2, 1, 3).reshape(
batch_size,
seq_len,
self.embed_dim,
)
return self.out_proj(attn_output)
class ProGenMLP(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
inner_dim = args.n_inner if args.n_inner is not None else 4 * args.embed_dim
self.fc_in = nn.Linear(args.embed_dim, inner_dim, bias=True)
self.fc_out = nn.Linear(inner_dim, args.embed_dim, bias=True)
def __call__(self, hidden_states: mx.array) -> mx.array:
return self.fc_out(gelu_new(self.fc_in(hidden_states)))
class ProGenBlock(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.ln_1 = nn.LayerNorm(
args.embed_dim,
eps=args.layer_norm_epsilon,
affine=True,
bias=True,
)
self.attn = ProGenAttention(args)
self.mlp = ProGenMLP(args)
def __call__(
self,
hidden_states: mx.array,
mask: Optional[Any] = None,
cache: Optional[Any] = None,
) -> mx.array:
residual = hidden_states
normed = self.ln_1(hidden_states)
attn_output = self.attn(normed, mask=mask, cache=cache)
mlp_output = self.mlp(normed)
return residual + attn_output + mlp_output
class ProGenModel(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.wte = nn.Embedding(args.vocab_size_emb, args.embed_dim)
self.h = [ProGenBlock(args) for _ in range(args.n_layer)]
self.ln_f = nn.LayerNorm(
args.embed_dim,
eps=args.layer_norm_epsilon,
affine=True,
bias=True,
)
def __call__(self, inputs: mx.array, cache=None) -> mx.array:
hidden_states = self.wte(inputs)
if cache is None:
cache = [None] * len(self.h)
mask = create_attention_mask(hidden_states, cache[0])
for block, c in zip(self.h, cache):
hidden_states = block(hidden_states, mask=mask, cache=c)
return self.ln_f(hidden_states)
class Model(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.model_type = args.model_type
self.transformer = ProGenModel(args)
self.lm_head = nn.Linear(args.embed_dim, args.vocab_size_lm_head, bias=True)
def __call__(self, inputs: mx.array, cache=None) -> mx.array:
return self.lm_head(self.transformer(inputs, cache=cache))
def sanitize(self, weights):
weights = dict(weights)
inv_freq = 1.0 / (
10000
** (
mx.arange(0, self.args.rotary_dim, 2, dtype=mx.float32)
/ self.args.rotary_dim
)
)
for layer_idx in range(self.args.n_layer):
weights[f"transformer.h.{layer_idx}.attn.rotary.inv_freq"] = inv_freq
return weights
@property
def layers(self):
return self.transformer.h