Skip to content

Commit db829a4

Browse files
committed
fix(inspect.histogram): handle rmsnorm / layernorm canvas nodes
Before: clicking 'Inspect weight histogram' on a rmsnorm or layernorm node in BrickContextPanel returned the dispatcher's generic 'Invalid params' chip because the handler raised ValueError when the brick's kind was outside BLOCK_BUILDERS. Norm primitives live behind _make_norm, not the block-builder registry, so any user with a norm tail on the canvas (every tiny_aya / parallel-block preset) hit this. Fix: branch in inspect_histogram — * BLOCK_BUILDERS[kind] → existing path * rmsnorm / layernorm → _make_norm(kind, hidden, eps) * residual → explicit 'no trainable weights' message instead of the generic 'unknown kind' Bonus fix: dim_env is a dict in the wire schema, but the handler was using getattr(dim_env, "H", 128) which silently always returned 128 since dicts don't have attributes. Switched to .get('H', 128). Verified end-to-end: aya_norm (rmsnorm @ H=128) now returns 128 weight values with mean = 1.0 (γ init). Tests: 3 new (rmsnorm, layernorm, residual rejection). Total inspect_histogram coverage 6/6 green.
1 parent 7908932 commit db829a4

2 files changed

Lines changed: 113 additions & 4 deletions

File tree

cppmega_v4/jsonrpc/histogram_method.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,34 @@ def inspect_histogram(params: HistogramParams,
8989
f"brick_id={params.brick_id!r} not in spec.graph.nodes"
9090
)
9191
kind = getattr(brick_node, "kind", "")
92-
if kind not in BLOCK_BUILDERS:
92+
# dim_env is a dict in the wire schema — use mapping access, not
93+
# getattr (which would always return the default for a dict).
94+
dim_env = params.spec.dim_env or {}
95+
hidden = int(dim_env.get("H", 128)) if isinstance(dim_env, dict) \
96+
else int(getattr(dim_env, "H", 128))
97+
brick_params = dict(getattr(brick_node, "params", {}) or {})
98+
if kind in BLOCK_BUILDERS:
99+
module = BLOCK_BUILDERS[kind](hidden, brick_params)
100+
elif kind in {"rmsnorm", "layernorm"}:
101+
# Norm primitives aren't in BLOCK_BUILDERS — instantiate
102+
# directly so 'Inspect weight histogram' works on canvas norm
103+
# nodes (e.g. tiny_aya rmsnorm tail). eps defaults to 1e-5.
104+
from cppmega_v4.models.unified_superblock_v4 import _make_norm
105+
eps = float(brick_params.get("eps", 1e-5))
106+
module = _make_norm(kind, hidden, eps)
107+
if module is None:
108+
raise ValueError(
109+
f"brick_id={params.brick_id!r} norm kind={kind!r} "
110+
f"resolved to a no-op (no weights to inspect)")
111+
elif kind == "residual":
112+
# Pure plumbing node — no parameters to histogram.
113+
raise ValueError(
114+
f"brick_id={params.brick_id!r} kind='residual' has no "
115+
f"trainable weights (residual is a join, not a module)")
116+
else:
93117
raise ValueError(
94118
f"brick_id={params.brick_id!r} has unknown kind={kind!r}"
95119
)
96-
hidden = int(getattr(params.spec.dim_env, "H", 128))
97-
brick_params = dict(getattr(brick_node, "params", {}) or {})
98-
module = BLOCK_BUILDERS[kind](hidden, brick_params)
99120
flat_parts: list[mx.array] = []
100121
for _k, v in nn.utils.tree_flatten(module.parameters()):
101122
if hasattr(v, "shape"):

