Skip to content

Commit 12779b2

Browse files
committed
feat(e7-13): extended activations (mish/geglu/reglu/xielu) + catalog entries
Stage E7-13 of E2E Coverage Matrix v2 epic (cppmega-mlx-bb0.13). Brings the activation registry from 6 → 10 entries: adds dense mish and gated geglu/reglu/xielu. cppmega_mlx/nn/activations.py: - ActivationName Literal extended. - IS_GATED map covers all 10 entries (4 gated: swiglu/geglu/reglu/xielu; 6 dense: gelu/relu/relu2/sqrelu/silu/mish). - apply_activation gains four new branches: * mish: x * tanh(softplus(x)); softplus via log1p(exp) for bf16 stability * geglu: gelu_approx(gate) * up * reglu: max(gate, 0) * up * xielu: gelu_approx(gate) * silu(up) — extended xGLU variant seen in Megatron ablations. cppmega_v4/explain/catalog.py: +4 ExplainEntry items with paper references (Misra 2019 for Mish; Shazeer 2020 for GeGLU/ReGLU). xIELU is marked research-only with no canonical paper ref. vbgui/src/components/BrickContextPanel.tsx: activation dropdown extended to 11 options (10 activation names + the legacy 'glu' default in _build_mlp). Tests: - tests/v4/test_extended_activations.py (+10 pytest): * ACTIVATION_NAMES has exactly 10 * gated set has 4 entries (swiglu/geglu/reglu/xielu) * mish classified as dense * each new gated activation rejects missing gate * mish forward parity vs x * tanh(softplus(x)) * geglu/reglu/xielu forward parity vs reference impls * every activation has a catalog entry - tests/v4/test_optim_spec_lion.py: reused tests updated (6 → 10 expected names; "mish" replaced by "banana" in unknown-name probe). - tests/v4/test_catalog.py: dispatch_catalog_list_options_activation expects 10 names instead of 6. Regression: pytest 2290 (was 2270; +10 extended + 2 churn-from-rename) / 2 skip / 15 xfail / 0 fail (excl. pre-existing path_d). vbgui vitest 150 / 0 fail. Closes cppmega-mlx-bb0.13.
1 parent ba36fa4 commit 12779b2

6 files changed

Lines changed: 166 additions & 15 deletions

File tree

cppmega_mlx/nn/activations.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@
2323

2424

