Skip to content

feat(models): add Arcee AFMoE (afmoe) text model support #845

Closed

Description

@inureyes

Corrected against references/mlx-lm (commit 15b522f, 2026-07-11) on 2026-08-05.
The original body describes AFMoE as "structurally a qwen3_moe/deepseek MoE derivative with QK-norm". mlx_lm/models/afmoe.py and the arcee-ai/Trinity-Nano-Preview config carry six features the original body does not mention, several of which change the model class.

Checkpoint confirmed

arcee-ai/Trinity-* is the AFMoE family (model_type: afmoe). Validated targets, in ascending size:

  • mlx-community/Trinity-Nano-Preview-4bit (3.2 GB, 56 layers, hidden 1024, 128 experts) — downloaded, recommended for the port
  • mlx-community/Trinity-Mini-4bit (15.7 GB)

Note arcee-ai/AFM-4.5B is not this family; it is model_type: arcee.

What the original body missed

  • Hybrid sliding/full attention. layer_types interleaves sliding_attention and full_attention (global_attn_every_n_layers: 4, sliding_window: 2048). This makes the port a hybrid-cache model, not a plain MoE decoder: sliding layers need a RotatingKVCache and full layers a standard KVCache, which in this tree means the ModelOwnedSequenceState<Cache> wrapper that src/models/gemma3.rs uses, not the simple LanguageModel cache path.
  • Full-attention layers are NoPE. Attention.__init__ builds self.rope only when is_local_attention is true; global layers apply no positional encoding at all. Applying RoPE everywhere is the obvious mistake and is invisible on short prompts.
  • Sigmoid output gate on attention. gate = mx.sigmoid(self.gate_proj(x)); output = output * gate before o_proj. This is an extra gate_proj weight per attention block, absent from qwen3_moe.
  • Four norms per layer, not two: input_layernorm, post_attention_layernorm, pre_mlp_layernorm, post_mlp_layernorm. The block is h = x + post_attn_norm(attn(input_norm(x))) then out = h + post_mlp_norm(mlp(pre_mlp_norm(h))), a sandwich layout.
  • muP embedding scale. When mup_enabled (true on Trinity), the embedding output is multiplied by sqrt(hidden_size) before the stack.
  • Dense prefix. The first num_dense_layers (2 on Trinity) use a plain MLP; MoE starts at layer 2.

Routing detail, for the record: score_func: sigmoid, a expert_bias added for selection only while the returned weights are gathered from the unbiased scores, then route_norm renormalization and a route_scale multiply (2.826 on Trinity). Group routing exists but n_group: 1 on Trinity, so it never fires there.

Checkpoint weight layout

Experts ship pre-stacked as model.layers.{i}.mlp.experts.{gate,up,down}_proj, which is the layout SwitchGLU::from_weights already consumes directly. There is no per-expert split to write. The router is model.layers.{i}.mlp.router.gate, the selection bias is model.layers.{i}.mlp.expert_bias, and the shared expert is model.layers.{i}.mlp.shared_experts.*.

The Trinity config also carries per-tensor quantization overrides (model.layers.2.mlp.router.gate at 8 bits while the rest is 4-bit), so the loaders must infer per-tensor bit width rather than trusting the top-level bits.


Original issue body (retained for history)

Summary

Add support for the Arcee AFMoE (mixture-of-experts) family to the mlxcel Rust inference runtime, ported from mlx-lm. AFMoE is a MoE decoder with routed experts plus a shared expert, QK-RMSNorm on query/key, and RoPE via initialize_rope with rope_scaling. It maps cleanly onto the existing qwen3_moe / deepseek MoE path plus the qwen3 QK-norm.

Upstream reference

Public checkpoint

  • Unconfirmed. Verify an Arcee AFMoE checkpoint (and an MLX conversion) is publicly available before implementation. If none exists yet, keep this issue in the backlog and note the blocker; a real-checkpoint validation is required to close.

Architecture notes

  • MoE decoder. Routed experts: num_experts (default 128) with top-k routing num_experts_per_tok (default 8).
  • Shared expert: num_shared_experts (default 1) always-on expert whose output is added to the routed-expert combination.
  • QK-RMSNorm: RMSNorm applied to query and key projections (as in Qwen3).
  • RoPE through initialize_rope with rope_scaling.

Implementation plan

  • Reuse the MoE path from src/models/qwen3_moe.rs and the deepseek MoE decoders (src/models/deepseek_v2.rs, src/models/deepseek_v3.rs), which already implement routed top-k plus shared-expert combination over src/models/switch_layers.rs.
  • Reuse the QK-RMSNorm construction from src/models/qwen3.rs.
  • Map rope_scaling onto the existing Llama-family RoPE-scaling handling.
  • Confirm the router normalization and shared-expert scaling match upstream (weight normalization over selected experts, shared-expert add) before trusting parity.

Touchpoints & acceptance criteria

Follow docs/adding-models.md. Done only when the model loads and generates from a real checkpoint.

  • Config struct + serde parse (num_experts, num_experts_per_tok, num_shared_experts, QK-norm flag, rope_scaling).
  • from_weights constructs routed experts (via switch_layers.rs) plus the shared expert, plus QK-norm.
  • sanitize handles the checkpoint weight layout (stacked vs per-expert experts; standard bf16 to f16; key remapping).
  • Detection arm added in src/models/detection.rs keyed on the afmoe model_type.
  • Registration in src/model_metadata.rs via for_each_model_registration!.
  • TP/distributed arch-string wired if AFMoE is a target for tensor/pipeline parallel.
  • Unit tests in a _tests.rs file beside the implementation.
  • docs/supported-models.md updated with the new family.
  • Real-checkpoint validation: ./target/release/mlxcel generate produces coherent output and mlxcel list reports the architecture.

Effort

MEDIUM. Structurally a qwen3_moe/deepseek MoE derivative with QK-norm; contingent on a public checkpoint being available for validation.

Activity

  1. added
    type:enhancementNew features, capabilities, or significant additions
    area:modelsModel architectures, weights, loading, metadata
    on Jul 21, 2026
  2. added
    modelsize:small4-bit checkpoint <= 10GB; fast iteration, safe for smoke tests
    arch:hybridHybrid attention stack (linear/sliding/full or attention+SSM interleave)
    on Aug 5, 2026
  3. added a commit that references this issue on Aug 20, 2026
    f887a92
  4. self-assigned this
    on Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

arch:hybridHybrid attention stack (linear/sliding/full or attention+SSM interleave)area:modelsModel architectures, weights, loading, metadatamodelsize:small4-bit checkpoint <= 10GB; fast iteration, safe for smoke testsmodeltype:textText-only language modelpriority:mediumMedium prioritystatus:doneCompletedtype:enhancementNew features, capabilities, or significant additions

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions