Text Generation
MLX
Safetensors
glm_moe_dsa
apple-silicon
mixture-of-experts
4-bit precision
conversational
Instructions to use pipenetwork/GLM-5.3-MLX-4bit with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use pipenetwork/GLM-5.3-MLX-4bit 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("pipenetwork/GLM-5.3-MLX-4bit") 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 pipenetwork/GLM-5.3-MLX-4bit with Pi:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "pipenetwork/GLM-5.3-MLX-4bit"
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "mlx-lm": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "pipenetwork/GLM-5.3-MLX-4bit" } ] } } }Run Pi
# Start Pi in your project directory: pi
- MLX LM
How to use pipenetwork/GLM-5.3-MLX-4bit with MLX LM:
Generate or start a chat session
# Install MLX LM uv tool install mlx-lm # Interactive chat REPL mlx_lm.chat --model "pipenetwork/GLM-5.3-MLX-4bit"
Run an OpenAI-compatible server
# Install MLX LM uv tool install mlx-lm # Start the server mlx_lm.server --model "pipenetwork/GLM-5.3-MLX-4bit" # Calling the OpenAI-compatible server with curl curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "pipenetwork/GLM-5.3-MLX-4bit", "messages": [ {"role": "user", "content": "Hello"} ] }' - Hermes Agent
How to use pipenetwork/GLM-5.3-MLX-4bit 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 "pipenetwork/GLM-5.3-MLX-4bit"
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 pipenetwork/GLM-5.3-MLX-4bit
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use pipenetwork/GLM-5.3-MLX-4bit with OpenClaw:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "pipenetwork/GLM-5.3-MLX-4bit"
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 "pipenetwork/GLM-5.3-MLX-4bit" \ --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"
| # Copyright © 2025 Apple Inc. | |
| import math | |
| from dataclasses import dataclass | |
| from typing import Any, Dict, Optional | |
| import mlx.core as mx | |
| import mlx.nn as nn | |
| from mlx.nn.layers.distributed import shard_inplace, shard_linear, sum_gradients | |
| 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 CacheList, KVCache | |
| from mlx_lm.models.mla import MultiLinear | |
| from mlx_lm.models.rope_utils import initialize_rope | |
| from mlx_lm.models.switch_layers import SwitchGLU | |
| class ModelArgs(BaseModelArgs): | |
| model_type: str | |
| vocab_size: int | |
| hidden_size: int | |
| index_head_dim: int | |
| index_n_heads: int | |
| index_topk: int | |
| intermediate_size: int | |
| moe_intermediate_size: int | |
| num_hidden_layers: int | |
| num_attention_heads: int | |
| num_key_value_heads: int | |
| n_shared_experts: Optional[int] | |
| n_routed_experts: Optional[int] | |
| routed_scaling_factor: float | |
| kv_lora_rank: int | |
| q_lora_rank: int | |
| qk_rope_head_dim: int | |
| v_head_dim: int | |
| qk_nope_head_dim: int | |
| topk_method: str | |
| scoring_func: str | |
| norm_topk_prob: bool | |
| n_group: int | |
| topk_group: int | |
| num_experts_per_tok: int | |
| moe_layer_freq: int | |
| first_k_dense_replace: int | |
| max_position_embeddings: int | |
| rms_norm_eps: float | |
| rope_parameters: Dict | |
| attention_bias: bool | |
| rope_scaling: Dict = None | |
| rope_theta: Optional[float] = None | |
| indexer_rope_interleave: bool = True | |
| rope_interleave: bool = True | |
| indexer_types: Optional[list] = None | |
| index_topk_freq: int = 1 | |
| index_skip_topk_offset: int = 0 | |
| index_topk_pattern: Optional[Any] = None | |
| num_nextn_predict_layers: int = 0 | |
| mlp_layer_types: Optional[list] = None | |
| def __post_init__(self): | |
| self.rope_scaling = self.rope_parameters | |
| self.rope_theta = self.rope_parameters["rope_theta"] | |
| if self.indexer_types is None: | |
| # Reference schedule: a layer runs its own indexer ("full") iff | |
| # max(i - offset + 1, 0) % freq == 0; the others ("shared") reuse the most recent | |
| # full layer's top-k selection and carry no indexer weights. | |
| f, o = self.index_topk_freq, self.index_skip_topk_offset | |
| self.indexer_types = [ | |
| "full" if max(i - o + 1, 0) % f == 0 else "shared" | |
| for i in range(self.num_hidden_layers) | |
| ] | |
| class Indexer(nn.Module): | |
| def __init__(self, args: ModelArgs): | |
| super().__init__() | |
| self.dim = args.hidden_size | |
| self.n_heads = args.index_n_heads | |
| self.head_dim = args.index_head_dim | |
| self.rope_head_dim = args.qk_rope_head_dim | |
| self.index_topk = args.index_topk | |
| self.q_lora_rank = args.q_lora_rank | |
| self.wq_b = nn.Linear( | |
| self.q_lora_rank, self.n_heads * self.head_dim, bias=False | |
| ) | |
| self.wk = nn.Linear(self.dim, self.head_dim, bias=False) | |
| self.k_norm = nn.LayerNorm(self.head_dim, eps=1e-6) | |
| self.weights_proj = nn.Linear(self.dim, self.n_heads, bias=False) | |
| self.softmax_scale = self.head_dim**-0.5 | |
| self.rope = initialize_rope( | |
| dims=args.qk_rope_head_dim, | |
| base=args.rope_theta, | |
| traditional=args.indexer_rope_interleave, | |
| max_position_embeddings=args.max_position_embeddings, | |
| scaling_config=args.rope_scaling, | |
| ) | |
| def __call__( | |
| self, | |
| x: mx.array, | |
| qr: mx.array, | |
| mask: Optional[mx.array], | |
| cache: Optional[Any] = None, | |
| ): | |
| # Computes top_k indices for attention | |
| b, s, _ = x.shape | |
| q = self.wq_b(qr) | |
| q = q.reshape(b, s, self.n_heads, self.head_dim).swapaxes(1, 2) | |
| k = self.wk(x) | |
| k = self.k_norm(k) | |
| k = mx.reshape(k, (b, 1, s, self.head_dim)) | |
| 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, _ = cache.update_and_fetch(k, mx.zeros([b, 1, s, 0])) | |
| if k.shape[2] <= self.index_topk: | |
| return None | |
| # Scores in float32, as the reference (which also keeps weights_proj in fp32): in bf16 | |
| # keys at the top-k boundary flip. | |
| scores = q.astype(mx.float32) @ k.astype(mx.float32).swapaxes(-1, -2) | |
| scores = mx.maximum(scores, 0) | |
| weights = self.weights_proj(x).astype(mx.float32) * (self.n_heads**-0.5 * self.softmax_scale) | |
| weights = weights.swapaxes(-1, -2)[..., None] | |
| scores = scores * weights | |
| scores = scores.sum(axis=1, keepdims=True) | |
| if mask is not None: | |
| scores = mx.where(mask, scores, -float("inf")) | |
| return mx.argpartition(scores, kth=-self.index_topk, axis=-1)[ | |
| ..., -self.index_topk : | |
| ] | |
| class DeepseekV32Attention(nn.Module): | |
| def __init__(self, config: ModelArgs, layer_idx: int = 0): | |
| super().__init__() | |
| self.config = config | |
| self.hidden_size = config.hidden_size | |
| self.num_heads = config.num_attention_heads | |
| self.max_position_embeddings = config.max_position_embeddings | |
| self.rope_theta = config.rope_theta | |
| self.q_lora_rank = config.q_lora_rank | |
| self.qk_rope_head_dim = config.qk_rope_head_dim | |
| self.kv_lora_rank = config.kv_lora_rank | |
| self.v_head_dim = config.v_head_dim | |
| self.qk_nope_head_dim = config.qk_nope_head_dim | |
| self.q_head_dim = config.qk_nope_head_dim + config.qk_rope_head_dim | |
| self.scale = self.q_head_dim**-0.5 | |
| self.q_a_proj = nn.Linear( | |
| self.hidden_size, self.q_lora_rank, bias=config.attention_bias | |
| ) | |
| self.q_a_layernorm = nn.RMSNorm(self.q_lora_rank, eps=1e-6) | |
| self.q_b_proj = nn.Linear( | |
| self.q_lora_rank, self.num_heads * self.q_head_dim, bias=False | |
| ) | |
| self.kv_a_proj_with_mqa = nn.Linear( | |
| self.hidden_size, | |
| self.kv_lora_rank + self.qk_rope_head_dim, | |
| bias=config.attention_bias, | |
| ) | |
| self.kv_a_layernorm = nn.RMSNorm(self.kv_lora_rank, eps=1e-6) | |
| self.embed_q = MultiLinear( | |
| self.qk_nope_head_dim, self.kv_lora_rank, self.num_heads | |
| ) | |
| self.unembed_out = MultiLinear( | |
| self.kv_lora_rank, self.v_head_dim, self.num_heads | |
| ) | |
| self.o_proj = nn.Linear( | |
| self.num_heads * self.v_head_dim, | |
| self.hidden_size, | |
| bias=config.attention_bias, | |
| ) | |
| if self.config.rope_scaling is not None: | |
| mscale_all_dim = self.config.rope_scaling.get("mscale_all_dim", 0) | |
| if mscale_all_dim: | |
| scaling_factor = self.config.rope_scaling["factor"] | |
| if scaling_factor > 1: | |
| s = 0.1 * mscale_all_dim * math.log(scaling_factor) + 1.0 | |
| self.scale = self.scale * s * s | |
| # "shared" layers carry no indexer weights: they reuse the previous full layer's top-k. | |
| self.indexer = Indexer(config) if config.indexer_types[layer_idx] == "full" else None | |
| self.rope = initialize_rope( | |
| dims=self.qk_rope_head_dim, | |
| base=self.rope_theta, | |
| traditional=True, | |
| max_position_embeddings=self.max_position_embeddings, | |
| scaling_config=self.config.rope_scaling, | |
| ) | |
| def __call__( | |
| self, | |
| x: mx.array, | |
| mask: Optional[mx.array] = None, | |
| cache: Optional[Any] = None, | |
| prev_topk_indices: Optional[mx.array] = None, | |
| ): | |
| B, L, D = x.shape | |
| qr = self.q_a_layernorm(self.q_a_proj(x)) | |
| q = self.q_b_proj(qr) | |
| q = q.reshape(B, L, self.num_heads, self.q_head_dim).transpose(0, 2, 1, 3) | |
| q_nope, q_pe = mx.split(q, [self.qk_nope_head_dim], axis=-1) | |
| compressed_kv = self.kv_a_proj_with_mqa(x) | |
| compressed_kv, k_pe = mx.split(compressed_kv, [self.kv_lora_rank], axis=-1) | |
| k_pe = k_pe.reshape(B, L, 1, self.qk_rope_head_dim).transpose(0, 2, 1, 3) | |
| kv_latent = self.kv_a_layernorm(compressed_kv) | |
| offset = cache[0].offset if cache is not None else 0 | |
| q_pe = self.rope(q_pe, offset) | |
| k_pe = self.rope(k_pe, offset) | |
| kv_latent = mx.expand_dims(kv_latent, axis=1) | |
| if cache is not None: | |
| kv_latent, k_pe = cache[0].update_and_fetch(kv_latent, k_pe) | |
| else: | |
| cache = [None] * 2 | |
| if self.indexer is not None: | |
| topk_indices = self.indexer(x, qr, mask, cache=cache[1]) | |
| else: | |
| topk_indices = prev_topk_indices | |
| if topk_indices is not None: | |
| if L == 1: | |
| idx = topk_indices[:, :, 0, :, None] | |
| kv_latent = mx.take_along_axis( | |
| kv_latent, | |
| mx.broadcast_to(idx, idx.shape[:-1] + (kv_latent.shape[-1],)), | |
| axis=2, | |
| ) | |
| k_pe = mx.take_along_axis( | |
| k_pe, | |
| mx.broadcast_to(idx, idx.shape[:-1] + (k_pe.shape[-1],)), | |
| axis=2, | |
| ) | |
| if mask is not None: | |
| mask = mx.take_along_axis(mask, topk_indices, axis=-1) | |
| else: | |
| shape = list(topk_indices.shape) | |
| shape[-1] = kv_latent.shape[2] | |
| sparse_mask = mx.zeros(shape, dtype=mx.bool_) | |
| sparse_mask = mx.put_along_axis( | |
| sparse_mask, topk_indices, mx.array(True), axis=-1 | |
| ) | |
| if mask is not None: | |
| sparse_mask = sparse_mask & mask | |
| mask = sparse_mask | |
| # Ensure the indexer cache is evaluated even if the topk_indices are unused | |
| # to keep the graph from getting too large | |
| if self.indexer is not None and cache is not None and cache[0] is not None: | |
| cache[0].keys = mx.depends(cache[0].keys, (cache[1].keys, cache[1].values)) | |
| pe_scores = (q_pe * self.scale) @ k_pe.swapaxes(-1, -2) | |
| if mask is not None: | |
| pe_scores = mx.where( | |
| mask, | |
| pe_scores, | |
| mx.array(mx.finfo(pe_scores.dtype).min, pe_scores.dtype), | |
| ) | |
| if L == 1: | |
| q_nope = self.embed_q(q_nope) | |
| k = v = kv_latent | |
| else: | |
| k = self.embed_q(kv_latent, transpose=False) | |
| v = self.unembed_out(kv_latent) | |
| output = scaled_dot_product_attention( | |
| q_nope, k, v, cache=cache, scale=self.scale, mask=pe_scores | |
| ) | |
| if L == 1: | |
| output = self.unembed_out(output) | |
| output = output.transpose(0, 2, 1, 3).reshape(B, L, -1) | |
| return self.o_proj(output), topk_indices | |
| class DeepseekV32MLP(nn.Module): | |
| def __init__( | |
| self, config: ModelArgs, hidden_size: int = None, intermediate_size: int = None | |
| ): | |
| super().__init__() | |
| self.config = config | |
| self.hidden_size = config.hidden_size if hidden_size is None else hidden_size | |
| self.intermediate_size = ( | |
| config.intermediate_size if intermediate_size is None else intermediate_size | |
| ) | |
| self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) | |
| self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) | |
| self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False) | |
| def __call__(self, x): | |
| down_proj = self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x))) | |
| return down_proj | |
| def group_expert_select( | |
| gates, | |
| e_score_correction_bias, | |
| top_k, | |
| n_group, | |
| topk_group, | |
| routed_scaling_factor, | |
| norm_topk_prob, | |
| ): | |
| scores = mx.sigmoid(gates.astype(mx.float32)) | |
| orig_scores = scores | |
| scores = scores + e_score_correction_bias | |
| 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, :] | |
| scores = mx.put_along_axis( | |
| scores, mx.stop_gradient(group_idx), mx.array(0.0), axis=-2 | |
| ) | |
| scores = mx.flatten(scores, -2, -1) | |
| k = top_k | |
| inds = mx.argpartition(-scores, kth=k - 1, axis=-1)[..., :k] | |
| scores = mx.take_along_axis(orig_scores, inds, axis=-1) | |
| if top_k > 1 and norm_topk_prob: | |
| denominator = scores.sum(axis=-1, keepdims=True) | |
| scores = scores / denominator | |
| scores = scores * routed_scaling_factor | |
| return inds, scores | |
| class MoEGate(nn.Module): | |
| def __init__(self, config: ModelArgs): | |
| super().__init__() | |
| self.config = config | |
| self.top_k = config.num_experts_per_tok | |
| self.norm_topk_prob = config.norm_topk_prob | |
| self.n_routed_experts = config.n_routed_experts | |
| self.routed_scaling_factor = config.routed_scaling_factor | |
| self.n_group = config.n_group | |
| self.topk_group = config.topk_group | |
| self.weight = mx.zeros((self.n_routed_experts, config.hidden_size)) | |
| self.e_score_correction_bias = mx.zeros((self.n_routed_experts,)) | |
| assert config.topk_method == "noaux_tc", "Unsupported topk method." | |
| def __call__(self, x): | |
| return group_expert_select( | |
| x.astype(mx.float32) @ self.weight.astype(mx.float32).T, | |
| self.e_score_correction_bias, | |
| self.top_k, | |
| self.n_group, | |
| self.topk_group, | |
| self.routed_scaling_factor, | |
| self.norm_topk_prob, | |
| ) | |
| class DeepseekV32MoE(nn.Module): | |
| def __init__(self, config: ModelArgs): | |
| super().__init__() | |
| self.config = config | |
| self.num_experts_per_tok = config.num_experts_per_tok | |
| self.switch_mlp = SwitchGLU( | |
| config.hidden_size, | |
| config.moe_intermediate_size, | |
| config.n_routed_experts, | |
| ) | |
| self.gate = MoEGate(config) | |
| if config.n_shared_experts is not None: | |
| intermediate_size = config.moe_intermediate_size * config.n_shared_experts | |
| self.shared_experts = DeepseekV32MLP( | |
| config=config, intermediate_size=intermediate_size | |
| ) | |
| self.sharding_group = None | |
| def __call__(self, x): | |
| if self.sharding_group is not None: | |
| x = sum_gradients(self.sharding_group)(x) | |
| inds, scores = self.gate(x) | |
| y = self.switch_mlp(x, inds) | |
| y = (y * scores[..., None]).sum(axis=-2).astype(y.dtype) | |
| if self.config.n_shared_experts is not None: | |
| y = y + self.shared_experts(x) | |
| if self.sharding_group is not None: | |
| y = mx.distributed.all_sum(y, group=self.sharding_group) | |
| return y | |
| class DeepseekV32DecoderLayer(nn.Module): | |
| def __init__(self, config: ModelArgs, layer_idx: int): | |
| super().__init__() | |
| self.self_attn = DeepseekV32Attention(config, layer_idx) | |
| self.mlp = ( | |
| DeepseekV32MoE(config) | |
| if ( | |
| config.n_routed_experts is not None | |
| and layer_idx >= config.first_k_dense_replace | |
| and layer_idx % config.moe_layer_freq == 0 | |
| ) | |
| else DeepseekV32MLP(config) | |
| ) | |
| self.input_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps) | |
| self.post_attention_layernorm = nn.RMSNorm( | |
| config.hidden_size, eps=config.rms_norm_eps | |
| ) | |
| def __call__( | |
| self, | |
| x: mx.array, | |
| mask: Optional[mx.array] = None, | |
| cache: Optional[Any] = None, | |
| prev_topk_indices: Optional[mx.array] = None, | |
| ): | |
| r, topk_indices = self.self_attn(self.input_layernorm(x), mask, cache, prev_topk_indices) | |
| h = x + r | |
| r = self.mlp(self.post_attention_layernorm(h)) | |
| return h + r, topk_indices | |
| class DeepseekV32Model(nn.Module): | |
| def __init__(self, config: ModelArgs): | |
| super().__init__() | |
| self.vocab_size = config.vocab_size | |
| self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size) | |
| self.layers = [ | |
| DeepseekV32DecoderLayer(config, idx) | |
| for idx in range(config.num_hidden_layers) | |
| ] | |
| self.start_idx = 0 | |
| self.end_idx = len(self.layers) | |
| self.num_layers = self.end_idx | |
| self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps) | |
| self.pipeline_rank = 0 | |
| self.pipeline_size = 1 | |
| def pipeline(self, group): | |
| # Split layers in reverse so rank=0 gets the last layers and | |
| # rank=pipeline_size-1 gets the first | |
| self.pipeline_rank = group.rank() | |
| self.pipeline_size = group.size() | |
| layers_per_rank = len(self.layers) // self.pipeline_size | |
| extra = len(self.layers) - layers_per_rank * self.pipeline_size | |
| if self.pipeline_rank < extra: | |
| layers_per_rank += 1 | |
| self.start_idx = (self.pipeline_size - self.pipeline_rank - 1) * layers_per_rank | |
| self.end_idx = self.start_idx + layers_per_rank | |
| self.layers = self.layers[: self.end_idx] | |
| self.layers[: self.start_idx] = [None] * self.start_idx | |
| self.num_layers = len(self.layers) - self.start_idx | |
| def __call__( | |
| self, | |
| x: mx.array, | |
| cache: Optional[Any] = None, | |
| ) -> mx.array: | |
| h = self.embed_tokens(x) | |
| pipeline_rank = self.pipeline_rank | |
| pipeline_size = self.pipeline_size | |
| if cache is None: | |
| cache = [None] * self.num_layers | |
| mask = create_attention_mask( | |
| h, cache[0][0] if cache[0] else None, return_array=True | |
| ) | |
| # Receive from the previous process in the pipeline | |
| if pipeline_rank < pipeline_size - 1: | |
| h = mx.distributed.recv_like(h, (pipeline_rank + 1)) | |
| # A "shared" layer reuses the top-k selection of the most recent "full" layer. | |
| topk = None | |
| for i in range(self.num_layers): | |
| h, topk = self.layers[self.start_idx + i](h, mask, cache[i], topk) | |
| # Send to the next process in the pipeline | |
| if pipeline_rank != 0: | |
| h = mx.distributed.send(h, (pipeline_rank - 1) % pipeline_size) | |
| if cache[-1] is not None: | |
| cache[-1][0].keys = mx.depends(cache[-1][0].keys, h) | |
| # Broadcast h while keeping it in the graph | |
| if pipeline_size > 1: | |
| h = mx.distributed.all_gather(h)[: h.shape[0]] | |
| return self.norm(h) | |
| class Model(nn.Module): | |
| def __init__(self, config: ModelArgs): | |
| super().__init__() | |
| self.args = config | |
| self.model_type = config.model_type | |
| self.model = DeepseekV32Model(config) | |
| self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) | |
| def __call__( | |
| self, | |
| inputs: mx.array, | |
| cache: Optional[Any] = None, | |
| ): | |
| out = self.model(inputs, cache) | |
| return self.lm_head(out) | |
| def sanitize(self, weights): | |
| # Remove multi-token prediction layers | |
| mpt_layer = self.args.num_hidden_layers | |
| new_weights = {} | |
| for k, v in weights.items(): | |
| parts = k.split(".") | |
| if len(parts) >= 3 and parts[1] == "layers" and int(parts[2]) >= mpt_layer: | |
| continue | |
| new_weights[k] = v | |
| weights = new_weights | |
| def dequant(weight, scale_inv): | |
| dtype = mx.bfloat16 | |
| weight = mx.from_fp8(weight, dtype=mx.bfloat16) | |
| bs = 128 # block size | |
| m, n = weight.shape | |
| pad_bottom = (-m) % bs | |
| pad_side = (-n) % bs | |
| weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side))) | |
| weight = weight.reshape( | |
| ((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs) | |
| ) | |
| weight = (weight * scale_inv[:, None, :, None]).reshape( | |
| m + pad_bottom, n + pad_side | |
| ) | |
| return weight[:m, :n].astype(dtype) | |
| # Dequantize | |
| new_weights = {} | |
| for k, v in weights.items(): | |
| if "weight_scale_inv" in k: | |
| scale_inv = v | |
| wk = k.replace("_scale_inv", "") | |
| weight = weights[wk] | |
| weight = dequant(weight, scale_inv) | |
| new_weights[wk] = weight | |
| elif k not in new_weights: | |
| new_weights[k] = v | |
| weights = new_weights | |
| # Stack experts | |
| for l in range(self.args.num_hidden_layers): | |
| prefix = f"model.layers.{l}" | |
| for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]: | |
| for k in ["weight", "scales", "biases"]: | |
| if f"{prefix}.mlp.experts.0.{m}.{k}" in weights: | |
| to_join = [ | |
| weights.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}") | |
| for e in range(self.args.n_routed_experts) | |
| ] | |
| weights[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join) | |
| prefix = f"model.layers.{l}.self_attn" | |
| if f"{prefix}.kv_b_proj.weight" in weights: | |
| layer = self.model.layers[l].self_attn.embed_q | |
| quantized = f"{prefix}.kv_b_proj.scales" in weights | |
| v = weights.pop(f"{prefix}.kv_b_proj.weight") | |
| head_dim = self.args.qk_nope_head_dim + self.args.v_head_dim | |
| if quantized: | |
| dims = self.args.kv_lora_rank | |
| scales = weights.pop(f"{prefix}.kv_b_proj.scales") | |
| biases = weights.pop(f"{prefix}.kv_b_proj.biases") | |
| # Try to infer bits and group size | |
| bits = (v.shape[-1] * 32) // dims | |
| group_size = dims // scales.shape[-1] | |
| v = mx.dequantize( | |
| v, scales, biases, bits=bits, group_size=group_size | |
| ) | |
| num_heads = self.args.num_attention_heads | |
| v = v.reshape(num_heads, head_dim, -1) | |
| wk = mx.contiguous( | |
| v[:, : self.args.qk_nope_head_dim, :].swapaxes(-1, -2) | |
| ) | |
| wv = mx.contiguous(v[:, self.args.qk_nope_head_dim :, :]) | |
| if quantized: | |
| wk, wk_scales, wk_biases = mx.quantize( | |
| wk, bits=bits, group_size=group_size | |
| ) | |
| wv, wv_scales, wv_biases = mx.quantize( | |
| wv, bits=bits, group_size=group_size | |
| ) | |
| weights[f"{prefix}.embed_q.scales"] = wk_scales | |
| weights[f"{prefix}.unembed_out.scales"] = wv_scales | |
| weights[f"{prefix}.embed_q.biases"] = wk_biases | |
| weights[f"{prefix}.unembed_out.biases"] = wv_biases | |
| weights[f"{prefix}.embed_q.weight"] = wk | |
| weights[f"{prefix}.unembed_out.weight"] = wv | |
| return weights | |
| def shard(self, group: Optional[mx.distributed.Group] = None): | |
| group = group or mx.distributed.init() | |
| N = group.size() | |
| rank = group.rank() | |
| for layer in self.model.layers: | |
| layer.self_attn.q_b_proj = shard_linear( | |
| layer.self_attn.q_b_proj, "all-to-sharded", group=group | |
| ) | |
| layer.self_attn.o_proj = shard_linear( | |
| layer.self_attn.o_proj, "sharded-to-all", group=group | |
| ) | |
| layer.self_attn.num_heads //= N | |
| num_heads = layer.self_attn.num_heads | |
| sh = rank * num_heads | |
| eh = sh + num_heads | |
| def shard_heads(w): | |
| return w[sh:eh] | |
| layer.self_attn.embed_q.apply(shard_heads) | |
| layer.self_attn.unembed_out.apply(shard_heads) | |
| # Shard the MLP | |
| if isinstance(layer.mlp, DeepseekV32MLP): | |
| layer.mlp.gate_proj = shard_linear( | |
| layer.mlp.gate_proj, "all-to-sharded", group=group | |
| ) | |
| layer.mlp.down_proj = shard_linear( | |
| layer.mlp.down_proj, "sharded-to-all", group=group | |
| ) | |
| layer.mlp.up_proj = shard_linear( | |
| layer.mlp.up_proj, "all-to-sharded", group=group | |
| ) | |
| # Shard the MoE. Shard in place since the MoE should be responsible | |
| # for aggregating the results. | |
| else: | |
| layer.mlp.sharding_group = group = group | |
| shard_inplace( | |
| layer.mlp.shared_experts.gate_proj, "all-to-sharded", group=group | |
| ) | |
| shard_inplace( | |
| layer.mlp.shared_experts.down_proj, "sharded-to-all", group=group | |
| ) | |
| shard_inplace( | |
| layer.mlp.shared_experts.up_proj, "all-to-sharded", group=group | |
| ) | |
| shard_inplace( | |
| layer.mlp.switch_mlp.gate_proj, "all-to-sharded", group=group | |
| ) | |
| shard_inplace( | |
| layer.mlp.switch_mlp.down_proj, "sharded-to-all", group=group | |
| ) | |
| shard_inplace( | |
| layer.mlp.switch_mlp.up_proj, "all-to-sharded", group=group | |
| ) | |
| def layers(self): | |
| return self.model.layers[self.model.start_idx : self.model.end_idx] | |
| def cast_predicate(self): | |
| def predicate(k): | |
| return "e_score_correction_bias" not in k | |
| return predicate | |
| def quant_predicate(self): | |
| def predicate(path, module): | |
| # The lightning indexer (0.03% of parameters, reference keeps weights_proj in fp32) and the | |
| # MoE router decide *what* is computed; keep them as stored. | |
| if ".indexer." in path or path.endswith("mlp.gate"): | |
| return False | |
| return True | |
| return predicate | |
| def make_cache(self): | |
| # Shared layers have no indexer, so no indexer key cache: an empty second KVCache would | |
| # trip mlx-lm's `cache.state` evaluation (keys is None) during generation. | |
| return [ | |
| CacheList(KVCache(), KVCache()) if layer.self_attn.indexer is not None else CacheList(KVCache()) | |
| for layer in self.layers | |
| ] | |