// Package cohere2_moe provides the Cohere2 MoE (Command A family, North) text // model implementation for MLX. // // Architecture notes (matches transformers' Cohere2MoeForCausalLM): // - Parallel residual blocks: a single input layernorm feeds both attention // and the MLP, and their outputs are summed onto the residual. // - Interleaved sliding-window and full attention layers. Sliding layers use // interleaved ("traditional") RoPE; full-attention layers use no positional // encoding (NoPE), except prefix dense layers when // prefix_dense_sliding_window_pattern == 1, which force RoPE. // - The first first_k_dense_replace layers use a dense SwiGLU MLP with // prefix_dense_intermediate_size; the rest are sparse MoE layers routed by // a linear gate with sigmoid or softmax selection over the top-k logits. // - Logits are scaled by logit_scale. Embeddings are tied by default. package cohere2_moe import ( "encoding/json" "fmt" "math" "github.com/ollama/ollama/x/mlxrunner/batch" "github.com/ollama/ollama/x/mlxrunner/cache" "github.com/ollama/ollama/x/mlxrunner/mlx" "github.com/ollama/ollama/x/mlxrunner/model" "github.com/ollama/ollama/x/mlxrunner/model/base" "github.com/ollama/ollama/x/models/nn" "github.com/ollama/ollama/x/tokenizer" ) func init() { base.Register("Cohere2MoeForCausalLM", NewModel) } // Config holds the Cohere2 MoE configuration (HuggingFace config.json). type Config struct { HiddenSize int32 `json:"hidden_size"` NumHiddenLayers int32 `json:"num_hidden_layers"` IntermediateSize int32 `json:"intermediate_size"` NumAttentionHeads int32 `json:"num_attention_heads"` NumKeyValueHeads int32 `json:"num_key_value_heads"` HeadDim int32 `json:"head_dim"` VocabSize int32 `json:"vocab_size"` MaxPositionEmbeddings int32 `json:"max_position_embeddings"` LayerNormEps float32 `json:"layer_norm_eps"` RMSNormEps *float32 `json:"rms_norm_eps"` RopeTheta float32 `json:"rope_theta"` LogitScale float32 `json:"logit_scale"` AttentionBias bool `json:"attention_bias"` TieWordEmbeddings *bool `json:"tie_word_embeddings"` SlidingWindow int32 `json:"sliding_window"` SlidingWindowPattern int32 `json:"sliding_window_pattern"` PrefixDenseSlidingWindowPattern int32 `json:"prefix_dense_sliding_window_pattern"` LayerTypes []string `json:"layer_types"` MLPLayerTypes []string `json:"mlp_layer_types"` FirstKDenseReplace int32 `json:"first_k_dense_replace"` PrefixDenseIntermediateSize int32 `json:"prefix_dense_intermediate_size"` NumExperts int32 `json:"num_experts"` NumExpertsPerTok int32 `json:"num_experts_per_tok"` NumSharedExperts int32 `json:"num_shared_experts"` SharedExpertCombinationStrategy string `json:"shared_expert_combination_strategy"` ExpertSelectionFn string `json:"expert_selection_fn"` NormTopKProb bool `json:"norm_topk_prob"` // Quantization metadata (set at load, not from config.json). QuantGroupSize int `json:"-"` QuantBits int `json:"-"` QuantMode string `json:"-"` TensorQuant map[string]*model.TensorQuantInfo `json:"-"` // Computed fields. Scale float32 `json:"-"` } // normLayer abstracts the per-config choice between RMSNorm (rms_norm_eps set) // and Cohere-style bias-free LayerNorm. type normLayer interface { Forward(x *mlx.Array) *mlx.Array } type rmsNorm struct { Weight *mlx.Array Eps float32 } func (n *rmsNorm) Forward(x *mlx.Array) *mlx.Array { return mlx.RMSNormFn(x, n.Weight, n.Eps) } type layerNorm struct { Weight *mlx.Array Eps float32 } func (n *layerNorm) Forward(x *mlx.Array) *mlx.Array { return mlx.LayerNormFn(x, n.Weight, nil, n.Eps) } // Model is the Cohere2 MoE model. type Model struct { EmbedTokens nn.EmbeddingLayer Layers []*Layer Norm normLayer LMHead nn.LinearLayer tok *tokenizer.Tokenizer *Config } // Layer is a parallel-residual transformer block. type Layer struct { InputNorm normLayer Attention *Attention MLP MLPBlock IsSliding bool UseRope bool } // Attention implements Cohere2 attention (no q/k norm). type Attention struct { QProj nn.LinearLayer KProj nn.LinearLayer VProj nn.LinearLayer OProj nn.LinearLayer } // MLPBlock is the feed-forward interface for dense and MoE blocks. type MLPBlock interface { Forward(x *mlx.Array, cfg *Config) *mlx.Array } // DenseMLP is a SwiGLU feed-forward block. type DenseMLP struct { GateProj nn.LinearLayer UpProj nn.LinearLayer DownProj nn.LinearLayer } // SparseMoE routes each token to the top-k of NumExperts expert MLPs. type SparseMoE struct { Router nn.LinearLayer SwitchMLP *SwitchMLP SharedExpert *DenseMLP } // SwitchMLP executes the selected expert MLPs with stacked expert weights. type SwitchMLP struct { GateWeight *mlx.Array UpWeight *mlx.Array DownWeight *mlx.Array GateWeightQ, GateScales, GateBiases *mlx.Array UpWeightQ, UpScales, UpBiases *mlx.Array DownWeightQ, DownScales, DownBiases *mlx.Array GateBits, UpBits, DownBits int GateGroupSize, UpGroupSize, DownGroupSize int GateMode, UpMode, DownMode string UseQuantized bool } type stackedExpertWeights struct { Weight *mlx.Array Scales *mlx.Array Biases *mlx.Array Bits int GroupSize int Mode string } func parseConfig(configData []byte) (Config, error) { var raw map[string]json.RawMessage if err := json.Unmarshal(configData, &raw); err != nil { return Config{}, fmt.Errorf("parse config envelope: %w", err) } var cfg Config if err := json.Unmarshal(configData, &cfg); err != nil { return Config{}, fmt.Errorf("parse config: %w", err) } if cfg.HiddenSize <= 0 { return Config{}, fmt.Errorf("invalid hidden_size: %d", cfg.HiddenSize) } if cfg.NumHiddenLayers <= 0 { return Config{}, fmt.Errorf("invalid num_hidden_layers: %d", cfg.NumHiddenLayers) } if cfg.NumAttentionHeads <= 0 { return Config{}, fmt.Errorf("invalid num_attention_heads: %d", cfg.NumAttentionHeads) } if cfg.NumKeyValueHeads <= 0 { cfg.NumKeyValueHeads = cfg.NumAttentionHeads } if cfg.HeadDim <= 0 { if cfg.HiddenSize%cfg.NumAttentionHeads != 0 { return Config{}, fmt.Errorf("hidden_size (%d) must be divisible by num_attention_heads (%d)", cfg.HiddenSize, cfg.NumAttentionHeads) } cfg.HeadDim = cfg.HiddenSize / cfg.NumAttentionHeads } // Defaults follow transformers' Cohere2MoeConfig. if cfg.LayerNormEps == 0 { cfg.LayerNormEps = 1e-5 } if cfg.RopeTheta == 0 { cfg.RopeTheta = 10000 } if cfg.LogitScale == 0 { cfg.LogitScale = 0.0625 } if _, ok := raw["sliding_window"]; !ok { cfg.SlidingWindow = 4096 } if cfg.SlidingWindowPattern <= 0 { cfg.SlidingWindowPattern = 4 } if cfg.PrefixDenseSlidingWindowPattern <= 0 { cfg.PrefixDenseSlidingWindowPattern = 1 } if cfg.MaxPositionEmbeddings <= 0 { cfg.MaxPositionEmbeddings = 8192 } if cfg.NumExperts <= 0 { cfg.NumExperts = 8 } if cfg.NumExpertsPerTok <= 0 { cfg.NumExpertsPerTok = 2 } if cfg.NumExpertsPerTok > cfg.NumExperts { return Config{}, fmt.Errorf("num_experts_per_tok (%d) exceeds num_experts (%d)", cfg.NumExpertsPerTok, cfg.NumExperts) } if cfg.ExpertSelectionFn == "" { cfg.ExpertSelectionFn = "softmax" } if cfg.ExpertSelectionFn != "softmax" && cfg.ExpertSelectionFn != "sigmoid" { return Config{}, fmt.Errorf("unsupported expert_selection_fn: %q", cfg.ExpertSelectionFn) } if cfg.SharedExpertCombinationStrategy == "" { cfg.SharedExpertCombinationStrategy = "average" } if cfg.SharedExpertCombinationStrategy != "average" && cfg.SharedExpertCombinationStrategy != "sum" { return Config{}, fmt.Errorf("unsupported shared_expert_combination_strategy: %q", cfg.SharedExpertCombinationStrategy) } if _, ok := raw["norm_topk_prob"]; !ok { cfg.NormTopKProb = true } if cfg.PrefixDenseIntermediateSize <= 0 { cfg.PrefixDenseIntermediateSize = cfg.IntermediateSize } // Derive per-layer attention types when absent: the first // first_k_dense_replace layers follow prefix_dense_sliding_window_pattern, // the rest follow sliding_window_pattern (full attention every Nth layer). if len(cfg.LayerTypes) == 0 { cfg.LayerTypes = make([]string, cfg.NumHiddenLayers) for i := range cfg.NumHiddenLayers { if i < cfg.FirstKDenseReplace { cfg.LayerTypes[i] = patternLayerType(i, cfg.PrefixDenseSlidingWindowPattern) } else { cfg.LayerTypes[i] = patternLayerType(i-cfg.FirstKDenseReplace, cfg.SlidingWindowPattern) } } } if len(cfg.LayerTypes) != int(cfg.NumHiddenLayers) { return Config{}, fmt.Errorf("layer_types has %d entries, want %d", len(cfg.LayerTypes), cfg.NumHiddenLayers) } // Derive per-layer MLP types when absent: the first first_k_dense_replace // layers are dense, the rest sparse. if len(cfg.MLPLayerTypes) == 0 { cfg.MLPLayerTypes = make([]string, cfg.NumHiddenLayers) for i := range cfg.NumHiddenLayers { if i < cfg.FirstKDenseReplace { cfg.MLPLayerTypes[i] = "dense" } else { cfg.MLPLayerTypes[i] = "sparse" } } } if len(cfg.MLPLayerTypes) != int(cfg.NumHiddenLayers) { return Config{}, fmt.Errorf("mlp_layer_types has %d entries, want %d", len(cfg.MLPLayerTypes), cfg.NumHiddenLayers) } cfg.Scale = float32(1.0 / math.Sqrt(float64(cfg.HeadDim))) return cfg, nil } func patternLayerType(i, pattern int32) string { if pattern > 0 && (i+1)%pattern == 0 { return "full_attention" } return "sliding_attention" } func (cfg *Config) layerIsSliding(i int32) bool { return cfg.LayerTypes[i] == "sliding_attention" } func (cfg *Config) layerIsDense(i int32) bool { return cfg.MLPLayerTypes[i] == "dense" } // layerUsesRope reports whether layer i applies rotary embeddings: all sliding // layers do, and prefix dense layers force RoPE even with full attention when // prefix_dense_sliding_window_pattern == 1 (matching Cohere2MoeAttention's // force_rope). Other full-attention layers use no positional encoding. func (cfg *Config) layerUsesRope(i int32) bool { if cfg.layerIsSliding(i) { return true } return cfg.layerIsDense(i) && cfg.PrefixDenseSlidingWindowPattern == 1 } func (cfg *Config) newNorm(weight *mlx.Array) normLayer { if cfg.RMSNormEps != nil { return &rmsNorm{Weight: weight, Eps: *cfg.RMSNormEps} } return &layerNorm{Weight: weight, Eps: cfg.LayerNormEps} } func (cfg *Config) tieEmbeddings() bool { return cfg.TieWordEmbeddings == nil || *cfg.TieWordEmbeddings } // NewModel creates a Cohere2 MoE model from a manifest root. func NewModel(root *model.Root) (base.Model, error) { configData, err := root.Manifest.ReadConfig("config.json") if err != nil { return nil, fmt.Errorf("load config: %w", err) } cfg, err := parseConfig(configData) if err != nil { return nil, err } if qt := root.QuantType(); qt != "" { cfg.QuantGroupSize, cfg.QuantBits, cfg.QuantMode = model.QuantizationParams(qt) if gs := root.GroupSize(); gs > 0 { cfg.QuantGroupSize = gs } } else { cfg.QuantGroupSize, cfg.QuantBits, cfg.QuantMode = model.QuantizationParams("") } cfg.TensorQuant = root.AllTensorQuant() tokData, err := root.Manifest.ReadConfig("tokenizer.json") if err != nil { return nil, fmt.Errorf("load tokenizer config: %w", err) } tokConfig := &tokenizer.TokenizerConfig{ConfigJSON: configData} if genConfigData, err := root.Manifest.ReadConfig("generation_config.json"); err == nil { tokConfig.GenerationConfigJSON = genConfigData } if tokConfigData, err := root.Manifest.ReadConfig("tokenizer_config.json"); err == nil { tokConfig.TokenizerConfigJSON = tokConfigData } tok, err := tokenizer.LoadFromBytesWithConfig(tokData, tokConfig) if err != nil { return nil, fmt.Errorf("parse tokenizer: %w", err) } m := &Model{ Layers: make([]*Layer, cfg.NumHiddenLayers), Config: &cfg, tok: tok, } for i := range cfg.NumHiddenLayers { m.Layers[i] = &Layer{ IsSliding: cfg.layerIsSliding(i), UseRope: cfg.layerUsesRope(i), } } return m, nil } func supportsGatherQMM(mode string, bits int) bool { switch mode { case "affine": return bits == 4 || bits == 8 case "mxfp8": return bits == 8 case "nvfp4", "mxfp4": return bits == 4 default: return false } } // transposeExpertWeightForGatherMM converts stacked [E, out, in] expert // weights to the [E, in, out] layout GatherMM consumes, materialized once at // load so the forward path avoids per-call transposes. func transposeExpertWeightForGatherMM(w *mlx.Array) *mlx.Array { if w == nil || !w.Valid() || w.NumDims() != 3 { return w } t := mlx.Transpose(w, 0, 2, 1) cloned := t.Clone() mlx.Eval(cloned) return cloned } // loadStackedProjection returns expert weights already stacked as a single 3D // tensor (layers.N.mlp.switch_mlp..weight) — the layout `ollama create` // writes when it packs per-expert tensors at import. func loadStackedProjection(tensors map[string]*mlx.Array, cfg *Config, useQuantized bool, base string) *stackedExpertWeights { key := base + ".weight" w := tensors[key] if w == nil { return nil } scales := tensors[key+"_scale"] if scales == nil { return &stackedExpertWeights{Weight: w} } qbiases := tensors[key+"_qbias"] groupSize, bits, mode := model.ResolveLinearQuantParams( cfg.QuantGroupSize, cfg.QuantBits, cfg.QuantMode, cfg.TensorQuant, key, w, scales, ) if useQuantized && supportsGatherQMM(mode, bits) { return &stackedExpertWeights{ Weight: w, Scales: scales, Biases: qbiases, Bits: bits, GroupSize: groupSize, Mode: mode, } } return &stackedExpertWeights{ Weight: mlx.Dequantize(w, scales, qbiases, groupSize, bits, mode), Bits: bits, GroupSize: groupSize, Mode: mode, } } // LoadWeights assigns tensors to model fields. func (m *Model) LoadWeights(tensors map[string]*mlx.Array) error { cfg := m.Config linears := model.NewLinearFactory(tensors, cfg.QuantGroupSize, cfg.QuantBits, cfg.QuantMode, cfg.TensorQuant) embedTokens := model.MakeEmbeddingLayer(tensors, "model.embed_tokens", cfg.QuantGroupSize, cfg.QuantBits, cfg.QuantMode, cfg.TensorQuant) if embedTokens == nil { return fmt.Errorf("missing embedding weight: model.embed_tokens.weight") } m.EmbedTokens = embedTokens normWeight := tensors["model.norm.weight"] if normWeight == nil { return fmt.Errorf("missing final norm weight: model.norm.weight") } m.Norm = cfg.newNorm(normWeight) if cfg.tieEmbeddings() { m.LMHead = m.EmbedTokens.AsLinear() } else if lmHead := linears.Make("lm_head"); lmHead != nil { m.LMHead = lmHead } else { m.LMHead = m.EmbedTokens.AsLinear() } useQuantizedExperts := supportsGatherQMM(cfg.QuantMode, cfg.QuantBits) if !useQuantizedExperts && cfg.TensorQuant != nil { for _, tq := range cfg.TensorQuant { if tq == nil { continue } _, bits, mode := model.QuantizationParams(tq.QuantType) if supportsGatherQMM(mode, bits) { useQuantizedExperts = true break } } } for i := range cfg.NumHiddenLayers { layerPrefix := fmt.Sprintf("model.layers.%d", i) layer := &Layer{ IsSliding: cfg.layerIsSliding(i), UseRope: cfg.layerUsesRope(i), } normWeight := tensors[layerPrefix+".input_layernorm.weight"] if normWeight == nil { return fmt.Errorf("layer %d: missing input_layernorm", i) } layer.InputNorm = cfg.newNorm(normWeight) attn := &Attention{ QProj: linears.Make(layerPrefix + ".self_attn.q_proj"), KProj: linears.Make(layerPrefix + ".self_attn.k_proj"), VProj: linears.Make(layerPrefix + ".self_attn.v_proj"), OProj: linears.Make(layerPrefix + ".self_attn.o_proj"), } if attn.QProj == nil || attn.KProj == nil || attn.VProj == nil || attn.OProj == nil { return fmt.Errorf("layer %d: missing attention projections", i) } layer.Attention = attn if cfg.layerIsDense(i) { mlp := &DenseMLP{ GateProj: linears.Make(layerPrefix + ".mlp.gate_proj"), UpProj: linears.Make(layerPrefix + ".mlp.up_proj"), DownProj: linears.Make(layerPrefix + ".mlp.down_proj"), } if mlp.GateProj == nil || mlp.UpProj == nil || mlp.DownProj == nil { return fmt.Errorf("layer %d: missing dense mlp projections", i) } layer.MLP = mlp } else { moe := &SparseMoE{} moe.Router = linears.Make(layerPrefix + ".mlp.gate") if moe.Router == nil { return fmt.Errorf("layer %d: missing moe router gate", i) } gateW := loadStackedProjection(tensors, cfg, useQuantizedExperts, layerPrefix+".mlp.switch_mlp.gate_proj") upW := loadStackedProjection(tensors, cfg, useQuantizedExperts, layerPrefix+".mlp.switch_mlp.up_proj") downW := loadStackedProjection(tensors, cfg, useQuantizedExperts, layerPrefix+".mlp.switch_mlp.down_proj") if gateW == nil || upW == nil || downW == nil { return fmt.Errorf("layer %d: missing stacked switch_mlp expert weights (import the model with `ollama create`)", i) } switchMLP := &SwitchMLP{} if gateW.Scales != nil && upW.Scales != nil && downW.Scales != nil { switchMLP.UseQuantized = true switchMLP.GateWeightQ = gateW.Weight switchMLP.GateScales = gateW.Scales switchMLP.GateBiases = gateW.Biases switchMLP.GateBits = gateW.Bits switchMLP.GateGroupSize = gateW.GroupSize switchMLP.GateMode = gateW.Mode switchMLP.UpWeightQ = upW.Weight switchMLP.UpScales = upW.Scales switchMLP.UpBiases = upW.Biases switchMLP.UpBits = upW.Bits switchMLP.UpGroupSize = upW.GroupSize switchMLP.UpMode = upW.Mode switchMLP.DownWeightQ = downW.Weight switchMLP.DownScales = downW.Scales switchMLP.DownBiases = downW.Biases switchMLP.DownBits = downW.Bits switchMLP.DownGroupSize = downW.GroupSize switchMLP.DownMode = downW.Mode } else { switchMLP.GateWeight = transposeExpertWeightForGatherMM(gateW.Weight) switchMLP.UpWeight = transposeExpertWeightForGatherMM(upW.Weight) switchMLP.DownWeight = transposeExpertWeightForGatherMM(downW.Weight) } moe.SwitchMLP = switchMLP if cfg.NumSharedExperts > 0 { shared := &DenseMLP{ GateProj: linears.Make(layerPrefix + ".mlp.shared_experts.gate_proj"), UpProj: linears.Make(layerPrefix + ".mlp.shared_experts.up_proj"), DownProj: linears.Make(layerPrefix + ".mlp.shared_experts.down_proj"), } if shared.GateProj == nil { shared.GateProj = linears.Make(layerPrefix + ".mlp.shared_expert.gate_proj") shared.UpProj = linears.Make(layerPrefix + ".mlp.shared_expert.up_proj") shared.DownProj = linears.Make(layerPrefix + ".mlp.shared_expert.down_proj") } if shared.GateProj == nil || shared.UpProj == nil || shared.DownProj == nil { return fmt.Errorf("layer %d: missing shared expert projections", i) } moe.SharedExpert = shared } layer.MLP = moe } m.Layers[i] = layer } return nil } func (a *Attention) Forward(x *mlx.Array, b *batch.Batch, c cache.Cache, positions *mlx.Array, B, L int32, useRope bool, cfg *Config) *mlx.Array { q := a.QProj.Forward(x) k := a.KProj.Forward(x) v := a.VProj.Forward(x) q = mlx.Transpose(mlx.Reshape(q, B, L, cfg.NumAttentionHeads, cfg.HeadDim), 0, 2, 1, 3) k = mlx.Transpose(mlx.Reshape(k, B, L, cfg.NumKeyValueHeads, cfg.HeadDim), 0, 2, 1, 3) v = mlx.Transpose(mlx.Reshape(v, B, L, cfg.NumKeyValueHeads, cfg.HeadDim), 0, 2, 1, 3) // Cohere uses interleaved pairs (traditional RoPE). Full-attention layers // outside the forced-RoPE prefix use no positional encoding. if useRope { q = mlx.RoPEWithBase(q, int(cfg.HeadDim), true, cfg.RopeTheta, 1.0, positions) k = mlx.RoPEWithBase(k, int(cfg.HeadDim), true, cfg.RopeTheta, 1.0, positions) } var kv nn.SDPAOption if c != nil { history := c.(cache.Attention).Update(b, k, v) kv = nn.WithKVHistory(history) } else { kv = nn.WithKV(k, v, b.SeqQueryLens) } out := nn.ScaledDotProductAttention(b, q, cfg.Scale, kv, nn.WithMask(nn.CausalMask())) out = mlx.Reshape(mlx.Transpose(out, 0, 2, 1, 3), B, L, cfg.NumAttentionHeads*cfg.HeadDim) return a.OProj.Forward(out) } func (m *DenseMLP) Forward(x *mlx.Array, _ *Config) *mlx.Array { return m.DownProj.Forward(mlx.SwiGLU(m.GateProj.Forward(x), m.UpProj.Forward(x))) } // route selects the top-k experts. Selection happens on the raw router logits // and the activation (sigmoid or softmax) is applied to just the selected // entries, matching Cohere2MoeTopKRouter (both activations are monotonic, so // selection order is unchanged). func (moe *SparseMoE) route(x *mlx.Array, cfg *Config) (inds, scores *mlx.Array) { logits := moe.Router.Forward(x) inds = mlx.Argpartition(mlx.Neg(logits), int(cfg.NumExpertsPerTok)-1, -1) dims := inds.Dims() inds = mlx.SliceStartStop(inds, []int32{0, 0, 0}, []int32{int32(dims[0]), int32(dims[1]), cfg.NumExpertsPerTok}) selected := mlx.TakeAlongAxis(logits, inds, -1) if cfg.ExpertSelectionFn == "sigmoid" { scores = mlx.Sigmoid(selected) if cfg.NormTopKProb && cfg.NumExpertsPerTok > 1 { scores = mlx.Div(scores, mlx.Sum(scores, -1, true)) } } else { scores = mlx.SoftmaxAxis(selected, -1, true) } return inds, scores } func (moe *SparseMoE) Forward(x *mlx.Array, cfg *Config) *mlx.Array { dims := x.Dims() B, L := int32(dims[0]), int32(dims[1]) inds, scores := moe.route(x, cfg) expertOut := moe.SwitchMLP.Forward(x, inds, cfg) y := mlx.Sum(mlx.Mul(expertOut, mlx.ExpandDims(scores, -1)), 2, false) if moe.SharedExpert != nil { y = mlx.Add(y, moe.SharedExpert.Forward(x, cfg)) if cfg.SharedExpertCombinationStrategy == "average" { y = mlx.MulScalar(y, 0.5) } } return mlx.Reshape(y, B, L, cfg.HiddenSize) } func (s *SwitchMLP) Forward(x *mlx.Array, indices *mlx.Array, cfg *Config) *mlx.Array { dims := x.Dims() B, L := int32(dims[0]), int32(dims[1]) topK := cfg.NumExpertsPerTok xFlat := mlx.Reshape(x, B*L, 1, 1, cfg.HiddenSize) idxFlat := mlx.Reshape(indices, B*L, topK) // Sorting tokens by expert improves gather matmul locality for prefill // batches; the cost outweighs the benefit for small decode batches. doSort := B*L >= 64 var invOrder *mlx.Array n := B * L * topK if doSort { idxAll := mlx.Flatten(idxFlat) order := mlx.Argsort(idxAll, 0) invOrder = mlx.Argsort(order, 0) xFlat = mlx.ExpandDims(mlx.Take(mlx.Squeeze(xFlat, 1), mlx.FloorDivideScalar(order, topK), 0), 1) idxFlat = mlx.Reshape(mlx.Take(idxAll, order, 0), n, 1) } var gate, up, hidden, down *mlx.Array if s.UseQuantized { gate = mlx.GatherQMM(xFlat, s.GateWeightQ, s.GateScales, s.GateBiases, nil, idxFlat, true, s.GateGroupSize, s.GateBits, s.GateMode, doSort) up = mlx.GatherQMM(xFlat, s.UpWeightQ, s.UpScales, s.UpBiases, nil, idxFlat, true, s.UpGroupSize, s.UpBits, s.UpMode, doSort) hidden = mlx.SwiGLU(gate, up) down = mlx.GatherQMM(hidden, s.DownWeightQ, s.DownScales, s.DownBiases, nil, idxFlat, true, s.DownGroupSize, s.DownBits, s.DownMode, doSort) } else { gate = mlx.GatherMM(xFlat, s.GateWeight, nil, idxFlat, doSort) up = mlx.GatherMM(xFlat, s.UpWeight, nil, idxFlat, doSort) hidden = mlx.SwiGLU(gate, up) down = mlx.GatherMM(hidden, s.DownWeight, nil, idxFlat, doSort) } if doSort { down = mlx.Reshape(mlx.Take(mlx.Squeeze(mlx.Squeeze(down, 2), 1), invOrder, 0), B*L, topK, cfg.HiddenSize) } else { down = mlx.Squeeze(down, 2) } return mlx.Reshape(down, B, L, topK, cfg.HiddenSize) } // Forward runs a parallel-residual block: one shared layernorm feeds both // attention and the MLP, and the residual adds both outputs. func (l *Layer) Forward(x *mlx.Array, b *batch.Batch, c cache.Cache, positions *mlx.Array, B, L int32, cfg *Config) *mlx.Array { normed := l.InputNorm.Forward(x) attnOut := l.Attention.Forward(normed, b, c, positions, B, L, l.UseRope, cfg) mlpOut := l.MLP.Forward(normed, cfg) return mlx.Add(x, mlx.Add(attnOut, mlpOut)) } func (m *Model) Forward(b *batch.Batch, caches []cache.Cache) *mlx.Array { dims := b.InputIDs.Dims() B, L := int32(dims[0]), int32(dims[1]) positions := mlx.FromValues(b.SeqOffsets, len(b.SeqOffsets)) h := m.EmbedTokens.Forward(b.InputIDs) for i, layer := range m.Layers { var c cache.Cache if caches != nil && i < len(caches) { c = caches[i] } h = layer.Forward(h, b, c, positions, B, L, m.Config) } return m.Norm.Forward(h) } func (m *Model) Unembed(x *mlx.Array) *mlx.Array { logits := m.LMHead.Forward(x) if m.LogitScale != 1.0 { logits = mlx.MulScalar(logits, m.LogitScale) } return logits } func (m *Model) NumLayers() int { return len(m.Layers) } func (m *Model) MaxContextLength() int { return int(m.MaxPositionEmbeddings) } func (m *Model) Tokenizer() *tokenizer.Tokenizer { return m.tok } // NewCaches creates per-layer caches: rotating (bounded) caches for sliding // window layers and standard KV caches for full attention layers. func (m *Model) NewCaches() []cache.Cache { caches := make([]cache.Cache, len(m.Layers)) for i, layer := range m.Layers { if m.SlidingWindow > 0 && layer.IsSliding { caches[i] = cache.NewRotatingKVCache(int(m.SlidingWindow)) } else { caches[i] = cache.NewKVCache() } } return caches }