Skip to main content

ES-MoE Toolkit

Expert-sparse MoE block for torch models and Ultralytics YOLO


Python PyPI Docs Demo Colab Molab License

DeepWiki Ingest Diagram

Drop it into a torch model or add it to YOLO in one call, the router loss reaches the optimiser, and every number here has a run record behind it. Try It.


Docs: English · 中文 · Quick start in Colab: notebooks/quickstart.ipynb · Ask questions about the code: DeepWiki.


Install

uv add esmoe            # the block, for any torch model
uv add esmoe[yolo]      # ... and the Ultralytics integration

esmoe[ultralytics] is that extra under its other name, and esmoe[tensorflow], esmoe[jax], esmoe[flax] and esmoe[keras] each bring one backend. The distribution, the import and the command line are all esmoe; the project is written ES-MoE in prose.

In a torch model

import esmoe
from torch import nn

net = nn.Sequential(
    nn.Conv2d(3, 64, 3, 2, 1),
    esmoe.ESMoE(kernels=[3, 5, 7], top_k=1),          # three experts, one per sample
    esmoe.ESMoE(kernels=[(1, 9), (9, 1)], top_k=2),   # two strip experts, both per sample
)

with esmoe.routing(net) as route:
    loss = task_loss(net(x), y) + 0.01 * route.loss()
loss.backward()

kernels gives each expert its receptive field, an int or an (h, w) pair, and with it how many experts there are. expert gives a branch its structure, one factory for all of them or one each. top_k says how many run per sample. The block takes its width from the first forward pass, so nothing has to declare it. To put blocks into a model that already exists:

esmoe.graft(resnet, at="layer3", mode="after", kernels=[3, 5, 7], top_k=1)

at takes a submodule name, several names, or a (name, module) -> bool test; mode is after, before or replace.

In Ultralytics YOLO

One call registers the block, grafts it into the config, builds the model, and makes the router loss part of the optimised training loss:

model = esmoe.equip("yolo11n.yaml", weight=0.01)
model.train(data="coco8.yaml", epochs=10)

Or take the steps apart when you need control over each one:

from ultralytics import YOLO
import esmoe

esmoe.register()                                             # resolve `ESMoE` in a model.yaml
esmoe.graft("yolov8n.yaml", out="v8-esmoe.yaml", at=[4, 6])  # insert blocks, renumber the head
model = YOLO("v8-esmoe.yaml")
esmoe.attach_aux_loss(model, weight=0.01)                    # the router loss joins the total

From the shell:

esmoe graft yolo11n.yaml -o yolo11n-esmoe.yaml --kernels 3,5,7 -k 1

attach_aux_loss adds an esmoe_aux entry to the trainer's loss table, so a non-zero, back-propagated auxiliary term shows up in results.csv rather than merely in a config.

Written by hand, a config layer is just:

[-1, 1, ESMoE, [4, 2]]                              # num_experts, top_k
[-1, 1, ESMoE, [{kernels: [3, 5, 7], top_k: 1}]]    # or say what you mean
[-1, 1, ESMoE, [4, 2, null, {out_norm: true}]]      # ... and settings the trainer must keep
[-1, 1, ESMoE, [4, 2, null, {out_channels: 320}]]   # widened ...
[-1, 1, Index, [320, 0]]                            # ... and the official layer that declares it

ESMoE, ES_MoE and ES_MOE all resolve to the block, and a config that already names one is taken as written: equip grafts nothing into it. The block is channel preserving and infers its width on the first forward, which is what lets stock parse_model size it without a patch. A widened block instead hands a one-element list to the official Index layer after it, whose declared width parse_model does read; graft(out_channels=...) writes both. Settings belong in the config because the trainer rebuilds the model from it, dropping anything set on the blocks beforehand.

On tensorflow, jax and keras

Each framework gets its own implementation, written the way that framework is written: a tf.Module on TensorFlow, pure functions on Jax, an nnx.Module on Flax, a keras.layers.Layer on Keras 3. They read their arguments through the same code, so they take the same ones.

from esmoe.tensorflow import ESMoE, Sequential  # tf.Module, no Keras
from esmoe.jax import ESMoE                     # init/apply, no Flax
from esmoe.flax import ESMoE                    # nnx.Module, the current Flax API
from esmoe.keras import ESMoE                   # keras.layers.Layer, any Keras backend

One difference holds for all three: they evaluate every expert and weight them by the gate. Skipping an unrouted expert depends on the data, which a traced graph cannot express, so the inference-time saving is torch's alone. The routing and the balancing term are the same everywhere. esmoe.backend(name) returns the package for torch, tensorflow, jax, flax or keras.

Extend

Experts and the balancing objective are plain callables, so a variant is a few lines:

esmoe.ESMoE(kernels=[3, 5], expert=[MyExpert, esmoe.DWExpert], balance=my_balance_fn)

MyExpert(c1, c2, k) -> Module, my_balance_fn(probs, gate) -> scalar. To train with them under YOLO, pass them to graft or equip: a function or class defined at module level goes into the config as module:qualname, and every rebuild of the model -- the trainer's, each DDP worker's -- imports it back from that name. A lambda or anything defined in __main__ is refused when grafting. esmoe.blocks(model) walks every block in a model, esmoe.collect_aux_loss(model) returns the current step's router loss for custom training loops, and block.spec() reports the settings a block is holding.