2525
ActivationName = Literal[
26-
"gelu", "relu", "relu2", "sqrelu", "silu", "swiglu",
26+
"gelu", "relu", "relu2", "sqrelu", "silu", "mish",
27+
"swiglu", "geglu", "reglu", "xielu",
2728
]
28-
"""Recognised activation names — E7-12 set (6 entries).
29+
"""Recognised activation names — E7-12 + E7-13 set (10 entries).
2930
30-
Gated entries (require a ``gate`` companion projection): swiglu.
31-
Dense entries (single projection input): gelu, relu, relu2, sqrelu, silu.
31+
Gated entries (require a ``gate`` companion projection):
32+
swiglu, geglu, reglu, xielu.
33+
Dense entries (single projection input):
34+
gelu, relu, relu2, sqrelu, silu, mish.
3235
"""
3336

3437

@@ -38,7 +41,11 @@
3841
"relu2": False,
3942
"sqrelu": False,
4043
"silu": False,
44+
"mish": False,
4145
"swiglu": True,
46+
"geglu": True,
47+
"reglu": True,
48+
"xielu": True,
4249
}
4350

4451

@@ -93,9 +100,24 @@ def apply_activation(
93100
return mx.square(mx.maximum(x, 0))
94101
if name == "silu":
95102
return nn.silu(x)
103+
if name == "mish":
104+
# x * tanh(softplus(x)); softplus(z) = log(1 + exp(z))
105+
# Use log1p for numerical stability.
106+
return x * mx.tanh(mx.log1p(mx.exp(x)))
96107
if name == "swiglu":
97-
assert gate is not None # narrowed for type-checkers
108+
assert gate is not None
98109
return nn.silu(gate) * x
110+
if name == "geglu":
111+
assert gate is not None
112+
return nn.gelu_approx(gate) * x
113+
if name == "reglu":
114+
assert gate is not None
115+
return mx.maximum(gate, 0) * x
116+
if name == "xielu":
117+
# Extended xGLU variant: gelu(gate) * silu(x); not a single
118+
# published paper but appears in some Megatron-style ablations.
119+
assert gate is not None
120+
return nn.gelu_approx(gate) * nn.silu(x)
99121

100122
raise AssertionError(f"unreachable: {name!r}")
101123

cppmega_v4/explain/catalog.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,55 @@ def list_options(category: str) -> list[ExplainEntry]:
265265
"projection; verify will block the mismatch",
266266
"Convention: intermediate_size = 8/3*H rounded to 256"),
267267
),
268+
ExplainEntry(
269+
category="activation", name="mish",
270+
summary="Mish — `x * tanh(softplus(x))` (Misra 2019). Smooth, "
271+
"self-regularizing.",
272+
when_to_use="Vision tasks; some LLM ablations show +0.5-1% "
273+
"accuracy vs ReLU/GELU.",
274+
when_to_avoid="LLM pretraining at scale — SwiGLU dominates.",
275+
recommended_params={},
276+
paper_ref="Misra, 2019",
277+
paper_url="https://arxiv.org/abs/1908.08681",
278+
gotchas=("More compute than GELU; not significantly better on "
279+
"LLM downstream metrics",),
280+
),
281+
ExplainEntry(
282+
category="activation", name="geglu",
283+
summary="Gated GELU — `gelu(gate) * up` (Shazeer 2020).",
284+
when_to_use="GLM / Falcon-family pretraining; fine-tuning from "
285+
"GELU-pretrained checkpoints with gating.",
286+
when_to_avoid="Same as SwiGLU memory caveat; pick SwiGLU if "
287+
"you're starting fresh in 2025.",
288+
recommended_params={},
289+
paper_ref="Shazeer, 2020",
290+
paper_url="https://arxiv.org/abs/2002.05202",
291+
gotchas=("Requires gated_mlp brick",),
292+
),
293+
ExplainEntry(
294+
category="activation", name="reglu",
295+
summary="Gated ReLU — `relu(gate) * up` (Shazeer 2020).",
296+
when_to_use="Hardware-constrained inference where ReLU is "
297+
"cheaper than GELU/SiLU on the target accelerator.",
298+
when_to_avoid="Modern LLM pretraining — quality lags SwiGLU.",
299+
recommended_params={},
300+
paper_ref="Shazeer, 2020",
301+
paper_url="https://arxiv.org/abs/2002.05202",
302+
gotchas=("Dead-gate problem: some gate units may emit 0 "
303+
"indefinitely",),
304+
),
305+
ExplainEntry(
306+
category="activation", name="xielu",
307+
summary="Extended xGLU — `gelu(gate) * silu(up)`. Niche, "
308+
"appears in Megatron ablations.",
309+
when_to_use="Research / ablation runs; not a default choice.",
310+
when_to_avoid="Production pretraining — under-studied.",
311+
recommended_params={},
312+
paper_ref=None,
313+
paper_url=None,
314+
gotchas=("Combined gating + activation cost > SwiGLU; quality "
315+
"rarely better",),
316+
),
268317
]
269318

270319

tests/v4/test_catalog.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,11 @@ def test_dispatch_catalog_list_options_activation():
190190
})
191191
assert resp.error is None
192192
options = resp.result["options"]
193-
assert len(options) == 6
193+
# E7-13 extended the activation registry from 6 → 10 entries.
194+
assert len(options) == 10
194195
names = {o["name"] for o in options}
195-
assert names == {"gelu", "relu", "relu2", "sqrelu", "silu", "swiglu"}
196+
assert names == {"gelu", "relu", "relu2", "sqrelu", "silu", "mish",
197+
"swiglu", "geglu", "reglu", "xielu"}
196198

197199

198200
def test_dispatch_catalog_explain_rejects_extra_params():
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""E7-13 tests: extended activations (mish/geglu/reglu/xielu)."""
2+
3+
from __future__ import annotations
4+
5+
import mlx.core as mx
6+
import mlx.nn as nn
7+
import pytest
8+
9+
from cppmega_mlx.nn.activations import (
10+
ACTIVATION_NAMES, IS_GATED, apply_activation, is_gated,
11+
)
12+
from cppmega_v4.explain import get_entry
13+
14+
15+
def test_activation_names_have_ten_entries():
16+
assert set(ACTIVATION_NAMES) == {
17+
"gelu", "relu", "relu2", "sqrelu", "silu", "mish",
18+
"swiglu", "geglu", "reglu", "xielu",
19+
}
20+
21+
22+
def test_gated_set_has_four_entries():
23+
gated = {n for n, g in IS_GATED.items() if g}
24+
assert gated == {"swiglu", "geglu", "reglu", "xielu"}
25+
26+
27+
def test_mish_is_dense():
28+
assert not is_gated("mish")
29+
30+
31+
@pytest.mark.parametrize("name", ["geglu", "reglu", "xielu"])
32+
def test_new_gated_activations_require_gate(name):
33+
x = mx.random.normal((2, 4))
34+
with pytest.raises(ValueError, match="is gated"):
35+
apply_activation(name, x)
36+
37+
38+
def test_mish_forward_matches_reference():
39+
"""Mish = x * tanh(softplus(x))."""
40+
x = mx.random.normal((4, 8), key=mx.random.key(0))
41+
got = apply_activation("mish", x)
42+
expected = x * mx.tanh(mx.log1p(mx.exp(x)))
43+
assert mx.allclose(got, expected).item()
44+
45+
46+
def test_geglu_uses_gelu_on_gate():
47+
x = mx.random.normal((2, 4), key=mx.random.key(0))
48+
g = mx.random.normal((2, 4), key=mx.random.key(1))
49+
got = apply_activation("geglu", x, gate=g)
50+
expected = nn.gelu_approx(g) * x
51+
assert mx.allclose(got, expected).item()
52+
53+
54+
def test_reglu_uses_relu_on_gate():
55+
x = mx.random.normal((2, 4), key=mx.random.key(0))
56+
g = mx.random.normal((2, 4), key=mx.random.key(1))
57+
got = apply_activation("reglu", x, gate=g)
58+
expected = mx.maximum(g, 0) * x
59+
assert mx.allclose(got, expected).item()
60+
61+
62+
def test_xielu_uses_gelu_gate_silu_value():
63+
x = mx.random.normal((2, 4), key=mx.random.key(0))
64+
g = mx.random.normal((2, 4), key=mx.random.key(1))
65+
got = apply_activation("xielu", x, gate=g)
66+
expected = nn.gelu_approx(g) * nn.silu(x)
67+
assert mx.allclose(got, expected).item()
68+
69+
70+
@pytest.mark.parametrize("name", ACTIVATION_NAMES)
71+
def test_every_activation_has_catalog_entry(name):
72+
entry = get_entry("activation", name)
73+
assert entry is not None, f"missing catalog entry for {name}"
74+
assert entry.summary

