AbdallaHolmes's picture
Upload lfm2_moe.py with huggingface_hub
c0d0494 verified
Raw
History Blame Contribute Delete
6.3 kB
# North Star OS addition (2026-07-14), style-matched to the Apple BSD-3 tree it lives in.
#
# LFM2.5-MoE (LiquidAI LFM2.5-8B-A1B): the LFM2 hybrid (short-conv + GQA) with a
# 32-expert top-4 sparse SwiGLU MLP on all layers except the first
# `num_dense_layers`. The expert compute rides the SwitchGLU/SwitchLinear (GatherMM)
# primitive, so ONLY the routed experts' weight slabs are read per token. Router math
# matches HF Lfm2MoeTopKRouter EXACTLY (ground truth, since we load HF weights):
# SIGMOID(fp32) -> +expert_bias (SELECTION only) -> top-k -> gather sigmoid weights
# (bias excluded) -> L1 renorm -> routed_scaling_factor.
import torch
import torch.nn as nn
from typing_extensions import Self, override
from coreai_models.models.base import BaseForCausalLM
from coreai_models.models.macos.lfm2 import (
ConvState,
Lfm2DecoderLayer,
Lfm2MLP,
Lfm2Model,
_fuse_lfm2_attention_weights,
build_lfm2_reference_inputs,
)
from coreai_models.primitives.macos.cache import KVCache
from coreai_models.primitives.macos.switch import SwitchGLU
class Lfm2MoeSparseBlock(nn.Module):
def __init__(self, config) -> None:
super().__init__()
dim = config.hidden_size
self.top_k = config.num_experts_per_tok
self.norm_topk_prob = getattr(config, "norm_topk_prob", True)
self.use_expert_bias = getattr(config, "use_expert_bias", False)
self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
self.gate = nn.Linear(dim, config.num_experts, bias=False)
if self.use_expert_bias:
self.expert_bias = nn.Parameter(torch.zeros(config.num_experts))
self.switch_mlp = SwitchGLU(dim, config.moe_intermediate_size, config.num_experts)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Matches HF Lfm2MoeTopKRouter EXACTLY (ground truth, since we load HF weights):
# SIGMOID gate (not softmax); expert_bias added ONLY to pick the top-k experts,
# while the weights are the *sigmoid* scores gathered at those experts (bias
# excluded from weights); then L1-renorm and routed_scaling_factor.
routing_weights = self.gate(x).to(torch.float32).sigmoid() # (B, S, E)
scores_for_routing = (
routing_weights + self.expert_bias.to(torch.float32)
if self.use_expert_bias
else routing_weights
)
_, indices = torch.topk(scores_for_routing, self.top_k, dim=-1) # selection
scores = torch.gather(routing_weights, dim=-1, index=indices) # weights = sigmoid, no bias
if self.norm_topk_prob:
scores = scores / (torch.sum(scores, dim=-1, keepdim=True) + 1e-6)
scores = (scores * self.routed_scaling_factor).to(x.dtype)
y = self.switch_mlp(x, indices.to(torch.uint16)) # (B, S, top_k, D)
y = y * scores.unsqueeze(-1)
return torch.sum(y, dim=-2).to(x.dtype)
class Lfm2MoeDecoderLayer(Lfm2DecoderLayer):
def _build_feed_forward(self, config, layer_idx: int) -> nn.Module:
if layer_idx < getattr(config, "num_dense_layers", 0):
# lfm2_moe stores the dense ff dim post-adjust — use it as-is.
return Lfm2MLP(config, auto_adjust=False)
return Lfm2MoeSparseBlock(config)
class Lfm2MoeModel(Lfm2Model):
layer_cls = Lfm2MoeDecoderLayer
class Lfm2MoeForCausalLM(BaseForCausalLM):
_HF_MODEL_CLASS = None # loaded straight from safetensors; no HF class needed
@override
def _init_model(self, config) -> None:
self.model = Lfm2MoeModel(config)
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
if getattr(config, "tie_word_embeddings", False):
self.lm_head.weight = self.model.embed_tokens.weight
@BaseForCausalLM.cast_logits_bfloat16_to_float16
def forward(
self,
input_ids: torch.Tensor,
position_ids: torch.IntTensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
conv_state: torch.Tensor,
) -> torch.Tensor:
cache = KVCache(k_cache, v_cache)
conv = ConvState(conv_state)
out = self.model(input_ids, position_ids, cache, conv)
return self.lm_head(out)
@override
def _mutate_state_dict(self: Self, state_dict: dict[str, torch.Tensor]) -> None:
_fuse_lfm2_attention_weights(self.model, state_dict)
# Stack per-expert w1/w3/w2 into the SwitchGLU gate/up/down layout
# (1, num_experts, out_dims, in_dims). Checkpoint keys:
# model.layers.{i}.feed_forward.experts.{e}.{w1,w2,w3}.weight
proj_map = {"w1": "gate_proj", "w3": "up_proj", "w2": "down_proj"}
for i in range(len(self.model.layers)):
prefix = f"model.layers.{i}.feed_forward"
if f"{prefix}.experts.0.w1.weight" not in state_dict:
continue
num_experts = 0
while f"{prefix}.experts.{num_experts}.w1.weight" in state_dict:
num_experts += 1
for src, dst in proj_map.items():
first = state_dict[f"{prefix}.experts.0.{src}.weight"]
output = torch.empty(
(1, num_experts) + first.shape, dtype=first.dtype, device=first.device
)
for e in range(num_experts):
output[0, e] = state_dict.pop(f"{prefix}.experts.{e}.{src}.weight")
state_dict[f"{prefix}.switch_mlp.{dst}.weight"] = output
def load_state_dict(self, state_dict, strict: bool = True, assign: bool = False):
result = super().load_state_dict(state_dict, strict=strict, assign=assign)
if getattr(self.config, "tie_word_embeddings", False):
self.lm_head.weight = self.model.embed_tokens.weight
return result
# ---- export hooks (consumed by export_macos_model when present) ----
@staticmethod
def state_names() -> tuple[str, ...]:
return ("k_cache", "v_cache", "conv_state")
@classmethod
def build_reference_inputs(cls, config, target_dtype, max_context_length, trace_query_len, trace_offset, trace_kv_seq_len):
return build_lfm2_reference_inputs(
config, target_dtype, max_context_length, trace_query_len, trace_offset, trace_kv_seq_len
)