Compatibility

backend written against router loss graft tested on
torch nn.Module a weak registry any nn.Module every CI leg, and one with ultralytics absent
tensorflow tf.Module a weak registry attribute or position a leg with keras and ultralytics absent
jax pure functions returned by apply not applicable a leg with keras and ultralytics absent
flax nnx.Module an nnx.Intermediate nnx.Sequential a leg with keras and ultralytics absent
Keras 3 keras.layers.Layer add_loss Sequential one leg per Keras backend: tensorflow, jax, torch

The Ultralytics integration runs on the torch block:

backbone build + forward grafted config aux loss in training protocol runs
YOLOv5 yes yes yes yes
YOLOv8 yes yes yes yes
YOLOv9 yes yes yes yes
YOLOv10 yes yes yes yes
YOLO11 yes yes yes yes
YOLO12 yes yes yes yes
YOLO26 yes yes yes yes
YOLO-Master (fork) yes yes yes no

Every row is built, grafted and trained in CI, against both the latest ultralytics and 8.4.101, which report loss items in different shapes. The last column separates "builds and trains" from "was run under the full accuracy protocol".

DDP works: attach_aux_loss routes model.train() through a trainer class that lives in esmoe.yolo.trainer, so the worker processes ultralytics spawns register the block and the auxiliary loss before they build. Verified by scripts/verify.py (the real worker file in a fresh interpreter; two gloo ranks with agreeing router gradients). Inside a process group an expert that no image routed to joins the graph at zero weight, so the settings ultralytics uses under compile=True (find_unused_parameters=False, static_graph=True) train as well; tests/test_distributed.py checks that with two gloo ranks, and that the block compiles and agrees with eager.

attach_aux_loss(model, weight=1.0, recipe="upstream") trains the way YOLO-Master's own trainer trains a routed model, for a like-for-like comparison with it.

Defaults, and what they cost

ESMoE(num_experts=4, top_k=2) with attach_aux_loss(weight=0.01). The block adds 4.5% to 12.5% parameters depending on the backbone and 4% to 10% wall-clock per run.

Paired mAP50 delta by backbone generation

Accuracy depends on what the backbone ends in, not on how new it is: positive on the SPPF family (+0.0055 v5n, +0.0025 v8n, +0.0025 v9t), flat once the end is an attention block (-0.0002 v10n, +0.0013 11n), negative on area attention and the E2E head (-0.0018 12n, -0.0034 26n). Three seeds read direction, not magnitude; the same-configuration repeats sit at the same scale as the effect. Default grafting leaves a head branch that names the old backbone end by index reading the pre-block tensor, and graft(..., rewire=True) retargets it, which is the only 3/3 arm on v8n.

What the balancing term reliably buys is that no expert dies: without it all six checkpoints lose two of four experts, and the Switch term at 0.01 leaves none dead in 66.

The dataset, the protocol, every run record and the per-seed figures are on the experiments page, with the criteria written before each round in docs/JUDGMENT.md.

Develop

uv sync --group dev
uv run ruff format . && uv run ruff check .
uv run pytest -q

CI runs thirteen legs on ubuntu-26.04: the full suite on Python 3.12 and 3.13 against two ultralytics releases, one leg per backend with the other frameworks absent, the Keras layer on each of its three backends, ten correctness checks on real training, and the quick-start notebook. Contributing guidelines are in .github/CONTRIBUTING.md.

Reproducing the accuracy study is a separate path: every run writes one machine-readable record to results/, and the checkpoints live on the checkpoints branch. The experiments page has the commands.

Linked projects

  • ultralytics - the official YOLO framework this plugs into.
  • flax - the Jax library one of the implementations is written against.
  • keras3 - the multi-backend framework one of the implementations runs on.
  • Tencent/YOLO-Master - where ES-MoE comes from (paper).

License

AGPL-3.0-only, matching the Ultralytics ecosystem it builds on.

Metadata

Release files for esmoe 2.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for esmoe 2.2.1
File Size Uploaded Provenance Actions
esmoe-2.2.1.tar.gz 91.7 kB With provenance Details for esmoe-2.2.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for esmoe 2.2.1
File Interpreter ABI Platform Actions
esmoe-2.2.1-py3-none-any.whl Python 3 none any Details for esmoe-2.2.1-py3-none-any.whl

Total release size: 162.1 kB

Release files / esmoe-2.2.1.tar.gz

Download URL esmoe-2.2.1.tar.gz
Size 91.7 kB
Tags Source
SHA-256 checksum
How to use checksums
f504d691ae7d29d109d25b0f46536c910127074a607a961a9a26f9bc14df43e1
BLAKE2b-256 checksum
How to use checksums
9a51ecc0381c6be7a670fb2c69066651109cf92e572b146988ccb88ca5c03a31
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / esmoe-2.2.1-py3-none-any.whl

Download URL esmoe-2.2.1-py3-none-any.whl
Size 70.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fa227912f70fcf81028f16a654ceba85fed2f9e1c2891855bda232a7f3fd4c65
BLAKE2b-256 checksum
How to use checksums
0273f3c6f56e0c1a2f0598f5de4c9ee187bc40eafcacd0a6f674587f85e16751
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for esmoe 2.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for esmoe-2.2.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page