tests/v4/test_optim_spec_lion.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,24 @@ def test_existing_factories_unchanged(factory):
160160
# ---------------------------------------------------------------------------
161161

162162

163-
def test_activation_names_contains_six_entries():
163+
def test_activation_names_contains_ten_entries():
164+
# E7-13 extended the set from 6 → 10 (added mish + geglu/reglu/xielu).
164165
assert set(ACTIVATION_NAMES) == {
165-
"gelu", "relu", "relu2", "sqrelu", "silu", "swiglu",
166+
"gelu", "relu", "relu2", "sqrelu", "silu", "mish",
167+
"swiglu", "geglu", "reglu", "xielu",
166168
}
167169

168170

169-
def test_is_gated_only_swiglu_is_gated():
170-
assert is_gated("swiglu") is True
171-
for name in ("gelu", "relu", "relu2", "sqrelu", "silu"):
171+
def test_is_gated_four_gated_six_dense():
172+
for name in ("swiglu", "geglu", "reglu", "xielu"):
173+
assert is_gated(name) is True
174+
for name in ("gelu", "relu", "relu2", "sqrelu", "silu", "mish"):
172175
assert is_gated(name) is False
173176

174177

175178
def test_is_gated_rejects_unknown_name():
176-
with pytest.raises(ValueError, match="unknown activation 'mish'"):
177-
is_gated("mish")
179+
with pytest.raises(ValueError, match="unknown activation 'banana'"):
180+
is_gated("banana")
178181

179182

180183
@pytest.mark.parametrize("name", ["gelu", "relu", "relu2", "sqrelu", "silu"])

vbgui/src/components/BrickContextPanel.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { Tooltip } from "@/components/Tooltip";
1010
import { ExplainModal } from "@/components/ExplainModal";
1111

1212
const ACTIVATION_OPTIONS = [
13-
"glu", "gelu", "relu", "relu2", "sqrelu", "silu", "swiglu",
13+
"glu", "gelu", "relu", "relu2", "sqrelu", "silu", "mish",
14+
"swiglu", "geglu", "reglu", "xielu",
1415
];
1516
const NORM_OPTIONS = ["rmsnorm", "layernorm", "none"];
1617

0 commit comments

Comments
 (0)