Skip to content

Commit c3c0965

Browse files
authored
Merge pull request #120 from KempnerInstitute/docs/moe-router-zloss
docs: document moe_router_z_loss_weight (router z-loss)
2 parents 9f3fa23 + d9cd9d7 commit c3c0965

4 files changed

Lines changed: 44 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- **MoE router z-loss** (`moe_router_z_loss_weight`, ST-MoE style). An optional penalty on the router's pre-softmax logits — per MoE layer `mean_token(logsumexp(router_logits))²` — summed across layers and added to the training loss as `moe_router_z_loss_weight × z_loss`. It keeps router logits from growing without bound, targeting *logit-growth stability* (not load balance — that's the aux loss). Default `0.0` is off: the term is never added, so training, outputs, and gradients are unchanged. `z_loss` is a plain attribute like `aux_loss` (not a buffer/parameter), so it never enters `state_dict` — checkpoint-safe.
12+
- `kempnerforge/config/model.py`: `moe_router_z_loss_weight: float = 0.0` (with a non-negativity check).
13+
- `kempnerforge/model/router.py`: both `SoftmaxTopKRouter` and `SigmoidTopKRouter` set `self.z_loss = (logsumexp(logits, dim=-1) ** 2).mean()`.
14+
- `kempnerforge/model/moe.py` / `transformer.py`: `MoEMLP.z_loss` exposes the per-layer value; `Transformer.get_moe_router_z_loss()` sums it across MoE layers (mirrors `get_moe_aux_loss()`).
15+
- `scripts/train.py`: adds `moe_router_z_loss_weight × z_loss` to the loss on both forward paths (gated on `weight > 0`) and logs `moe/router_z_loss`.
16+
- Tests: `tests/unit/test_config.py` (default, rejects negative) and `tests/unit/test_moe.py` (z-loss computed; scales with the coefficient).
1117
- **Fine-grained MoE experts** (`moe_expert_ffn_multiplier`). Decouples each expert's FFN hidden width from the dense FFN: the per-expert hidden dim is `computed_ffn_hidden_dim × moe_expert_ffn_multiplier`, rounded to a multiple of 16. The default `1.0` is a no-op (each expert is a full dense FFN, zero behavior change); set `0.5` for fine-grained experts so top-2 routing matches the dense FFN's activated FLOPs (`2 × F/2 = F`) while adding total capacity — the DeepSeekMoE recipe. Applies to routed and shared experts wherever they are built (`build_moe` and MoMa's `ExpertChoiceMoE`).
1218
- `kempnerforge/config/model.py`: `moe_expert_ffn_multiplier: float = 1.0` (with a positivity check) and a `computed_expert_ffn_hidden_dim` property; `num_params_estimate` accounts for the smaller experts.
1319
- `kempnerforge/model/{moe,moma,mot,transformer}.py`: experts are built at `computed_expert_ffn_hidden_dim` instead of the dense FFN width.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ PyTorch-native framework for fault-tolerant distributed training of foundation m
2727

2828
**Architecture**
2929
- Decoder-only Transformer: RoPE, GQA, SwiGLU, RMSNorm, `torch.compile`
30-
- Mixture-of-Experts: softmax top-k and DeepSeek-V3 sigmoid routers, shared experts, fine-grained experts, configurable frequency
30+
- Mixture-of-Experts: softmax top-k and DeepSeek-V3 sigmoid routers, shared experts, fine-grained experts, router z-loss, configurable frequency
3131

3232
**Parallelism**
3333
- FSDP2, Tensor, Expert, and Pipeline Parallelism
@@ -212,7 +212,7 @@ Core MoE, Expert Parallelism, DeepSeekMoE, grouped GEMM, FSDP2 compatibility, an
212212

213213
| Feature | Status | Impact |
214214
|---------|:------:|--------|
215-
| Router improvements (sequence aux loss, gradient scaling, adaptive bias) | **Done** | Training quality |
215+
| Router improvements (sequence aux loss, gradient scaling, adaptive bias, z-loss) | **Done** | Training quality |
216216
| FP8 mixed precision | **Done** | 2x compute throughput |
217217
| Pipeline parallelism for MoE (PP+EP+TP composition) | Blocked | Required for 100B+ |
218218
| Communication-computation overlap (async EP dispatch) | Planned | 15-30% throughput |

docs/moe/aux-loss-and-balancing.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,41 @@ Leave `moe_aux_loss_weight` at its default 0.01 — it's the
153153
multiplier on `aux_loss` downstream, so the effective weight on the
154154
balance loss is `moe_aux_loss_weight × sequence_aux_loss_weight`.
155155

156+
## Router z-loss (logit-growth stability)
157+
158+
Opt-in via `moe_router_z_loss_weight > 0` (default `0.0` = off). The
159+
ST-MoE *z-loss* penalizes the router's pre-softmax logits so they don't
160+
grow without bound. It is orthogonal to the balancing losses above — it
161+
targets *logit-growth stability*, not load balance:
162+
163+
```python
164+
# kempnerforge/model/router.py — both routers, after computing logits
165+
self.z_loss = (torch.logsumexp(logits, dim=-1) ** 2).mean()
166+
```
167+
168+
Both `SoftmaxTopKRouter` and `SigmoidTopKRouter` set it. `MoEMLP.z_loss`
169+
surfaces the per-layer value, and `Transformer.get_moe_router_z_loss()`
170+
sums it across MoE layers — mirroring `aux_loss` / `get_moe_aux_loss()`.
171+
The training loop adds it only when the weight is positive:
172+
173+
```python
174+
# scripts/train.py
175+
if mc.moe_router_z_loss_weight > 0:
176+
z = inner_transformer(model).get_moe_router_z_loss()
177+
loss = loss + mc.moe_router_z_loss_weight * z
178+
```
179+
180+
Logged as `moe/router_z_loss`. `z_loss` is a plain attribute (like
181+
`aux_loss`), not a buffer or parameter, so it never enters `state_dict`
182+
(checkpoint-safe); with the default `0.0` the term is never added, so
183+
training, outputs, and gradients are unchanged.
184+
185+
**When to use**: long runs where router logits drift upward. A small
186+
weight such as `1e-3` keeps the summed router z-loss bounded (~2–3, vs.
187+
~14→24 when off) at identical LM loss — stabilizing the router at
188+
negligible cost. Distinct from the output-logit `z_loss_weight`
189+
(a cross-entropy regularizer); this one acts on the *router* logits.
190+
156191
## Per-expert gradient scaling
157192

158193
Opt-in via `moe_gradient_scale = true`. Normalizes each expert's

docs/moe/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ the distributed mechanics (all-to-all, expert parallelism) live under
3838
| `moe_sequence_aux_loss_weight` | `0.0` | Sigmoid-only: sequence-level balance penalty |
3939
| `moe_bias_schedule` | `"constant"` | Sigmoid-only: bias update rate schedule |
4040
| `moe_expert_ffn_multiplier` | `1.0` | Per-expert FFN hidden = `multiplier × dense FFN`; `0.5` = fine-grained, so top-2 matches the dense FFN's activated FLOPs |
41+
| `moe_router_z_loss_weight` | `0.0` | `>0` adds the ST-MoE router z-loss `(logsumexp logits)²` to stabilize router logits (see [Aux loss and balancing](aux-loss-and-balancing.md)) |
4142

4243
All fields live in
4344
[`kempnerforge/config/model.py`](https://github.com/KempnerInstitute/KempnerForge/blob/main/kempnerforge/config/model.py)

0 commit comments

Comments
 (0)