API¶
Every entry point below imports from the top-level esmoe; the package ships py.typed, so the signatures are visible to IDEs and mypy.
equip¶
esmoe.equip(base="yolov8n.yaml", *, weight=0.01, recipe="esmoe", out=None, **graft_kwargs) -> YOLO
Register, graft, build and wire in one call. out names the grafted config to keep; without it the config goes to a temporary directory (a YOLO wrapper loads models by path). weight and recipe go to attach_aux_loss, graft_kwargs pass through to graft.
inject_esmoe¶
esmoe.inject_esmoe() -> type[ESMoE]
Exposes ESMoE where parse_model resolves layer names, after which any model.yaml can write [-1, 1, ESMoE, [4, 2]].
graft¶
esmoe.graft(base="yolov8n.yaml", out=None, *, at="backbone_end", num_experts=4, top_k=2,
rewire=False, out_channels=None, **settings) -> dict
Inserts blocks after the layers named by at and renumbers every later reference. at is "backbone_end", "backbone_stages" (one block after each backbone stage, reproducing the upstream layout), one index or several. With rewire=True every later consumer of an insertion layer is pointed at the block; without it, a head branch that names the old backbone end by index (YOLOv8's P5 lateral) keeps reading the pre-block feature.
**settings takes balance, out_norm, dense_training, sparse_inference, dynamic_threshold and expert, and writes them into the config rather than onto the instance: the trainer rebuilds the model from model.yaml, and anything set on the instance goes with the instance it discards. balance may be a name or a function, expert a class or a factory. The ones that ship are written by short name (objectives switch, gshard, master, gshard_probs; the expert dw); a custom one is written as module:qualname, and every rebuild -- the one in a DDP worker included -- imports the same object back from that name. A custom function or class therefore has to live at module level in a module the training environment can import; a lambda, a nested function or anything defined in __main__ is refused when grafting.
out_channels widens the blocks to that many channels, taken literally rather than scaled by the yaml's width multiple. Stock parse_model takes a third-party module's output width to be its input width, so every widened block is followed by the official Index layer: the block hands it a one-element list, and parse_model reads that layer's declared width. With rewire=True consumers are pointed at that layer.
attach_aux_loss¶
esmoe.attach_aux_loss(model, weight=0.01, recipe="esmoe") -> model
Puts the auxiliary loss into the optimised training loss; training logs gain an esmoe_aux column. Also routes model.train() through esmoe.trainer, which is how DDP workers register the block and recover the weight and recipe on their own. Inside a process group an expert no image routed to joins the graph at zero weight, so multi-GPU training works under compile=True too, where ultralytics turns find_unused_parameters off.
recipe decides how the block and its term train:
"esmoe"(default, and what every run outside the same-configuration comparison used): the term timesweight, counted per image the way the task loss counts."upstream": the three things YOLO-Master's trainer does to any model with a routed module, for runs compared against it. The term is divided by a running mean of its own magnitude (decay 0.99, starting at 1.0), multiplied byweight, capped at 3.0 and added once to each of box, cls and dfl; router parameters get a group of their own at half the learning rate, outside Muon; expert parameters stay frozen for the first 3 epochs. The last two happen in the trainer, so they apply to a YOLO model trained throughmodel.train(). Constants and sources are inesmoe.upstream.
collect_aux_loss¶
esmoe.collect_aux_loss(model, device=None) -> Tensor
Sums the auxiliary loss each block last published to the registry, for custom training loops. It reads without clearing, so call clear_aux_loss before each forward.
clear_aux_loss¶
esmoe.clear_aux_loss() -> None
Drops every value blocks have published into the registry. The loss patch attach_aux_loss installs calls it before each forward; a custom training loop that does not go through that patch has to call it itself, or a block that does not run in some step will still be answering with the value from the step before.
odd / odd_kernels¶
esmoe.odd(size) -> int
esmoe.odd_kernels(num_experts, max_kernel_size=15) -> list[int]
odd steps an even kernel down (4 -> 3) so the padding stays centred, which is what upstream does to both explicit kernel sizes and the cap. odd_kernels generates the default heterogeneous set 3, 5, 7, ... capped at max_kernel_size -- the one ESMoE uses when expert_kernel_sizes is not given.
ESMoE¶
esmoe.ESMoE(num_experts=4, top_k=2, channels=None, options=None, *,
out_channels=None, reduction=8, max_kernel_size=15,
expert_kernel_sizes=None, expert=DWExpert, **settings)
A mixture-of-experts block, channel-preserving unless out_channels says otherwise. channels is inferred on the first forward when omitted; top_k=None activates every expert. expert is a (c1, c2, k) -> Module factory, or its name (a short name from esmoe.EXPERTS, or module:qualname). options is the mapping a config carries settings in ([-1, 1, ESMoE, [4, 2, null, {out_norm: true}]]); it is equivalent to **settings and may also carry expert and out_channels. An out_channels given through options makes the block return a one-element list for the Index layer after it.
The five settings and their defaults (esmoe.SETTINGS, a read-only mapping):
| setting | default here | upstream | what it does |
|---|---|---|---|
balance |
switch_balance |
gshard_balance |
the objective, (probs, gate) -> scalar, or a name from esmoe.BALANCES, or module:qualname |
out_norm |
False |
always on | BatchNorm + SiLU after the weighted sum (the paper's eq. 2 Norm) |
dense_training |
False |
always on | run every expert while training; unrouted ones are weighted zero but their normalisation statistics keep moving |
sparse_inference |
True |
same | skip unrouted experts outside training |
dynamic_threshold |
0.0 |
0.4 |
outside training, drop a routed expert whose share of the mixture -- its weight after the top-k renormalisation -- is below the threshold, keep the leader, renormalise |
The last two affect inference only, the first three affect training. The defaults for out_norm, dense_training and dynamic_threshold keep the runs already in results/ reproducible; they are not a judgement against upstream. The default for balance is settled by data: an objective that reads the gate (upstream's gshard, the paper's master) has no gradient for an expert outside the top-k, and five of six such checkpoints lost an expert, as did all six same-configuration B checkpoints, while Switch reads the full softmax and lost none in 66 (judgment lines, rounds six to eight).
block.spec() returns the five settings a block is holding, plus expert when a custom one is in use, and block.configure(**settings) changes them on paths that never reach a trainer -- inference, export, a unit test. esmoe.blocks(model) walks every block in a model in module order, and scripts/blockspec.py reads back from any checkpoint what was actually in force.
blocks¶
esmoe.blocks(model) -> Iterator[ESMoE]
Yields every block in a model in module order. It is a generator, so walking the blocks again takes another call; a YOLO object and its .model both work:
for block in esmoe.blocks(model):
print(block.spec())
Balancing objectives¶
esmoe.switch_balance(probs, gate) -> Tensor
esmoe.gshard_balance(probs, gate) -> Tensor
esmoe.master_balance(probs, gate) -> Tensor
esmoe.gshard_probs_balance(probs, gate) -> Tensor
probs is the router's full softmax and gate the top-k selection renormalised, both shaped (batch, E). The usage u below is the per-expert mean, normalised to sum to one.
| function | short name | reads | value |
|---|---|---|---|
switch_balance |
switch |
probs and gate > 0 |
E · Σ mean(probs)ᵢ · mean(gate > 0)ᵢ |
gshard_balance |
gshard |
gate |
E · Σ uᵢ², the form upstream's ES_MOE uses |
master_balance |
master |
gate |
(1/E) · Σ (uᵢ − 1/E)², the paper's eq. 13, equal to (gshard − 1)/E² |
gshard_probs_balance |
gshard_probs |
probs |
E · Σ uᵢ², kept to isolate which tensor is read |
esmoe.BALANCES maps the short names to the functions. A custom objective takes the same arguments and returns a scalar.
DWExpert¶
esmoe.DWExpert(c1, c2, k)
The default expert: a k×k depthwise convolution (groups=c1, no bias), then a 1×1 pointwise convolution, BatchNorm and SiLU. esmoe.EXPERTS maps short names to expert factories and holds dw only; a custom expert follows (c1, c2, k) -> Module.
Command line¶
esmoe graft BASE -o OUT [options]
esmoe info
| option | default | what it does |
|---|---|---|
-o, --out |
required | where to write the config |
-e, --num-experts |
4 |
experts per block |
-k, --top-k |
2 |
experts active per image |
--at |
backbone_end |
backbone_end, backbone_stages, one index, or comma-separated indices |
--rewire |
off | consumers read the block's output |
--out-channels |
none | widen the blocks to this literal width |
--balance |
switch |
switch, gshard, master, gshard_probs or module:qualname |
--expert |
dw |
module:qualname of a custom expert |
--out-norm |
off | BatchNorm + SiLU after the weighted sum |
--dense-training |
off | run every expert while training |
Only settings given explicitly are written into the config. dynamic_threshold and sparse_inference have no option; pass them through equip or the config. esmoe info prints the esmoe, ultralytics and torch versions.