analyze: add MoE router gating/share diagnostic plots
New "model" family in the gallery: router_gating (mean soft gate weight vs. pre-step energy, showing the router's soft decision boundaries) and router_share_by_pdg/router_share_by_process (stacked top-1 dispatch share by species / true physics process). Needs a live checkpoint's Router, so it's a documented exception to the rest of the package's polars/numpy-only contract; gracefully degrades to a placeholder for non-MoE checkpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""Tests for the MoE router-gating diagnostic (giant.analysis.router_gating)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import polars as pl
|
||||
import torch
|
||||
|
||||
from giant.analysis.router_gating import (
|
||||
compute_router_gating,
|
||||
compute_router_share_by_pdg,
|
||||
compute_router_share_by_process,
|
||||
)
|
||||
from giant.data.transforms import Normalizer
|
||||
from giant.model.network import build_models
|
||||
|
||||
_PDG_MAP = {11: 0, 22: 1}
|
||||
_MAT_MAP = {"G4_PbWO4": 0, "G4_Pb": 1}
|
||||
|
||||
|
||||
def _model_cfg() -> dict:
|
||||
return {
|
||||
"router": {
|
||||
"enabled": True,
|
||||
"type": "energy",
|
||||
"n_experts": 2,
|
||||
"temperature": 0.5,
|
||||
"learn_centers": True,
|
||||
"energy_idx": 3,
|
||||
},
|
||||
"pdg_vocab": len(_PDG_MAP),
|
||||
"mat_vocab": len(_MAT_MAP),
|
||||
"conditioning": "embedding",
|
||||
}
|
||||
|
||||
|
||||
def _write_checkpoint(tmp_path) -> str:
|
||||
cfg = _model_cfg()
|
||||
stage1, _ = build_models(cfg)
|
||||
norm = Normalizer()
|
||||
norm.mean = np.zeros(15, dtype=np.float32)
|
||||
norm.std = np.ones(15, dtype=np.float32)
|
||||
ckpt = {
|
||||
"model_config": cfg,
|
||||
"model": stage1.state_dict(),
|
||||
"pdg_map": _PDG_MAP,
|
||||
"mat_map": _MAT_MAP,
|
||||
"normalizer": {"cond": norm.to_dict()},
|
||||
}
|
||||
path = tmp_path / "ckpt.pt"
|
||||
torch.save(ckpt, path)
|
||||
return str(path)
|
||||
|
||||
|
||||
def _steps_frame(process: bool = False) -> pl.LazyFrame:
|
||||
n = 40
|
||||
rng = np.random.default_rng(0)
|
||||
pre_e = np.concatenate([rng.uniform(1, 10, n // 2), rng.uniform(100, 1000, n // 2)])
|
||||
pdg = np.where(np.arange(n) % 2 == 0, 11, 22)
|
||||
material = np.where(np.arange(n) % 3 == 0, "G4_Pb", "G4_PbWO4")
|
||||
data = {
|
||||
"event_id": np.arange(n),
|
||||
"pdg": pdg,
|
||||
"pre_x": np.zeros(n),
|
||||
"pre_y": np.zeros(n),
|
||||
"pre_z": np.zeros(n),
|
||||
"pre_E": pre_e,
|
||||
"pre_dx": np.zeros(n),
|
||||
"pre_dy": np.zeros(n),
|
||||
"pre_dz": np.ones(n),
|
||||
"post_x": np.zeros(n),
|
||||
"post_y": np.zeros(n),
|
||||
"post_z": np.ones(n),
|
||||
"post_E": pre_e * 0.5,
|
||||
"post_dx": np.zeros(n),
|
||||
"post_dy": np.zeros(n),
|
||||
"post_dz": np.ones(n),
|
||||
"edep": pre_e * 0.5,
|
||||
"step_length": np.ones(n),
|
||||
"material": material,
|
||||
"layer_id": np.zeros(n, dtype=np.int64),
|
||||
}
|
||||
if process:
|
||||
data["process"] = np.where(pdg == 11, "eIoni", "compt")
|
||||
return pl.DataFrame(data).lazy()
|
||||
|
||||
|
||||
def test_compute_router_gating_shapes(tmp_path):
|
||||
checkpoint = _write_checkpoint(tmp_path)
|
||||
lf = _steps_frame()
|
||||
r = compute_router_gating(checkpoint, lf, lf)
|
||||
assert r.kind == "router_gating"
|
||||
assert r.payload["n_experts"] == 2
|
||||
for side in ("rollout", "reference"):
|
||||
means = r.payload[side]["means"]
|
||||
assert means, f"{side} produced no bins"
|
||||
assert all(abs(sum(row) - 1.0) < 1e-5 for row in means)
|
||||
|
||||
|
||||
def test_compute_router_gating_missing_checkpoint_is_unavailable():
|
||||
lf = _steps_frame()
|
||||
r = compute_router_gating(None, lf, lf)
|
||||
assert r.kind == "unavailable"
|
||||
assert "note" in r.payload
|
||||
assert r.title
|
||||
|
||||
|
||||
def test_compute_router_share_by_pdg(tmp_path):
|
||||
checkpoint = _write_checkpoint(tmp_path)
|
||||
lf = _steps_frame()
|
||||
r = compute_router_share_by_pdg(checkpoint, lf, lf, top_pdgs=[11, 22])
|
||||
assert r.kind == "router_share"
|
||||
for side in ("rollout", "reference"):
|
||||
assert set(r.payload[side]) == {"e-", "gamma"}
|
||||
for shares in r.payload[side].values():
|
||||
assert abs(sum(shares) - 1.0) < 1e-5
|
||||
|
||||
|
||||
def test_compute_router_share_by_process(tmp_path):
|
||||
checkpoint = _write_checkpoint(tmp_path)
|
||||
lf = _steps_frame(process=True)
|
||||
r = compute_router_share_by_process(checkpoint, lf)
|
||||
assert r.kind == "router_share"
|
||||
assert set(r.payload["categories"]) <= {"eIoni", "compt"}
|
||||
for shares in r.payload["reference"].values():
|
||||
assert abs(sum(shares) - 1.0) < 1e-5
|
||||
Reference in New Issue
Block a user