Text Generation
MLX
Safetensors
English
Korean
Japanese
solar_open2
solar-open2
upstage
solar
Mixture of Experts
quantized
dwq
apple-silicon
mixed-precision
Eval Results
custom_code
conversational
4-bit precision
Instructions to use avlp12/Solar-Open2-250B-Alis-MLX-Dynamic with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use avlp12/Solar-Open2-250B-Alis-MLX-Dynamic with MLX:
# Make sure mlx-lm is installed # pip install --upgrade mlx-lm # Generate text with mlx-lm from mlx_lm import load, generate model, tokenizer = load("avlp12/Solar-Open2-250B-Alis-MLX-Dynamic") prompt = "Write a story about Einstein" messages = [{"role": "user", "content": prompt}] prompt = tokenizer.apply_chat_template( messages, add_generation_prompt=True ) text = generate(model, tokenizer, prompt=prompt, verbose=True) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Pi
How to use avlp12/Solar-Open2-250B-Alis-MLX-Dynamic with Pi:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "avlp12/Solar-Open2-250B-Alis-MLX-Dynamic"
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "mlx-lm": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "avlp12/Solar-Open2-250B-Alis-MLX-Dynamic" } ] } } }Run Pi
# Start Pi in your project directory: pi
- OpenClaw new
How to use avlp12/Solar-Open2-250B-Alis-MLX-Dynamic with OpenClaw:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "avlp12/Solar-Open2-250B-Alis-MLX-Dynamic"
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "avlp12/Solar-Open2-250B-Alis-MLX-Dynamic" \ --custom-provider-id mlx-lm \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
- MLX LM
How to use avlp12/Solar-Open2-250B-Alis-MLX-Dynamic with MLX LM:
Generate or start a chat session
# Install MLX LM uv tool install mlx-lm # Interactive chat REPL mlx_lm.chat --model "avlp12/Solar-Open2-250B-Alis-MLX-Dynamic"
Run an OpenAI-compatible server
# Install MLX LM uv tool install mlx-lm # Start the server mlx_lm.server --model "avlp12/Solar-Open2-250B-Alis-MLX-Dynamic" # Calling the OpenAI-compatible server with curl curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "avlp12/Solar-Open2-250B-Alis-MLX-Dynamic", "messages": [ {"role": "user", "content": "Hello"} ] }' - Hermes Agent
How to use avlp12/Solar-Open2-250B-Alis-MLX-Dynamic with Hermes Agent:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "avlp12/Solar-Open2-250B-Alis-MLX-Dynamic"
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default avlp12/Solar-Open2-250B-Alis-MLX-Dynamic
Run Hermes
hermes
| # Copyright © 2025 Apple Inc. | |
| # | |
| # mlx-lm port of Upstage Solar Open 2 (250B-A15B hybrid MoE). | |
| # | |
| # Architecture (ground truth: upstageAI/transformers @ v5.14.1-solar-open2, | |
| # src/transformers/models/solar_open2/modeling_solar_open2.py): | |
| # * 48 decoder layers; explicit ``gqa_layers`` (0,4,8,...,44) use NoPE | |
| # grouped-query full attention, the other 36 use Kimi-Delta-Attention | |
| # (KDA, gated delta rule with a per-(head, dim) vector decay). | |
| # * Every layer past ``first_k_dense_replace`` is a MoE layer: | |
| # 320 routed experts (top-8, sigmoid router with grouped top-k and a | |
| # non-trainable e_score_correction_bias) + 1 shared expert. | |
| # | |
| # Reuse strategy: structure follows mlx_lm/models/kimi_linear.py (same KDA | |
| # family) with the Solar-specific gate math (lower-bounded log-decay gate, | |
| # optional beta x2 for negative eigenvalues) and a plain NoPE GQA layer | |
| # instead of MLA. The recurrent metal kernel / ops fallbacks are imported | |
| # from mlx_lm/models/gated_delta.py; the MoE feed-forward is | |
| # mlx_lm/models/switch_layers.py:SwitchGLU so QuantizedSwitchGLU conversion | |
| # and alis-dwq's SwitchGLU hooks keep working unchanged. | |
| from dataclasses import dataclass | |
| from functools import partial | |
| from typing import Any, Dict, List, Optional, Tuple | |
| import mlx.core as mx | |
| import mlx.nn as nn | |
| from .activations import swiglu | |
| from .base import ( | |
| BaseModelArgs, | |
| create_attention_mask, | |
| create_ssm_mask, | |
| scaled_dot_product_attention, | |
| ) | |
| from .cache import ArraysCache, KVCache | |
| from .gated_delta import gated_delta_kernel, gated_delta_ops | |
| from .rope_utils import initialize_rope | |
| from .switch_layers import SwitchGLU | |
| class ModelArgs(BaseModelArgs): | |
| model_type: str | |
| vocab_size: int = 196608 | |
| hidden_size: int = 4096 | |
| num_hidden_layers: int = 48 | |
| num_attention_heads: int = 64 | |
| num_key_value_heads: int = 8 | |
| head_dim: int = 128 | |
| intermediate_size: int = 10240 | |
| moe_intermediate_size: int = 1280 | |
| rms_norm_eps: float = 1e-5 | |
| # Kept for checkpoint-config compatibility; unused unless use_rope=True. | |
| rope_theta: float = 10000.0 | |
| rope_scaling: Optional[Dict[str, Any]] = None | |
| partial_rotary_factor: float = 1.0 # was silently dropped by from_dict | |
| max_position_embeddings: int = 1048576 | |
| tie_word_embeddings: bool = False | |
| attention_bias: bool = False | |
| # Full-attention (GQA) layer options. | |
| use_rope: bool = False # NoPE by default | |
| use_qk_norm: bool = False | |
| use_gqa_gate: bool = True | |
| use_gqa_gate_bias: bool = False | |
| gqa_interval: int = 4 | |
| gqa_layers: Optional[List[int]] = None # takes priority over gqa_interval | |
| # KDA (linear attention) options. | |
| linear_attn_config: Optional[Dict[str, Any]] = None | |
| kda_use_full_proj: bool = False | |
| kda_gate_lower_bound: Optional[float] = -5.0 | |
| # HF SolarOpen2Config's *code* default is True (configuration:145; its | |
| # docstring wrongly says False); the shipped checkpoint config sets it | |
| # explicitly to true, so real-model behavior is unchanged either way. | |
| # Match the HF code default. | |
| kda_allow_neg_eigval: bool = True | |
| # MoE options. | |
| n_routed_experts: int = 320 | |
| n_shared_experts: int = 1 | |
| num_experts_per_tok: int = 8 | |
| norm_topk_prob: bool = True | |
| routed_scaling_factor: float = 1.0 | |
| n_group: int = 1 | |
| topk_group: int = 1 | |
| first_k_dense_replace: int = 0 | |
| hidden_act: str = "silu" # SwitchGLU hard-codes SwiGLU; silu-only port | |
| def __post_init__(self): | |
| if self.hidden_act != "silu": | |
| raise ValueError( | |
| f"hidden_act={self.hidden_act!r} is not supported: SwitchGLU " | |
| "and SolarMLP hard-code SwiGLU (silu-only port)." | |
| ) | |
| if self.linear_attn_config is None: | |
| self.linear_attn_config = { | |
| "short_conv_kernel_size": 4, | |
| "head_dim": self.head_dim, | |
| "num_heads": self.num_attention_heads, | |
| "num_kv_heads": None, | |
| } | |
| # Per-layer attention pattern, mirroring SolarOpen2Config.__post_init__. | |
| if self.gqa_layers is not None: | |
| full = set(self.gqa_layers) | |
| self.layer_types = [ | |
| "full_attention" if i in full else "linear_attention" | |
| for i in range(self.num_hidden_layers) | |
| ] | |
| else: | |
| self.layer_types = [ | |
| ( | |
| "full_attention" | |
| if (i + 1) % self.gqa_interval == 0 | |
| else "linear_attention" | |
| ) | |
| for i in range(self.num_hidden_layers) | |
| ] | |
| # -------------------------------------------------------------------------- | |
| # KDA helpers | |
| # -------------------------------------------------------------------------- | |
| def _kda_decay_gate( | |
| g_raw: mx.array, | |
| A_log: mx.array, | |
| dt_bias: mx.array, | |
| lower_bound: Optional[float], | |
| ) -> mx.array: | |
| """Solar Open 2 KDA log-decay gate (reference: ``torch_kda_gate`` + | |
| ``fla.ops.kda.gate.fused_kda_gate``). | |
| ``g = -exp(A_log) * softplus(g_raw + dt_bias)`` computed in float32, | |
| clamped from below at ``lower_bound`` (-5.0 by default), then | |
| exponentiated once so the result is the multiplicative per-(head, dim) | |
| decay consumed by ``gated_delta_ops`` / ``gated_delta_kernel``. | |
| Differs from kimi_linear's ``compute_g``: that variant has no lower-bound | |
| clamp because Kimi Linear leaves the gate unclamped. | |
| """ | |
| g = g_raw.astype(mx.float32) + dt_bias.astype(mx.float32) | |
| g = -mx.exp(A_log.astype(mx.float32)) * nn.softplus(g) | |
| if lower_bound is not None: | |
| g = mx.maximum(g, mx.array(lower_bound, dtype=mx.float32)) | |
| return mx.exp(g) | |
| def _l2norm(x: mx.array, eps: float) -> mx.array: | |
| """x * rsqrt(sum(x^2) + eps) computed in float32 (HF l2norm reference).""" | |
| xf = x.astype(mx.float32) | |
| return xf * mx.rsqrt((xf * xf).sum(axis=-1, keepdims=True) + eps) | |
| class SolarRMSNormGated(nn.Module): | |
| """Gated RMSNorm at the KDA output: ``RMSNorm(x) * sigmoid(gate)``, | |
| computed in float32 like ``SolarOpen2RMSNormGated`` / | |
| ``fla.modules.FusedRMSNormGated(activation="sigmoid")``.""" | |
| def __init__(self, hidden_size: int, eps: float = 1e-5): | |
| super().__init__() | |
| self.weight = mx.ones((hidden_size,)) | |
| self.eps = eps | |
| def __call__(self, x: mx.array, gate: mx.array) -> mx.array: | |
| return _gated_rms_norm(x, self.weight, gate, self.eps) | |
| def _gated_rms_norm(x, weight, gate, eps): | |
| xf = x.astype(mx.float32) | |
| variance = (xf * xf).mean(axis=-1, keepdims=True) | |
| xf = xf * mx.rsqrt(variance + eps) | |
| xf = xf * weight.astype(mx.float32) * mx.sigmoid(gate.astype(mx.float32)) | |
| return xf.astype(x.dtype) | |
| class ShortConv1d(nn.Module): | |
| """Depthwise causal short conv (silu, no bias) with a (kernel - 1)-token | |
| recurrent state. Identical to kimi_linear.ShortConv1d; kept local so the | |
| port is self-contained.""" | |
| def __init__(self, channels: int, kernel_size: int): | |
| super().__init__() | |
| self.kernel_size = kernel_size | |
| self.conv = nn.Conv1d( | |
| in_channels=channels, | |
| out_channels=channels, | |
| kernel_size=kernel_size, | |
| bias=False, | |
| groups=channels, | |
| padding=0, | |
| ) | |
| def __call__( | |
| self, | |
| x: mx.array, | |
| state: Optional[mx.array], | |
| mask: Optional[mx.array], | |
| lengths: Optional[mx.array], | |
| ) -> Tuple[mx.array, mx.array]: | |
| if mask is not None: | |
| x = mx.where(mask[..., None], x, 0) | |
| if state is None: | |
| state = mx.zeros( | |
| (x.shape[0], self.kernel_size - 1, x.shape[-1]), dtype=x.dtype | |
| ) | |
| conv_input = mx.concatenate([state, x], axis=1) | |
| out = nn.silu(self.conv(conv_input)) | |
| n_keep = self.kernel_size - 1 | |
| if lengths is not None: | |
| ends = mx.clip(lengths, 0, x.shape[1]) | |
| positions = (ends[:, None] + mx.arange(n_keep))[..., None] | |
| new_state = mx.take_along_axis(conv_input, positions, axis=1) | |
| else: | |
| new_state = mx.contiguous(conv_input[:, -n_keep:, :]) | |
| return out, new_state | |
| class SolarDeltaAttention(nn.Module): | |
| """Kimi-Delta-Attention layer as configured for Solar Open 2. | |
| Differences vs kimi_linear.KimiDeltaAttention: | |
| * optional GQA-style kv heads (``linear_attn_config.num_kv_heads``; | |
| Solar Open 2 sets it to null -> equal to num_heads), | |
| * ``kda_use_full_proj`` switches between full-rank ``f_proj``/``g_proj`` | |
| and the factored low-rank ``f_a_proj``/``f_b_proj`` / | |
| ``g_a_proj``/``g_b_proj`` (Solar Open 2 uses the factored form), | |
| * the decay gate is lower-bounded (``kda_gate_lower_bound=-5.0``), | |
| * ``kda_allow_neg_eigval`` scales beta by 2 (beta in [0, 2]). | |
| """ | |
| def __init__(self, args: ModelArgs, layer_idx: int): | |
| super().__init__() | |
| cfg = args.linear_attn_config | |
| self.layer_idx = layer_idx | |
| self.num_heads = cfg["num_heads"] | |
| self.head_dim = cfg["head_dim"] | |
| self.num_kv_heads = cfg.get("num_kv_heads") or self.num_heads | |
| self.n_rep = self.num_heads // self.num_kv_heads | |
| self.conv_kernel = cfg.get("short_conv_kernel_size", 4) | |
| self.use_full_proj = args.kda_use_full_proj | |
| self.gate_lower_bound = args.kda_gate_lower_bound | |
| self.allow_neg_eigval = args.kda_allow_neg_eigval | |
| self.projection_dim = self.num_heads * self.head_dim | |
| self.kv_projection_dim = self.num_kv_heads * self.head_dim | |
| hidden = args.hidden_size | |
| self.scale = float(self.head_dim) ** -0.5 | |
| self.q_proj = nn.Linear(hidden, self.projection_dim, bias=False) | |
| self.k_proj = nn.Linear(hidden, self.kv_projection_dim, bias=False) | |
| self.v_proj = nn.Linear(hidden, self.kv_projection_dim, bias=False) | |
| self.b_proj = nn.Linear(hidden, self.num_heads, bias=False) | |
| self.q_conv = ShortConv1d(self.projection_dim, self.conv_kernel) | |
| self.k_conv = ShortConv1d(self.kv_projection_dim, self.conv_kernel) | |
| self.v_conv = ShortConv1d(self.kv_projection_dim, self.conv_kernel) | |
| if self.use_full_proj: | |
| self.f_proj = nn.Linear(hidden, self.projection_dim, bias=False) | |
| self.g_proj = nn.Linear(hidden, self.projection_dim, bias=False) | |
| else: | |
| self.f_a_proj = nn.Linear(hidden, self.head_dim, bias=False) | |
| self.f_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False) | |
| self.g_a_proj = nn.Linear(hidden, self.head_dim, bias=False) | |
| self.g_b_proj = nn.Linear(self.head_dim, self.projection_dim, bias=False) | |
| # Shapes match the HF checkpoint: A_log (1, 1, H, 1), dt_bias (H*Dh,). | |
| self.A_log = mx.expand_dims( | |
| mx.log(mx.random.uniform(low=1.0, high=16.0, shape=(self.num_heads,))), | |
| (0, 1, 3), | |
| ) | |
| self.dt_bias = mx.ones((self.projection_dim,)) | |
| self.o_norm = SolarRMSNormGated(self.head_dim, eps=args.rms_norm_eps) | |
| self.o_proj = nn.Linear(self.projection_dim, hidden, bias=False) | |
| def __call__( | |
| self, | |
| x: mx.array, | |
| mask: Optional[mx.array] = None, | |
| cache: Optional[Any] = None, | |
| ) -> mx.array: | |
| B, T, _ = x.shape | |
| dtype = x.dtype | |
| if cache is not None: | |
| q_state, k_state, v_state, ssm_state = cache | |
| lengths = cache.lengths | |
| else: | |
| q_state = k_state = v_state = ssm_state = None | |
| lengths = None | |
| q_conv, q_state = self.q_conv(self.q_proj(x), q_state, mask, lengths) | |
| k_conv, k_state = self.k_conv(self.k_proj(x), k_state, mask, lengths) | |
| v_conv, v_state = self.v_conv(self.v_proj(x), v_state, mask, lengths) | |
| if cache is not None: | |
| cache[0] = q_state | |
| cache[1] = k_state | |
| cache[2] = v_state | |
| q = q_conv.reshape(B, T, self.num_heads, self.head_dim) | |
| k = k_conv.reshape(B, T, self.num_kv_heads, self.head_dim) | |
| v = v_conv.reshape(B, T, self.num_kv_heads, self.head_dim) | |
| # l2-normalize q/k exactly like the reference | |
| # (use_qk_l2norm_in_kernel=True): x * rsqrt(sum(x^2) + eps) in f32. | |
| # Folding this through mx.fast.rms_norm is NOT equivalent: rms_norm | |
| # puts eps under the mean (i.e. D*eps on the sum), which measurably | |
| # shifts small-norm head vectors (validated on real weights, see | |
| # VALIDATION_RESULTS.md). q/k/v go into the recurrence in f32, | |
| # mirroring the f32 casts in torch_recurrent_kda. | |
| q = _l2norm(q, 1e-6) * self.scale | |
| k = _l2norm(k, 1e-6) | |
| v = v.astype(mx.float32) | |
| if self.use_full_proj: | |
| g_raw = self.f_proj(x) | |
| else: | |
| g_raw = self.f_b_proj(self.f_a_proj(x)) | |
| g_raw = g_raw.reshape(B, T, self.num_heads, self.head_dim) | |
| beta = mx.sigmoid(self.b_proj(x)) | |
| if self.allow_neg_eigval: | |
| beta = beta * 2.0 | |
| g = _kda_decay_gate( | |
| g_raw, | |
| self.A_log.reshape(self.num_heads, 1), | |
| self.dt_bias.reshape(self.num_heads, self.head_dim), | |
| self.gate_lower_bound, | |
| ) | |
| if ssm_state is None: | |
| ssm_state = mx.zeros( | |
| (B, self.num_kv_heads, self.head_dim, self.head_dim), | |
| dtype=mx.float32, | |
| ) | |
| if ( | |
| self.training | |
| or mx.default_device() != mx.gpu | |
| or not mx.metal.is_available() | |
| ): | |
| out, ssm_state = gated_delta_ops(q, k, v, g, beta, ssm_state, mask) | |
| else: | |
| out, ssm_state = gated_delta_kernel(q, k, v, g, beta, ssm_state, mask) | |
| if cache is not None: | |
| cache[3] = ssm_state | |
| cache.advance(T) | |
| if self.use_full_proj: | |
| g_out = self.g_proj(x) | |
| else: | |
| g_out = self.g_b_proj(self.g_a_proj(x)) | |
| g_out = g_out.reshape(B, T, self.num_heads, self.head_dim) | |
| out = self.o_norm(out.reshape(B, T, self.num_heads, self.head_dim), g_out) | |
| out = out.reshape(B, T, -1).astype(dtype) | |
| return self.o_proj(out) | |
| class SolarFullAttention(nn.Module): | |
| """Grouped-query full attention: NoPE by default (use_rope=False), | |
| optional per-head qk RMSNorm (off in Solar Open 2), and a sigmoid output | |
| gate (``use_gqa_gate=True``: ``attn_out * sigmoid(g_proj(x))``).""" | |
| def __init__(self, args: ModelArgs, layer_idx: int): | |
| super().__init__() | |
| self.layer_idx = layer_idx | |
| self.num_heads = args.num_attention_heads | |
| self.num_key_value_heads = args.num_key_value_heads | |
| self.head_dim = args.head_dim | |
| self.scale = float(self.head_dim) ** -0.5 | |
| self.use_gqa_gate = args.use_gqa_gate | |
| self.use_qk_norm = args.use_qk_norm | |
| self.use_rope = args.use_rope | |
| hidden = args.hidden_size | |
| self.q_proj = nn.Linear( | |
| hidden, self.num_heads * self.head_dim, bias=args.attention_bias | |
| ) | |
| self.k_proj = nn.Linear( | |
| hidden, self.num_key_value_heads * self.head_dim, bias=args.attention_bias | |
| ) | |
| self.v_proj = nn.Linear( | |
| hidden, self.num_key_value_heads * self.head_dim, bias=args.attention_bias | |
| ) | |
| self.o_proj = nn.Linear(self.num_heads * self.head_dim, hidden, bias=False) | |
| if self.use_qk_norm: | |
| self.q_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps) | |
| self.k_norm = nn.RMSNorm(self.head_dim, eps=args.rms_norm_eps) | |
| if self.use_rope: | |
| self.rope = initialize_rope( | |
| int(self.head_dim * args.partial_rotary_factor), | |
| base=args.rope_theta, | |
| traditional=False, | |
| scaling_config=args.rope_scaling, | |
| max_position_embeddings=args.max_position_embeddings, | |
| ) | |
| else: | |
| self.rope = None | |
| if self.use_gqa_gate: | |
| self.g_proj = nn.Linear( | |
| hidden, | |
| self.num_heads * self.head_dim, | |
| bias=args.use_gqa_gate_bias, | |
| ) | |
| def __call__( | |
| self, | |
| x: mx.array, | |
| mask: Optional[mx.array] = None, | |
| cache: Optional[KVCache] = None, | |
| ) -> mx.array: | |
| B, L, _ = x.shape | |
| q = self.q_proj(x).reshape(B, L, self.num_heads, self.head_dim) | |
| k = self.k_proj(x).reshape(B, L, self.num_key_value_heads, self.head_dim) | |
| v = self.v_proj(x).reshape(B, L, self.num_key_value_heads, self.head_dim) | |
| if self.use_qk_norm: | |
| q = self.q_norm(q) | |
| k = self.k_norm(k) | |
| q = q.transpose(0, 2, 1, 3) | |
| k = k.transpose(0, 2, 1, 3) | |
| v = v.transpose(0, 2, 1, 3) | |
| if self.rope is not None: | |
| 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) | |
| if self.use_gqa_gate: | |
| out = out * mx.sigmoid(self.g_proj(x)) | |
| return self.o_proj(out) | |
| # -------------------------------------------------------------------------- | |
| # MoE | |
| # -------------------------------------------------------------------------- | |
| class SolarMLP(nn.Module): | |
| def __init__( | |
| self, | |
| args: ModelArgs, | |
| hidden_size: Optional[int] = None, | |
| intermediate_size: Optional[int] = None, | |
| ): | |
| super().__init__() | |
| dim = hidden_size or args.hidden_size | |
| hidden = intermediate_size or args.intermediate_size | |
| self.gate_proj = nn.Linear(dim, hidden, bias=False) | |
| self.up_proj = nn.Linear(dim, hidden, bias=False) | |
| self.down_proj = nn.Linear(hidden, dim, bias=False) | |
| def __call__(self, x: mx.array) -> mx.array: | |
| return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x))) | |
| def _expert_select( | |
| gates: mx.array, | |
| bias: Optional[mx.array], | |
| top_k: int, | |
| n_group: int, | |
| topk_group: int, | |
| routed_scaling_factor: float, | |
| renormalize: bool, | |
| ) -> Tuple[mx.array, mx.array]: | |
| """Sigmoid router with optional grouped top-k (SolarOpen2TopkRouter). | |
| Selection uses bias-adjusted scores; the returned weights are the raw | |
| sigmoid scores at the selected experts (bias only affects *which* experts | |
| are picked), optionally renormalized (``norm_topk_prob``) and scaled by | |
| ``routed_scaling_factor``. | |
| """ | |
| scores = mx.sigmoid(gates) | |
| orig_scores = scores | |
| if bias is not None: | |
| scores = scores + bias.astype(scores.dtype) | |
| if n_group > 1: | |
| scores = mx.unflatten(scores, axis=-1, shape=(n_group, -1)) | |
| group_scores = mx.topk(scores, 2, axis=-1).sum(axis=-1, keepdims=True) | |
| k = n_group - topk_group | |
| group_idx = mx.argpartition(group_scores, kth=k - 1, axis=-2)[..., :k, :] | |
| # HF masks dropped groups with -inf (masked_fill), not 0.0: with a | |
| # negative e_score_correction_bias an unmasked adjusted score can be | |
| # < 0, and a 0.0 mask would then pick a MASKED expert HF never picks | |
| # (attacks/attack6 A6a). | |
| scores = mx.put_along_axis( | |
| scores, | |
| mx.stop_gradient(group_idx), | |
| mx.array(-float("inf"), dtype=scores.dtype), | |
| axis=-2, | |
| ) | |
| scores = mx.flatten(scores, -2, -1) | |
| inds = mx.argpartition(-scores, kth=top_k - 1, axis=-1)[..., :top_k] | |
| weights = mx.take_along_axis(orig_scores, inds, axis=-1) | |
| if top_k > 1 and renormalize: | |
| weights = weights / (weights.sum(axis=-1, keepdims=True) + 1e-20) | |
| return inds, weights * routed_scaling_factor | |
| class SolarSparseMoE(nn.Module): | |
| """Routed MoE + shared expert. | |
| Naming contract (kept identical to kimi_linear so alis-dwq's hooks match): | |
| * ``gate`` -- router Linear; module path ``...mlp.gate`` matches | |
| alis-dwq's ``(?:^|\.)(?:gate|router)$`` router regex | |
| * ``switch_mlp`` -- SwitchGLU instance (expert_traffic hooks the class) | |
| * ``shared_experts`` | |
| * ``e_score_correction_bias`` lives on this module (not on ``gate``) so | |
| an 8-bit quantization of ``gate`` cannot drop it; ``sanitize`` moves | |
| the HF key ``...mlp.gate.e_score_correction_bias`` here. | |
| """ | |
| def __init__(self, args: ModelArgs): | |
| super().__init__() | |
| self.args = args | |
| hidden = args.hidden_size | |
| experts = args.n_routed_experts | |
| self.gate = nn.Linear(hidden, experts, bias=False) | |
| self.switch_mlp = SwitchGLU(hidden, args.moe_intermediate_size, experts) | |
| self.e_score_correction_bias = mx.zeros((experts,), dtype=mx.float32) | |
| if args.n_shared_experts: | |
| shared_hidden = args.moe_intermediate_size * args.n_shared_experts | |
| self.shared_experts = SolarMLP(args, intermediate_size=shared_hidden) | |
| else: | |
| self.shared_experts = None | |
| def __call__(self, x: mx.array) -> mx.array: | |
| # HF computes the router logits in float32: | |
| # F.linear(hidden_states.float(), gate.weight.float()) | |
| # Mirror that exactly — a bf16 matmul's reduction-order noise flips | |
| # razor-tie routing decisions (attacks/attack1b: 7/64 tokens flipped | |
| # vs HF f32 logits; both-sides-f32 gives 0/64). A quantized router | |
| # (packed uint32 weight) cannot upcast, so that path keeps the | |
| # activation-dtype matmul with an f32 cast of the logits. | |
| if hasattr(self.gate, "bits"): | |
| gates = self.gate(x).astype(mx.float32) | |
| else: | |
| gates = x.astype(mx.float32) @ self.gate.weight.astype(mx.float32).T | |
| inds, weights = _expert_select( | |
| gates, | |
| self.e_score_correction_bias, | |
| self.args.num_experts_per_tok, | |
| self.args.n_group, | |
| self.args.topk_group, | |
| self.args.routed_scaling_factor, | |
| self.args.norm_topk_prob, | |
| ) | |
| out = self.switch_mlp(x, inds) | |
| # HF casts the weighted expert sum back to the activation dtype. | |
| out = (out * weights[..., None]).sum(axis=-2).astype(x.dtype) | |
| if self.shared_experts is not None: | |
| out = out + self.shared_experts(x) | |
| return out | |
| class SolarDecoderLayer(nn.Module): | |
| def __init__(self, args: ModelArgs, layer_idx: int): | |
| super().__init__() | |
| self.is_linear = args.layer_types[layer_idx] == "linear_attention" | |
| if self.is_linear: | |
| self.self_attn = SolarDeltaAttention(args, layer_idx) | |
| else: | |
| self.self_attn = SolarFullAttention(args, layer_idx) | |
| if layer_idx >= args.first_k_dense_replace: | |
| self.mlp = SolarSparseMoE(args) | |
| else: | |
| self.mlp = SolarMLP(args) | |
| self.input_layernorm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps) | |
| self.post_attention_layernorm = nn.RMSNorm( | |
| args.hidden_size, eps=args.rms_norm_eps | |
| ) | |
| def __call__( | |
| self, | |
| x: mx.array, | |
| mask: Optional[mx.array] = None, | |
| cache: Optional[Any] = None, | |
| ) -> mx.array: | |
| y = self.self_attn(self.input_layernorm(x), mask, cache) | |
| h = x + y | |
| z = self.mlp(self.post_attention_layernorm(h)) | |
| return h + z | |
| class SolarOpen2Model(nn.Module): | |
| def __init__(self, args: ModelArgs): | |
| super().__init__() | |
| self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size) | |
| self.layers = [SolarDecoderLayer(args, i) for i in range(args.num_hidden_layers)] | |
| self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps) | |
| self.ssm_idx = None | |
| self.attn_idx = None | |
| for i, layer in enumerate(self.layers): | |
| if layer.is_linear: | |
| self.ssm_idx = i | |
| break | |
| for i, layer in enumerate(self.layers): | |
| if not layer.is_linear: | |
| self.attn_idx = i | |
| break | |
| def __call__( | |
| self, | |
| inputs: mx.array, | |
| cache: Optional[List[Any]] = None, | |
| ) -> mx.array: | |
| h = self.embed_tokens(inputs) | |
| if cache is None: | |
| cache = [None] * len(self.layers) | |
| ssm_mask = ( | |
| create_ssm_mask(h, cache[self.ssm_idx]) | |
| if self.ssm_idx is not None | |
| else None | |
| ) | |
| attn_mask = ( | |
| create_attention_mask(h, cache[self.attn_idx]) | |
| if self.attn_idx is not None | |
| else None | |
| ) | |
| for layer, layer_cache in zip(self.layers, cache): | |
| mask = ssm_mask if layer.is_linear else attn_mask | |
| h = layer(h, mask=mask, cache=layer_cache) | |
| return self.norm(h) | |
| def sanitize_layer_weights( | |
| weights: Dict[str, mx.array], | |
| prefix: str, | |
| layer: "SolarDecoderLayer", | |
| num_experts: int, | |
| ) -> Dict[str, mx.array]: | |
| """In-place HF -> MLX key conversion for one decoder layer. | |
| Handles both on-disk expert layouts: | |
| * per-expert tensors (the actual upstage/Solar-Open2-250B checkpoint): | |
| ``{prefix}.mlp.experts.{i}.{gate,up,down}_proj.weight`` -> stacked | |
| ``switch_mlp.{gate,up,down}_proj.weight``; | |
| * fused 3D tensors (module layout of the HF modeling code): | |
| ``experts.gate_up_proj`` (E, 2I, H) split into gate/up, and | |
| ``experts.down_proj`` renamed. | |
| """ | |
| if isinstance(layer.mlp, SolarSparseMoE): | |
| mlp_prefix = f"{prefix}.mlp" | |
| if f"{mlp_prefix}.experts.0.gate_proj.weight" in weights: | |
| # Per-expert layout: refuse silently-misconverted pre-quantized | |
| # tensors (scales/biases companions would be left behind and the | |
| # stacked uint32 "weights" would be nonsense). | |
| if any( | |
| k.startswith(f"{mlp_prefix}.experts.") | |
| and (k.endswith(".scales") or k.endswith(".biases")) | |
| for k in weights | |
| ): | |
| raise NotImplementedError( | |
| "Pre-quantized HF expert tensors are not supported by " | |
| "this port; load the BF16 checkpoint and quantize " | |
| "with mlx-lm instead." | |
| ) | |
| for name in ("gate_proj", "up_proj", "down_proj"): | |
| stacked = [ | |
| weights.pop(f"{mlp_prefix}.experts.{i}.{name}.weight") | |
| for i in range(num_experts) | |
| ] | |
| weights[f"{mlp_prefix}.switch_mlp.{name}.weight"] = mx.stack(stacked) | |
| else: | |
| if f"{mlp_prefix}.experts.gate_up_proj.scales" in weights: | |
| raise NotImplementedError( | |
| "Pre-quantized HF expert tensors are not supported by " | |
| "this port; load the BF16 checkpoint and quantize " | |
| "with mlx-lm instead." | |
| ) | |
| gu_key = f"{mlp_prefix}.experts.gate_up_proj" | |
| if gu_key in weights: | |
| gu = weights.pop(gu_key) | |
| # mlx#3836: mx.split silently corrupts >2**31-element tensors; | |
| # a fused HF-layout gate_up_proj (E, 2I, H) is 1.56x that for | |
| # this model. Use strided slices instead. | |
| half = gu.shape[1] // 2 | |
| gate_w, up_w = gu[:, :half], gu[:, half:] | |
| weights[f"{mlp_prefix}.switch_mlp.gate_proj.weight"] = mx.contiguous( | |
| gate_w | |
| ) | |
| weights[f"{mlp_prefix}.switch_mlp.up_proj.weight"] = mx.contiguous( | |
| up_w | |
| ) | |
| down_key = f"{mlp_prefix}.experts.down_proj" | |
| if down_key in weights: | |
| weights[f"{mlp_prefix}.switch_mlp.down_proj.weight"] = weights.pop( | |
| down_key | |
| ) | |
| # Router bias lives on the MoE block in MLX (see class note). | |
| bias_key = f"{mlp_prefix}.gate.e_score_correction_bias" | |
| if bias_key in weights: | |
| weights[f"{mlp_prefix}.e_score_correction_bias"] = weights.pop(bias_key) | |
| attn = layer.self_attn | |
| if isinstance(attn, SolarDeltaAttention): | |
| attn_prefix = f"{prefix}.self_attn" | |
| for src_name, dst_name in ( | |
| ("q_conv1d", "q_conv"), | |
| ("k_conv1d", "k_conv"), | |
| ("v_conv1d", "v_conv"), | |
| ): | |
| src_key = f"{attn_prefix}.{src_name}.weight" | |
| if src_key in weights: | |
| w = weights.pop(src_key) | |
| if w.ndim == 3: | |
| # HF depthwise conv weight (C, 1, K) -> | |
| # MLX Conv1d weight (C, K, 1) | |
| w = w.moveaxis(2, 1) | |
| weights[f"{attn_prefix}.{dst_name}.conv.weight"] = w | |
| dt_key = f"{attn_prefix}.dt_bias" | |
| if dt_key in weights and weights[dt_key].ndim > 1: | |
| weights[dt_key] = mx.reshape(weights[dt_key], (-1,)) | |
| return weights | |
| class Model(nn.Module): | |
| def __init__(self, args: ModelArgs): | |
| super().__init__() | |
| self.args = args | |
| self.model_type = args.model_type | |
| self.model = SolarOpen2Model(args) | |
| if args.tie_word_embeddings: | |
| self.lm_head = None | |
| else: | |
| self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False) | |
| def __call__( | |
| self, | |
| inputs: mx.array, | |
| cache: Optional[List[Any]] = None, | |
| ) -> mx.array: | |
| out = self.model(inputs, cache) | |
| if self.lm_head is None: | |
| return self.model.embed_tokens.as_linear(out) | |
| return self.lm_head(out) | |
| def layers(self): | |
| return self.model.layers | |
| def make_cache(self): | |
| caches: List[Any] = [] | |
| for layer in self.layers: | |
| if layer.is_linear: | |
| # [q_conv_state, k_conv_state, v_conv_state, recurrent_state] | |
| caches.append(ArraysCache(size=4)) | |
| else: | |
| caches.append(KVCache()) | |
| return caches | |
| def sanitize(self, weights: Dict[str, mx.array]) -> Dict[str, mx.array]: | |
| # Drop non-persistent HF rope buffers if a checkpoint carries them. | |
| weights = {k: v for k, v in weights.items() if "rotary_emb.inv_freq" not in k} | |
| if self.args.tie_word_embeddings: | |
| weights.pop("lm_head.weight", None) | |
| for layer_idx, layer in enumerate(self.layers): | |
| sanitize_layer_weights( | |
| weights, | |
| f"model.layers.{layer_idx}", | |
| layer, | |
| self.args.n_routed_experts, | |
| ) | |
| return weights | |
| def cast_predicate(self): | |
| def predicate(path: str): | |
| if "e_score_correction_bias" in path: | |
| return False | |
| if path.endswith("A_log") or path.endswith("dt_bias"): | |
| return False | |
| return True | |
| return predicate | |
| def quant_predicate_builder(self, group_size: int, bits: int): | |
| """Per-tensor bit assignment for Solar-Open2. | |
| The CLI global ``bits``/``group_size`` apply to the routed experts | |
| only (they dominate the parameter count, so they alone decide the | |
| model size). Everything else follows the runbook intent — sensitive | |
| paths run one tier up: | |
| - router (``mlp.gate``) → 8b/g64: routing is selection-sensitive | |
| and the tensor is tiny (320 x 4096 per layer). | |
| - KDA low-rank chokepoints (``f_a/f_b/g_a/g_b/b_proj``) → 8b/g64: | |
| these feed the recurrent state, so quantization error compounds | |
| across time steps. | |
| - ``embed_tokens`` / ``lm_head`` → 6b/g64 (8b when the global is 8). | |
| - attention q/k/v/o, GQA elementwise gate (``g_proj``), and the | |
| shared expert → global bits + 2 (capped at 8). | |
| At bits=8/g64 this reproduces the uniform 8-bit map, so the recipe | |
| is a no-op for the reference build. | |
| """ | |
| attn_bits = min(bits + 2, 8) | |
| embed_bits = max(6, min(bits, 8)) | |
| chokepoint = ("f_a_proj", "f_b_proj", "g_a_proj", "g_b_proj") | |
| upper = ("q_proj", "k_proj", "v_proj", "o_proj", "g_proj", "shared_experts") | |
| def predicate(path, _): | |
| if path.endswith("mlp.gate"): | |
| return {"group_size": 64, "bits": 8} | |
| if "switch_mlp" in path: | |
| return True # routed experts take the global CLI values | |
| if any(k in path for k in chokepoint) or path.endswith("b_proj"): | |
| return {"group_size": 64, "bits": 8} | |
| if "embed_tokens" in path or "lm_head" in path: | |
| return {"group_size": 64, "bits": embed_bits} | |
| if any(k in path for k in upper): | |
| return {"group_size": 64, "bits": attn_bits} | |
| return True | |
| return predicate | |