feat: add tract_moe_ffn operator for Mixture-of-Experts FFN - #2084
feat: add tract_moe_ffn operator for Mixture-of-Experts FFN#2084JulienBalianSonos wants to merge 42 commits into
Conversation
9f96279 to
4e1671e
Compare
Implements the tract_moe_ffn operator in the tract_transformers extension, enabling inference of MoE-based models (Mixtral, GPT-OSS, Qwen MoE) exported via torch_to_nnef. The operator encapsulates the full MoE FFN block: - Router: x @ wg.T -> top-k expert selection with softmax gating - Token grouping: batch tokens per expert for efficient GEMM - Expert FFN: SwiGLU (silu(x@w1) * (x@w3)) @ w2 with BLAS-backed matmul - Weighted scatter-add of expert outputs Real conditional compute: unused experts are fully skipped. Handles both 2D [T,D] and 3D [B,S,D] input shapes. Verified bit-exact against PyTorch on TitanML/tiny-mixtral (8 experts, top-2, 246M params).
…clamped-swiglu act_alpha/act_limit; codegen keeps biased/clamped path on reference eval)
…n (no router_logits, no rank-align unsqueeze)
…ax_all|sigmoid|raw); fix stale op unit tests (single-output + gate); regen harness asset
… native dtype, cast output back); compute router in f32 to stabilize top-k selection vs PyTorch; f16-faithful weighted scatter
…rch CPU f32 matmul accumulation; native-f16 accumulation derailed greedy decoding
dccfa06 to
c547fe8
Compare
|
🔴 Bench vs main — 1 speed regression(s) Reference: main nightly, latest 2026-07-09 (0d old) · PR Speed — evaltime · prefill · decode
lower is better except prefill/decode (tok/s) · adaptive thresholds (max(floor, k×noise) vs the series' own history) · single-shot vs nightly reference · full report → run |
The routed Q40 path handles the fully block-quantized case; models that fall back to per-expert subplans still went through them one at a time, both when building and when evaluating. Build: each plan slices its own expert out of the shared weight tensors and optimizes an independent sub-model, which includes prepacking that expert's weights. Serially that is layers x experts optimizations on the model-load critical path, 768 of them for a 24-layer 32-expert model (measured 62s). Spread them over rayon's global pool, as FlashSdpa already does for its heads. Eval: a prompt pass with top-k routing lights up nearly every expert in every layer, and each expert's matmuls are too narrow to fill the machine alone. Run the active experts concurrently and keep the weighted scatter serial and in fixed expert order so the reduction stays deterministic. `OpState` is deliberately not `Send`, so experts can no longer share long-lived pre-spawned states. Expert sub-models are stateless matmuls, so spawn per eval and drop the per-expert states from the op state entirely; only the router keeps one. Measured on a 20B gpt-oss-style export on the subplan path: prepare 62s -> 13s, decode 4.9 -> 7.3 tok/s, output byte-identical.
build_expert_plan upcast every non-block-quant expert weight to f32. For a mixed-precision export that weight is the bulk of the model: gpt-oss keeps the down projection in f16, about 12.7GB of a 22GB export, so the upcast doubled it and cost both prepare time and decode bandwidth. Keep f16 weights as they were exported; the matmul still accumulates in f32. Measured on a 20B gpt-oss export, subplan path: prepare 20.9s -> 12.1s, prefill 7.0s -> 2.8s, decode 5.6 -> 9.0 tok/s, output unchanged.
|
🔴 Bench vs main — 40 speed regression(s) · 12 load/memory Reference: main nightly, latest 2026-07-28 (0d old) · PR Speed — evaltime · prefill · decode
Load & memory (worst first)
🟢 19 improvement(s)
lower is better except prefill/decode (tok/s) · adaptive thresholds (max(floor, k×noise) vs the series' own history) · single-shot vs nightly reference · full report → run |
# Conflicts: # metal/src/kernels/mod.rs # transformers/src/lib.rs # transformers/src/ops/mod.rs
|
🔴 Bench vs main — 1 speed regression(s) · Reference: 2026-08-04 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
83 ms → 120 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
127 ms → 169 ms | |
| speaker_id RSS @ ready · pulse8 |
cortex-a7 |
17.7 MB → 21.5 MB |
|
🔴 Bench vs main — 1 speed regression(s) · Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 102 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 151 ms | |
| arm_ml_kws_cnn_m load · pass |
cortex-a53 |
47 ms → 53 ms | |
| arm_ml_kws_cnn_m load · pass |
cortex-a7 |
80 ms → 89 ms | |
| hey_snips_v1 load · 400ms |
cortex-a7 |
70 ms → 77 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a53 |
73 ms → 80 ms |
|
🔴 Bench vs main — 1 speed regression(s) · Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 99 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 147 ms | |
| arm_ml_kws_cnn_m load · pass |
cortex-a7 |
80 ms → 89 ms | |
| mobilenet_v1_1 RSS @ ready · pass |
cortex-a53 |
48.1 MB → 53.4 MB |
|
🔴 Bench vs main — 2 speed regression(s) · Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 113 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 170 ms | |
| hey_snips_v4_model17 load+optimize · 2sec |
cortex-a7 |
5.12 s → 5.7 s | |
| hey_snips_v4_model17 load · 2sec |
cortex-a7 |
4.39 s → 4.84 s | |
| mobilenet_v1_1 RSS @ ready · pass |
cortex-a53 |
48.1 MB → 52.7 MB | |
| mobilenet_v1_1 load · pass |
cortex-a9 |
2.09 s → 2.25 s | |
| mobilenet_v1_1 load+optimize · pass |
cortex-a9 |
2.37 s → 2.54 s |
|
🔴 Bench vs main — 2 speed regression(s) · Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 121 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 179 ms | |
| mobilenet_v1_1 RSS @ ready · pass |
cortex-a53 |
48.1 MB → 53.2 MB | |
| en_tdnn_15M RSS @ ready · pulse_120ms |
cortex-a55 |
112 MB → 120 MB | |
| arm_ml_kws_cnn_m load · pass |
cortex-a53 |
47 ms → 50 ms |
|
🔴 Bench vs main — 3 speed regression(s) · Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 117 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 168 ms | |
| hey_snips_v1 load · 400ms |
cortex-a9 |
77 ms → 92 ms | |
| hey_snips_v1 load+optimize · 400ms |
cortex-a9 |
147 ms → 172 ms | |
| hey_snips_v4_model17_nnef load · pulse8 |
cortex-a7 |
876 ms → 990 ms | |
| mobilenet_v1_1 load · pass |
cortex-a9 |
2.09 s → 2.28 s | |
| hey_snips_v4_model17_nnef load+optimize · pulse8 |
cortex-a7 |
1.55 s → 1.69 s | |
| mobilenet_v1_1 load+optimize · pass |
cortex-a9 |
2.37 s → 2.58 s | |
| inceptionv3 load+optimize · pass |
apple-m1-max |
350 ms → 371 ms | |
| hey_snips_v31 load+optimize · 400ms |
cortex-a9 |
344 ms → 363 ms | |
| inceptionv3 load · pass |
apple-m1-max |
237 ms → 250 ms | |
| en_tdnn_15M RSS @ ready · 2600ms |
cortex-a55 |
112 MB → 118 MB |
|
🔴 Bench vs main — 2 speed regression(s) · Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 99 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 146 ms | |
| mobilenet_v1_1 RSS @ ready · pass |
cortex-a53 |
48.1 MB → 53.4 MB | |
| hey_snips_v4_model17 load+optimize · pulse8 |
cortex-a7 |
7.52 s → 8.21 s | |
| hey_snips_v4_model17 load · pulse8 |
cortex-a7 |
6.78 s → 7.3 s |
|
Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode no inference-speed regressions Improvements
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| hey_snips_v1 load · 400ms |
cortex-a7 |
70 ms → 105 ms | |
| hey_snips_v1 load+optimize · 400ms |
cortex-a7 |
117 ms → 154 ms | |
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 98 ms | |
| hey_snips_v31 load+optimize · 400ms |
cortex-a7 |
390 ms → 437 ms | |
| hey_snips_v31 load · 400ms |
cortex-a7 |
342 ms → 382 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 145 ms | |
| hey_snips_v31 load+optimize · 400ms |
cortex-a9 |
344 ms → 362 ms |
|
Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode no inference-speed regressions Improvements
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 98 ms | |
| hey_snips_v31 load · 400ms |
cortex-a7 |
342 ms → 394 ms | |
| hey_snips_v31 load+optimize · 400ms |
cortex-a7 |
390 ms → 448 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 144 ms | |
| mobilenet_v1_1 RSS @ ready · pass |
cortex-a53 |
48.1 MB → 53.2 MB | |
| arm_ml_kws_cnn_m load · pass |
cortex-a53 |
47 ms → 50 ms |
|
Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode no inference-speed regressions Improvements
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 97 ms | |
| hey_snips_v4_model17_nnef load+optimize · pulse8 |
cortex-a7 |
1.55 s → 1.73 s | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 143 ms | |
| mobilenet_v1_1 load · pass |
cortex-a9 |
2.09 s → 2.25 s | |
| mobilenet_v1_1 load+optimize · pass |
cortex-a9 |
2.37 s → 2.54 s | |
| arm_ml_kws_cnn_m load · pass |
cortex-a53 |
47 ms → 50 ms | |
| en_tdnn_15M RSS @ ready · 2600ms |
cortex-a55 |
112 MB → 118 MB | |
| hey_snips_v31 load+optimize · 400ms |
cortex-a9 |
344 ms → 362 ms |
|
Reference: 2026-08-05 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode no inference-speed regressions Improvements
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
84 ms → 98 ms | |
| hey_snips_v1 load · 400ms |
cortex-a7 |
70 ms → 79 ms | |
| hey_snips_v31 load · 400ms |
cortex-a7 |
342 ms → 383 ms | |
| hey_snips_v31 load+optimize · 400ms |
cortex-a7 |
390 ms → 436 ms | |
| inceptionv1q load+optimize · pass |
cortex-a55 |
3.89 s → 4.33 s | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
130 ms → 145 ms | |
| mobilenet_v1_1 RSS @ ready · pass |
cortex-a53 |
48.1 MB → 53.4 MB | |
| hey_snips_v1 load+optimize · 400ms |
cortex-a7 |
117 ms → 130 ms | |
| arm_ml_kws_cnn_m load · pass |
cortex-a53 |
47 ms → 51 ms | |
| hey_snips_v31 load+optimize · 400ms |
cortex-a9 |
344 ms → 362 ms |
|
🔴 Bench vs main — 1 speed regression(s) · Reference: 2026-08-06 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode
Improvements
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
83 ms → 106 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
129 ms → 157 ms |
|
🔴 Bench vs main — 1 speed regression(s) · Reference: 2026-08-06 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode
Improvements
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
83 ms → 119 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
129 ms → 176 ms | |
| hey_snips_v1 load · 400ms |
cortex-a9 |
76 ms → 93 ms | |
| hey_snips_v1 load+optimize · 400ms |
cortex-a9 |
144 ms → 174 ms | |
| arm_ml_kws_cnn_m load · pass |
cortex-a53 |
48 ms → 53 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a53 |
74 ms → 81 ms | |
| hey_snips_v4_model17 load · pulse8 |
cortex-a7 |
6.74 s → 7.36 s | |
| hey_snips_v4_model17 load+optimize · pulse8 |
cortex-a7 |
7.48 s → 8.09 s | |
| hey_snips_v4_model17 load+optimize · 2sec |
cortex-a7 |
5.04 s → 5.4 s |
|
Reference: 2026-08-06 morning nightly run (0d old) · full report → run Speed — evaltime · prefill · decode no inference-speed regressions Improvements
|
| Δ | metric | device | main → PR |
|---|---|---|---|
| arm_ml_kws_cnn_m load · pass |
cortex-a9 |
83 ms → 106 ms | |
| arm_ml_kws_cnn_m load+optimize · pass |
cortex-a9 |
129 ms → 155 ms | |
| hey_snips_v4_model17 load · pulse8 |
cortex-a7 |
6.74 s → 7.67 s | |
| hey_snips_v4_model17 load+optimize · pulse8 |
cortex-a7 |
7.48 s → 8.43 s | |
| llama_3_2_3B_instruct_q40ef16_541 load+optimize · cuda |
jetson-orin-nx |
4.38 s → 4.73 s | |
| llama_3_2_3B_instruct_q40ef16_541 load · cuda |
jetson-orin-nx |
3.15 s → 3.34 s |
Implements the
tract_moe_ffnoperator in thetract_transformersextension, enabling inference of routed Mixture-of-Experts FFN blocks exported viatorch_to_nnef.The operator encapsulates the full MoE FFN block:
x @ wg.Tfollowed by top-k expert selection.activation(x @ w1) * (x @ w3)followed by@ w2for SwiGLU-style experts.Real conditional compute: unused experts are fully skipped. The op handles both 2D
[T,D]and 3D[B,S,D]input shapes.Model Coverage
The implemented semantics cover the routed FFN variants used by:
Current validation:
TitanML/tiny-mixtral(8 experts, top-2, 246M params).Granite MoE Benchmark
Local release measurements on Apple Silicon. The benchmark artifact is not included in this PR.
77.68 tok/s78.66 tok/s73.86 tok/s74.47 tok/s65.04 tok/s65.35 tok/s40.63 tok/sAdditional steady-state Metal harness results on the same model family:
1280.3 tok/s123.5 tok/s1254.6 tok/s122.0 tok/s1128.2 tok/s119.4 tok/s