9112e845e0
CI / Lint (ruff check) (push) Successful in 27s
CI / Format (ruff format) (push) Successful in 28s
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 23s
CI / Tests (push) Successful in 1m1s
CI / Lint (ruff check) (pull_request) Successful in 26s
CI / Format (ruff format) (pull_request) Successful in 27s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Type check (ty) (pull_request) Successful in 27s
CI / Tests (pull_request) Successful in 1m2s
Replaces train.py's single global training loop with a StageTrainer hierarchy (FlowDDPMStageTrainer, WGANStageTrainer) — one per active stage, each owning its own optimizer/LR schedule/EMA and reading only the shared batch tuple (stage 2 always teacher-forces on the ground-truth x1_s1, so stages never need each other's output at train time). Supports every stage1/stage2 generator combination, including the design doc's headline mixed case (stage1=flow + stage2=wgan) and its reverse, plus stage1-only/stage2-only ablation runs, routed+gumbel stages, and checkpoint save/resume. metrics.csv/wandb logging are stage-prefixed. validate_marginals calls are guarded with a one-time warning and a Wasserstein-magnitude fallback for wgan best-checkpoint selection, since giant/sample.py still assumes stage1 always owns n_sec_head (decision 1 moved it to stage 2 by default) — deferred to design doc step 6, not silently papered over. pipeline.py's run_setup_stage/run_train_job now read the new nested config directly; the dangling resolve_expert_dims call and the --mode wgan --router rejection are both gone (routed WGAN works). cli.py's train/new-run build correctly-shaped config overrides (architecture flags -> stage1_model only per the approved decision; --mode/--n-critic/--gp-weight/--critic-lr broadcast to both stages, matching migrate_config's own precedent and avoiding a regression on the common --mode case); predict/rollout's dangling build_models tuple-unpack is fixed; new-run now tags config_version, fixing a bug where a re-loaded v0.3 config.toml would have been silently corrupted by migrate_config mistaking it for v0.2. config.py's validate_config rejects mixed particle/material conditioning types for now (ConditionEncoder supports it, the data pipeline in giant/data/transforms.py doesn't yet). analysis/render.py and router_gating.py handle both the new nested model_config shape and legacy flat checkpoints. scripts/warm_setup_cache.py updated for run_setup_stage's new signature. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
128 lines
3.8 KiB
Python
128 lines
3.8 KiB
Python
"""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)["stage1"]
|
|
assert stage1 is not None
|
|
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
|