Kearm's picture
Native MLX FP32 conversion of LFM2.5-Encoder-350M
591cda4 verified
Raw History Blame Contribute Delete
8.24 kB
"""Native MLX implementation of LiquidAI/LFM2.5-Encoder-350M.
This is an encoder/masked-language model: attention and short convolutions are
bidirectional. It intentionally does not use mlx-lm's causal generation API.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
import mlx.core as mx
import mlx.nn as nn
@dataclass
class ModelArgs:
vocab_size: int
hidden_size: int
num_hidden_layers: int
num_attention_heads: int
num_key_value_heads: int
norm_eps: float
conv_bias: bool
conv_L_cache: int
block_dim: int
intermediate_size: int
block_multiple_of: int
block_ffn_dim_multiplier: float
block_auto_adjust_ff_dim: bool
layer_types: list[str]
rope_theta: float = 1_000_000.0
@classmethod
def from_dict(cls, config: dict) -> "ModelArgs":
rope = config.get("rope_parameters") or {}
fields = {
"vocab_size",
"hidden_size",
"num_hidden_layers",
"num_attention_heads",
"num_key_value_heads",
"norm_eps",
"conv_bias",
"conv_L_cache",
"block_dim",
"intermediate_size",
"block_multiple_of",
"block_ffn_dim_multiplier",
"block_auto_adjust_ff_dim",
"layer_types",
}
values = {key: config[key] for key in fields}
values["rope_theta"] = rope.get("rope_theta", config.get("rope_theta", 1_000_000.0))
return cls(**values)
class Attention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
dim = args.hidden_size
self.n_heads = args.num_attention_heads
self.n_kv_heads = args.num_key_value_heads
self.head_dim = dim // self.n_heads
self.scale = self.head_dim**-0.5
self.q_layernorm = nn.RMSNorm(self.head_dim, eps=args.norm_eps)
self.k_layernorm = nn.RMSNorm(self.head_dim, eps=args.norm_eps)
self.q_proj = nn.Linear(dim, self.n_heads * self.head_dim, bias=False)
self.k_proj = nn.Linear(dim, self.n_kv_heads * self.head_dim, bias=False)
self.v_proj = nn.Linear(dim, self.n_kv_heads * self.head_dim, bias=False)
self.out_proj = nn.Linear(self.n_heads * self.head_dim, dim, bias=False)
self.rope = nn.RoPE(self.head_dim, base=args.rope_theta, traditional=False)
def __call__(self, x: mx.array, mask: Optional[mx.array] = None) -> mx.array:
batch, length, _ = x.shape
queries = self.q_proj(x).reshape(batch, length, self.n_heads, -1)
keys = self.k_proj(x).reshape(batch, length, self.n_kv_heads, -1)
values = self.v_proj(x).reshape(batch, length, self.n_kv_heads, -1)
queries = self.q_layernorm(queries).transpose(0, 2, 1, 3)
keys = self.k_layernorm(keys).transpose(0, 2, 1, 3)
values = values.transpose(0, 2, 1, 3)
queries = self.rope(queries)
keys = self.rope(keys)
output = mx.fast.scaled_dot_product_attention(
queries, keys, values, scale=self.scale, mask=mask
)
output = output.transpose(0, 2, 1, 3).reshape(batch, length, -1)
return self.out_proj(output)
class ShortConv(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.kernel_size = args.conv_L_cache
self.conv = nn.Conv1d(
in_channels=args.hidden_size,
out_channels=args.hidden_size,
kernel_size=self.kernel_size,
groups=args.hidden_size,
bias=args.conv_bias,
)
self.in_proj = nn.Linear(args.hidden_size, 3 * args.hidden_size, bias=args.conv_bias)
self.out_proj = nn.Linear(args.hidden_size, args.hidden_size, bias=args.conv_bias)
def __call__(self, x: mx.array, padding_mask: Optional[mx.array] = None) -> mx.array:
if padding_mask is not None:
x = mx.where(padding_mask[..., None], x, 0)
gate_b, gate_c, value = mx.split(self.in_proj(x), 3, axis=-1)
gated = gate_b * value
pad = self.kernel_size // 2
gated = mx.pad(gated, [(0, 0), (pad, pad), (0, 0)])
conv_out = self.conv(gated)
if conv_out.shape[1] > x.shape[1]:
conv_out = conv_out[:, : x.shape[1], :]
elif conv_out.shape[1] < x.shape[1]:
conv_out = mx.pad(conv_out, [(0, 0), (0, x.shape[1] - conv_out.shape[1]), (0, 0)])
return self.out_proj(gate_c * conv_out)
class MLP(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
ff_dim = args.intermediate_size
if args.block_auto_adjust_ff_dim:
ff_dim = int(2 * ff_dim / 3)
ff_dim = int(args.block_ffn_dim_multiplier * ff_dim)
multiple = args.block_multiple_of
ff_dim = multiple * ((ff_dim + multiple - 1) // multiple)
self.w1 = nn.Linear(args.block_dim, ff_dim, bias=False)
self.w3 = nn.Linear(args.block_dim, ff_dim, bias=False)
self.w2 = nn.Linear(ff_dim, args.block_dim, bias=False)
def __call__(self, x: mx.array) -> mx.array:
return self.w2(nn.silu(self.w1(x)) * self.w3(x))
class EncoderLayer(nn.Module):
def __init__(self, args: ModelArgs, layer_type: str):
super().__init__()
self.is_attention_layer = layer_type == "full_attention"
if self.is_attention_layer:
self.self_attn = Attention(args)
else:
self.conv = ShortConv(args)
self.feed_forward = MLP(args)
self.operator_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
self.ffn_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
def __call__(
self,
x: mx.array,
attention_mask: Optional[mx.array],
padding_mask: Optional[mx.array],
) -> mx.array:
normalized = self.operator_norm(x)
if self.is_attention_layer:
residual = self.self_attn(normalized, mask=attention_mask)
else:
residual = self.conv(normalized, padding_mask=padding_mask)
hidden = x + residual
return hidden + self.feed_forward(self.ffn_norm(hidden))
class Lfm2BidirectionalModel(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
self.layers = [EncoderLayer(args, layer_type) for layer_type in args.layer_types]
self.embedding_norm = nn.RMSNorm(args.hidden_size, eps=args.norm_eps)
def __call__(
self, input_ids: mx.array, attention_mask: Optional[mx.array] = None
) -> mx.array:
hidden = self.embed_tokens(input_ids)
padding_mask = None
additive_mask = None
if attention_mask is not None:
padding_mask = attention_mask.astype(mx.bool_)
additive_mask = mx.where(
padding_mask[:, None, None, :],
mx.array(0.0, dtype=hidden.dtype),
mx.array(-1e9, dtype=hidden.dtype),
)
for layer in self.layers:
hidden = layer(hidden, additive_mask, padding_mask)
return self.embedding_norm(hidden)
class Model(nn.Module):
"""Bidirectional encoder with a tied masked-language-model head."""
def __init__(self, args: ModelArgs):
super().__init__()
self.args = args
self.lfm2 = Lfm2BidirectionalModel(args)
def __call__(
self,
input_ids: mx.array,
attention_mask: Optional[mx.array] = None,
*,
return_hidden: bool = False,
) -> mx.array | tuple[mx.array, mx.array]:
hidden = self.lfm2(input_ids, attention_mask=attention_mask)
logits = self.lfm2.embed_tokens.as_linear(hidden)
return (logits, hidden) if return_hidden else logits
def load_model(path: str | Path) -> Model:
path = Path(path)
with (path / "config.json").open() as handle:
args = ModelArgs.from_dict(json.load(handle))
model = Model(args)
weights = mx.load(str(path / "model.safetensors"))
model.load_weights(list(weights.items()), strict=True)
mx.eval(model.parameters())
return model