tests/v4/test_inspect_histogram.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,91 @@ def test_v7_h08_bucket_clip_64():
5656
# Bins are monotonic.
5757
for a, b in zip(r.bins, r.bins[1:]):
5858
assert a < b
59+
60+
61+
def test_histogram_handles_rmsnorm_node():
62+
"""fhxg: Inspect weight histogram on a `rmsnorm` canvas node must
63+
succeed — the kind is a norm primitive (not in BLOCK_BUILDERS) and
64+
used to surface 'Invalid params' before the fallback was added.
65+
66+
RMSNorm has `dim` channels initialised to 1.0 (gamma), so the
67+
histogram is a single peak at 1.0.
68+
"""
69+
from cppmega_v4.jsonrpc.histogram_method import (
70+
HistogramParams, inspect_histogram,
71+
)
72+
from cppmega_v4.jsonrpc.schema import VerifyParams
73+
spec = VerifyParams.model_validate({
74+
"graph": {
75+
"nodes": [
76+
{"id": "a", "kind": "attention", "params": {}},
77+
{"id": "n", "kind": "rmsnorm", "params": {}},
78+
],
79+
"edges": [{"src": "a", "dst": "n"}],
80+
},
81+
"dim_env": {"H": 128},
82+
"loss": {"kind": "cross_entropy", "head_outputs": ["n"]},
83+
"optim": {"kind": "adamw", "groups": [
84+
{"matcher": "all", "lr": 1e-3, "weight_decay": 0.01,
85+
"betas": [0.9, 0.95]}]},
86+
"sharding": None,
87+
"training": True,
88+
})
89+
r = inspect_histogram(HistogramParams(
90+
spec=spec, brick_id="n", kind="weight", buckets=32))
91+
assert r.n_values == 128
92+
assert abs(r.mean - 1.0) < 1e-6
93+
94+
95+
def test_histogram_handles_layernorm_node():
96+
from cppmega_v4.jsonrpc.histogram_method import (
97+
HistogramParams, inspect_histogram,
98+
)
99+
from cppmega_v4.jsonrpc.schema import VerifyParams
100+
spec = VerifyParams.model_validate({
101+
"graph": {
102+
"nodes": [
103+
{"id": "a", "kind": "attention", "params": {}},
104+
{"id": "n", "kind": "layernorm", "params": {}},
105+
],
106+
"edges": [{"src": "a", "dst": "n"}],
107+
},
108+
"dim_env": {"H": 64},
109+
"loss": {"kind": "cross_entropy", "head_outputs": ["n"]},
110+
"optim": {"kind": "adamw", "groups": [
111+
{"matcher": "all", "lr": 1e-3, "weight_decay": 0.01,
112+
"betas": [0.9, 0.95]}]},
113+
"sharding": None,
114+
"training": True,
115+
})
116+
r = inspect_histogram(HistogramParams(
117+
spec=spec, brick_id="n", kind="weight", buckets=16))
118+
# LayerNorm has gamma + beta = 2 * H channels.
119+
assert r.n_values == 2 * 64
120+
121+
122+
def test_histogram_residual_kind_rejected_cleanly():
123+
import pytest
124+
from cppmega_v4.jsonrpc.histogram_method import (
125+
HistogramParams, inspect_histogram,
126+
)
127+
from cppmega_v4.jsonrpc.schema import VerifyParams
128+
spec = VerifyParams.model_validate({
129+
"graph": {
130+
"nodes": [
131+
{"id": "a", "kind": "attention", "params": {}},
132+
{"id": "r", "kind": "residual", "params": {}},
133+
],
134+
"edges": [{"src": "a", "dst": "r"}],
135+
},
136+
"dim_env": {"H": 64},
137+
"loss": {"kind": "cross_entropy", "head_outputs": ["r"]},
138+
"optim": {"kind": "adamw", "groups": [
139+
{"matcher": "all", "lr": 1e-3, "weight_decay": 0.01,
140+
"betas": [0.9, 0.95]}]},
141+
"sharding": None,
142+
"training": True,
143+
})
144+
with pytest.raises(ValueError, match="no trainable weights"):
145+
inspect_histogram(HistogramParams(
146+
spec=spec, brick_id="r", kind="weight", buckets=8))

0 commit comments

Comments
 (0)