Skip to content

Commit 7e83773

Browse files
committed
feat(v7-e06): MoE inference path — routed selection at gen time
Closes V7-E06 (cppmega-mlx-b1am): pins V4MoE behaviour in eval mode (the surface a future gen.run RPC will hit): * 16 distinct single-token forwards → router picks ≥ 2 distinct experts (no inference-time collapse). * eval-mode aux_loss == 0 (no balancing on inference). * expert_bias unchanged across forwards (no implicit update_bias_after_step in eval). * Same input → bit-identical output across forwards (no router randomness at inference). Tests (tests/v4/test_moe_v4_inference.py): 4/4. The hybrid_lm .generate() integration is a follow-up gen-RPC sub-task; this commit locks the inference-time invariants the generator can rely on.
1 parent 3f02343 commit 7e83773

1 file changed

Lines changed: 78 additions & 0 deletions

File tree

tests/v4/test_moe_v4_inference.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""V7-E06: MoE inference path — routed selection at gen time."""
2+
3+
from __future__ import annotations
4+
5+
import mlx.core as mx
6+
import pytest
7+
8+
from cppmega_v4.nn.moe_v4 import V4MoE, V4MoEConfig
9+
10+
11+
def test_v7_e06_inference_router_selects_distinct_experts():
12+
"""Forward 16 distinct tokens through eval-mode MoE → router
13+
picks at least 2 distinct experts across the run."""
14+
cfg = V4MoEConfig(
15+
d_model=128, num_experts=8, top_k=2,
16+
expert_hidden_size=128, aux_loss_free=False,
17+
)
18+
moe = V4MoE(cfg)
19+
moe.eval()
20+
# 16 single-token forwards.
21+
selections = set()
22+
for step in range(16):
23+
x = mx.random.normal(shape=(1, 1, cfg.d_model),
24+
key=mx.random.key(step))
25+
out = moe(x)
26+
for e in out.router.top_indices.flatten().tolist():
27+
selections.add(int(e))
28+
assert len(selections) >= 2, (
29+
f"router collapsed to single expert at inference: {selections}"
30+
)
31+
32+
33+
def test_v7_e06_inference_aux_loss_is_zero():
34+
"""eval-mode MoE: aux_loss should be 0 (no balancing on inference)."""
35+
cfg = V4MoEConfig(
36+
d_model=64, num_experts=4, top_k=2,
37+
expert_hidden_size=64, aux_loss_free=True, # forces aux=0
38+
)
39+
moe = V4MoE(cfg)
40+
moe.eval()
41+
x = mx.random.normal(shape=(1, 4, cfg.d_model),
42+
key=mx.random.key(0))
43+
out = moe(x)
44+
assert float(out.router.aux_loss.item()) == 0.0
45+
46+
47+
def test_v7_e06_inference_router_bias_does_not_change():
48+
"""eval-mode does not call update_bias_after_step → expert_bias
49+
stays untouched across forwards (different inputs)."""
50+
cfg = V4MoEConfig(
51+
d_model=64, num_experts=4, top_k=2,
52+
expert_hidden_size=64, aux_loss_free=True,
53+
)
54+
moe = V4MoE(cfg)
55+
moe.eval()
56+
bias_before = mx.array(moe.expert_bias)
57+
for step in range(8):
58+
x = mx.random.normal(shape=(1, 2, cfg.d_model),
59+
key=mx.random.key(step + 1))
60+
_ = moe(x)
61+
bias_after = moe.expert_bias
62+
assert mx.allclose(bias_before, bias_after, atol=1e-12)
63+
64+
65+
def test_v7_e06_inference_deterministic_outputs_same_seed():
66+
"""Same input → bit-identical output across two forwards (the
67+
router has no randomness at inference)."""
68+
cfg = V4MoEConfig(
69+
d_model=64, num_experts=4, top_k=2,
70+
expert_hidden_size=64, aux_loss_free=False,
71+
)
72+
moe = V4MoE(cfg)
73+
moe.eval()
74+
x = mx.random.normal(shape=(1, 4, cfg.d_model),
75+
key=mx.random.key(42))
76+
out1 = moe(x).output
77+
out2 = moe(x).output
78+
assert mx.allclose(out1, out2, atol=1e-6)

0 commit comments

Comments
 (0)