"""MLX-LM adapter for the dense K2 Horizon configuration. K2 Horizon uses the Llama-style decoder/attention layout, but its RMSNorm normalizes two groups of the hidden dimension independently. This module keeps that detail instead of silently treating the checkpoint as Llama. """ from dataclasses import dataclass from typing import Any, Dict, List, Optional, Union import mlx.core as mx import mlx.nn as nn from mlx_lm.models.activations import swiglu from mlx_lm.models.base import BaseModelArgs, create_attention_mask, scaled_dot_product_attention from mlx_lm.models.cache import KVCache, RotatingKVCache from mlx_lm.models.rope_utils import initialize_rope @dataclass class ModelArgs(BaseModelArgs): model_type: str hidden_size: int num_hidden_layers: int intermediate_size: int num_attention_heads: int rms_norm_eps: float vocab_size: int num_key_value_heads: Optional[int] = None head_dim: Optional[int] = None max_position_embeddings: Optional[int] = None # K2 stores RoPE settings under rope_parameters; this is the model default. rope_theta: float = 10_000_000.0 rope_traditional: bool = False rope_scaling: Optional[Dict[str, Union[float, str]]] = None tie_word_embeddings: bool = False attention_bias: bool = False mlp_bias: bool = False layernorm_num_groups: int = 2 sliding_window: Optional[int] = None layer_types: Optional[List[str]] = None def __post_init__(self): if self.num_key_value_heads is None: self.num_key_value_heads = self.num_attention_heads if self.head_dim is None: self.head_dim = self.hidden_size // self.num_attention_heads if self.layer_types is None: self.layer_types = ["full_attention"] * self.num_hidden_layers class GroupRMSNorm(nn.Module): """K2's T5-style grouped RMS normalization.""" def __init__(self, dims: int, eps: float, groups: int): super().__init__() if dims % groups: raise ValueError(f"hidden size {dims} is not divisible by {groups} groups") self.weight = mx.ones((dims,)) self.groups = groups self.eps = eps def __call__(self, x: mx.array) -> mx.array: x = mx.unflatten(x, axis=-1, shape=(self.groups, -1)) x = mx.fast.rms_norm(x, weight=None, eps=self.eps) return self.weight * mx.flatten(x, -2) 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 = args.head_dim self.scale = self.head_dim**-0.5 bias = args.attention_bias self.q_proj = nn.Linear(dim, self.n_heads * self.head_dim, bias=bias) self.k_proj = nn.Linear(dim, self.n_kv_heads * self.head_dim, bias=bias) self.v_proj = nn.Linear(dim, self.n_kv_heads * self.head_dim, bias=bias) self.o_proj = nn.Linear(self.n_heads * self.head_dim, dim, bias=bias) self.rope = initialize_rope( self.head_dim, args.rope_theta, args.rope_traditional, args.rope_scaling, args.max_position_embeddings, ) def __call__(self, x: mx.array, mask=None, cache=None) -> mx.array: B, L, _ = x.shape q = self.q_proj(x).reshape(B, L, self.n_heads, self.head_dim).transpose(0, 2, 1, 3) k = self.k_proj(x).reshape(B, L, self.n_kv_heads, self.head_dim).transpose(0, 2, 1, 3) v = self.v_proj(x).reshape(B, L, self.n_kv_heads, self.head_dim).transpose(0, 2, 1, 3) offset = cache.offset if cache is not None else 0 q = self.rope(q, offset=offset) k = self.rope(k, offset=offset) if cache is not None: k, v = cache.update_and_fetch(k, v) out = scaled_dot_product_attention(q, k, v, cache=cache, scale=self.scale, mask=mask) out = out.transpose(0, 2, 1, 3).reshape(B, L, -1) return self.o_proj(out) class MLP(nn.Module): def __init__(self, args: ModelArgs): super().__init__() self.gate_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=args.mlp_bias) self.down_proj = nn.Linear(args.intermediate_size, args.hidden_size, bias=args.mlp_bias) self.up_proj = nn.Linear(args.hidden_size, args.intermediate_size, bias=args.mlp_bias) def __call__(self, x): return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x))) class TransformerBlock(nn.Module): def __init__(self, args: ModelArgs, use_sliding: bool = False): super().__init__() self.self_attn = Attention(args) self.mlp = MLP(args) self.input_layernorm = GroupRMSNorm( args.hidden_size, args.rms_norm_eps, args.layernorm_num_groups ) self.post_attention_layernorm = GroupRMSNorm( args.hidden_size, args.rms_norm_eps, args.layernorm_num_groups ) self.use_sliding = use_sliding def __call__(self, x, mask=None, cache=None): h = x + self.self_attn(self.input_layernorm(x), mask, cache) return h + self.mlp(self.post_attention_layernorm(h)) class K2HorizonModel(nn.Module): def __init__(self, args: ModelArgs): super().__init__() self.args = args self.vocab_size = args.vocab_size self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size) self.layers = [ TransformerBlock(args, use_sliding=t == "sliding_attention") for t in args.layer_types ] self.norm = GroupRMSNorm(args.hidden_size, args.rms_norm_eps, args.layernorm_num_groups) self.sliding_window = args.sliding_window self.fa_idx = 0 self.swa_idx = None for i, layer in enumerate(self.layers): if layer.use_sliding: self.swa_idx = i break def __call__(self, inputs, cache=None, input_embeddings=None): h = self.embed_tokens(inputs) if input_embeddings is None else input_embeddings if cache is None: cache = [None] * len(self.layers) fa_mask = create_attention_mask(h, cache[self.fa_idx]) swa_mask = None if self.swa_idx is not None: swa_mask = create_attention_mask( h, cache[self.swa_idx], window_size=self.sliding_window ) for layer, c in zip(self.layers, cache): h = layer(h, swa_mask if layer.use_sliding else fa_mask, c) return self.norm(h) class Model(nn.Module): def __init__(self, args: ModelArgs): super().__init__() self.args = args self.model_type = args.model_type self.model = K2HorizonModel(args) if not args.tie_word_embeddings: self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False) def __call__(self, inputs, cache=None, input_embeddings=None): h = self.model(inputs, cache, input_embeddings) return self.lm_head(h) def sanitize(self, weights): return { k: v for k, v in weights.items() if "rotary_emb.inv_freq" not in k } @property def layers(self): return self.model.layers def make_cache(self): return [ RotatingKVCache(max_size=self.model.sliding_window) if layer.use_sliding else KVCache() for layer in self.model.layers ]