Repository navigation
feat(models): add Arcee AFMoE (afmoe) text model support #845
Description
Corrected against
references/mlx-lm(commit15b522f, 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.pyand thearcee-ai/Trinity-Nano-Previewconfig 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 portmlx-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_typesinterleavessliding_attentionandfull_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 aRotatingKVCacheand full layers a standardKVCache, which in this tree means theModelOwnedSequenceState<Cache>wrapper thatsrc/models/gemma3.rsuses, not the simpleLanguageModelcache path. - Full-attention layers are NoPE.
Attention.__init__buildsself.ropeonly whenis_local_attentionis 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 * gatebeforeo_proj. This is an extragate_projweight 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 ish = x + post_attn_norm(attn(input_norm(x)))thenout = 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 bysqrt(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 routingnum_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_ropewithrope_scaling.
Implementation plan
- Reuse the MoE path from
src/models/qwen3_moe.rsand the deepseek MoE decoders (src/models/deepseek_v2.rs,src/models/deepseek_v3.rs), which already implement routed top-k plus shared-expert combination oversrc/models/switch_layers.rs. - Reuse the QK-RMSNorm construction from
src/models/qwen3.rs. - Map
rope_scalingonto 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_weightsconstructs routed experts (viaswitch_layers.rs) plus the shared expert, plus QK-norm. -
sanitizehandles the checkpoint weight layout (stacked vs per-expert experts; standard bf16 to f16; key remapping). - Detection arm added in
src/models/detection.rskeyed on theafmoemodel_type. - Registration in
src/model_metadata.rsviafor_each_model_registration!. - TP/distributed arch-string wired if AFMoE is a target for tensor/pipeline parallel.
- Unit tests in a
_tests.rsfile beside the implementation. -
docs/supported-models.mdupdated with the new family. - Real-checkpoint validation:
./target/release/mlxcel generateproduces coherent output andmlxcel listreports the architecture.
Effort
MEDIUM. Structurally a qwen3_moe/deepseek MoE derivative with QK-norm; contingent on a public checkpoint being available for validation.
Activity
docs(models): document the modelsize / modeltype / arch label taxonomy (
feat(models): add Arcee AFMoE (afmoe) text model support (lablup#1043)
docs(models): document the modelsize / modeltype / arch label taxonomy (
feat(models): add Arcee AFMoE (afmoe) text model support (lablup#1043)