Deduplicate n_sec_head/type_head MLPs into build_mlp_head (gitea #36)
CI / Format (ruff format) (push) Successful in 28s
CI / Lint (ruff check) (push) Successful in 29s
CI / Sync project version with tag (push) Has been skipped
CI / Lint (ruff check) (pull_request) Successful in 34s
CI / Type check (ty) (push) Successful in 37s
CI / Format (ruff format) (pull_request) Successful in 44s
CI / Type check (ty) (pull_request) Successful in 46s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Tests (pull_request) Successful in 4m5s
CI / Tests (push) Successful in 4m6s
CI / Format (ruff format) (push) Successful in 28s
CI / Lint (ruff check) (push) Successful in 29s
CI / Sync project version with tag (push) Has been skipped
CI / Lint (ruff check) (pull_request) Successful in 34s
CI / Type check (ty) (push) Successful in 37s
CI / Format (ruff format) (pull_request) Successful in 44s
CI / Type check (ty) (pull_request) Successful in 46s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Tests (pull_request) Successful in 4m5s
CI / Tests (push) Successful in 4m6s
The same two-layer classifier head (Linear(cond_out_dim, hidden_dim // 2)
-> SiLU -> Linear(hidden_dim // 2, out_dim)) was hand-rolled five times in
giant/model/models.py: Stage1Model.n_sec_head, Stage2OneShot.n_sec_head/
.type_head, and Stage2Autoregressive.n_sec_head/.type_head. The `// 2`
ratio and fixed 2-layer depth were undocumented magic numbers, and both
n_sec accuracy and secondary-species accuracy are known weak spots that
were untunable independently of the trunk they hang off.
Adds `build_mlp_head(in_dim, out_dim, hidden, depth, act)` to
giant/model/layers.py (depth=1 is a bare Linear; depth>=2 matches the old
hardcoded shape exactly), and a new `HeadConfig` (hidden_ratio, depth)
dataclass in giant/config.py, wired in as `stage1_model.heads.n_sec` and
`stage2_model.heads.{n_sec,type}` — split per head type (not one shared
block per stage) since n_sec and species prediction are called out as
separate weak spots that may want independent capacity. Defaults
(hidden_ratio=0.5, depth=2) reproduce the old hardcoded architecture
bit-for-bit, so every existing config.toml and migrated v0.2 checkpoint
is unaffected; no changes were needed to migrate_config or the legacy
migration surfaces. No new CLI flags, matching how other nested
sub-config (router.*, trunk.*) is set via config.toml rather than
per-field flags.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -93,6 +93,42 @@ def test_stage1_model_no_n_sec_head_by_default():
|
||||
assert model.n_sec_head is None
|
||||
|
||||
|
||||
def test_stage1_model_n_sec_head_default_cfg_matches_pre_gitea_36_shape():
|
||||
"""No n_sec_head_cfg given must reproduce the old hardcoded
|
||||
hidden_dim // 2, one-hidden-layer architecture exactly (gitea #36)."""
|
||||
model = Stage1Model(
|
||||
pdg_vocab=3,
|
||||
mat_vocab=2,
|
||||
particle_cfg=PARTICLE_CFG,
|
||||
material_cfg=MATERIAL_CFG,
|
||||
hidden_dim=40,
|
||||
cond_out_dim=12,
|
||||
n_sec_head_k_max=15,
|
||||
)
|
||||
assert model.n_sec_head is not None
|
||||
assert len(model.n_sec_head) == 3
|
||||
assert model.n_sec_head[0].in_features == 12
|
||||
assert model.n_sec_head[0].out_features == 20 # hidden_dim // 2
|
||||
assert model.n_sec_head[2].out_features == 16 # k_max + 1
|
||||
|
||||
|
||||
def test_stage1_model_n_sec_head_cfg_controls_hidden_width_and_depth():
|
||||
model = Stage1Model(
|
||||
pdg_vocab=3,
|
||||
mat_vocab=2,
|
||||
particle_cfg=PARTICLE_CFG,
|
||||
material_cfg=MATERIAL_CFG,
|
||||
hidden_dim=32,
|
||||
cond_out_dim=16,
|
||||
n_sec_head_k_max=15,
|
||||
n_sec_head_cfg={"hidden_ratio": 0.25, "depth": 1},
|
||||
)
|
||||
assert model.n_sec_head is not None
|
||||
assert len(model.n_sec_head) == 1
|
||||
assert model.n_sec_head[0].in_features == 16
|
||||
assert model.n_sec_head[0].out_features == 16
|
||||
|
||||
|
||||
# --- cat_col_layout / stage2_type_dim / stage2_trunk_sec_dim ---------------
|
||||
|
||||
|
||||
@@ -268,6 +304,38 @@ def test_stage2_oneshot_predict_type_raises_when_no_type_head():
|
||||
pass
|
||||
|
||||
|
||||
def test_stage2_oneshot_n_sec_head_and_type_head_cfg_control_hidden_width_and_depth():
|
||||
"""gitea #36: n_sec_head_cfg/type_head_cfg are independently tunable."""
|
||||
k_max, emb_dim = 5, 6
|
||||
particle_cfg = {"type": "onehot", "emb_dim": emb_dim, "n_layers": 1}
|
||||
sec_dim = stage2_trunk_sec_dim({"target": "onehot"}, "flow", k_max, emb_dim)
|
||||
model = Stage2OneShot(
|
||||
pdg_vocab=5,
|
||||
mat_vocab=3,
|
||||
particle_cfg=particle_cfg,
|
||||
material_cfg=MATERIAL_CFG,
|
||||
hidden_dim=40,
|
||||
n_res_blocks=1,
|
||||
cond_out_dim=12,
|
||||
context_dim=8,
|
||||
sec_dim=sec_dim,
|
||||
generator="flow",
|
||||
k_max=k_max,
|
||||
particle_type_cfg={"target": "onehot", "lambda": 1.0},
|
||||
n_sec_head_cfg={"hidden_ratio": 0.25, "depth": 1},
|
||||
type_head_cfg={"hidden_ratio": 0.75, "depth": 2},
|
||||
)
|
||||
assert model.n_sec_head is not None
|
||||
assert len(model.n_sec_head) == 1
|
||||
assert model.n_sec_head[0].in_features == 12
|
||||
assert model.n_sec_head[0].out_features == k_max + 1
|
||||
|
||||
assert model.type_head is not None
|
||||
assert len(model.type_head) == 3
|
||||
assert model.type_head[0].out_features == 30 # round(40 * 0.75)
|
||||
assert model.type_head[2].out_features == k_max * emb_dim
|
||||
|
||||
|
||||
def test_stage2_oneshot_forward_shape_onehot_wgan():
|
||||
B, k_max, emb_dim = 4, 5, 6
|
||||
model = _build_stage2("onehot", "wgan", emb_dim=emb_dim)
|
||||
@@ -518,6 +586,37 @@ def test_stage2_autoregressive_particle_type_n_classes_overrides_conditioning_em
|
||||
assert model.type_head[-1].out_features == 20
|
||||
|
||||
|
||||
def test_stage2_autoregressive_n_sec_head_and_type_head_cfg_control_hidden_width_and_depth():
|
||||
"""gitea #36, Stage2Autoregressive side — see the Stage2OneShot version
|
||||
of this test for the full rationale."""
|
||||
particle_cfg = {"type": "physical", "emb_dim": 6, "n_layers": 1}
|
||||
particle_type_cfg = {"target": "onehot", "lambda": 1.0}
|
||||
model = Stage2Autoregressive(
|
||||
pdg_vocab=5,
|
||||
mat_vocab=3,
|
||||
particle_cfg=particle_cfg,
|
||||
material_cfg=MATERIAL_CFG,
|
||||
hidden_dim=40,
|
||||
n_res_blocks=1,
|
||||
cond_out_dim=12,
|
||||
context_dim=8,
|
||||
generator="flow",
|
||||
k_max=5,
|
||||
particle_type_cfg=particle_type_cfg,
|
||||
n_sec_head_cfg={"hidden_ratio": 0.25, "depth": 1},
|
||||
type_head_cfg={"hidden_ratio": 0.75, "depth": 2},
|
||||
)
|
||||
assert model.n_sec_head is not None
|
||||
assert len(model.n_sec_head) == 1
|
||||
assert model.n_sec_head[0].in_features == 12
|
||||
assert model.n_sec_head[0].out_features == 6 # k_max + 1
|
||||
|
||||
assert model.type_head is not None
|
||||
assert len(model.type_head) == 3
|
||||
assert model.type_head[0].out_features == 30 # round(40 * 0.75)
|
||||
assert model.type_head[2].out_features == model.type_dim
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", ["physical", "onehot", "embedding"])
|
||||
@pytest.mark.parametrize("generator", ["wgan", "flow"])
|
||||
@pytest.mark.parametrize("history", ["markov", "attention"])
|
||||
@@ -813,6 +912,39 @@ def test_build_models_omitted_decoder_and_particle_type_match_default_config():
|
||||
assert built["stage2"].particle_type_cfg["target"] == "onehot"
|
||||
|
||||
|
||||
def test_build_models_custom_heads_block_controls_head_shapes():
|
||||
"""gitea #36: stage{1,2}_model.heads flows all the way from config dict
|
||||
through build_models to the actual constructed head shapes."""
|
||||
cfg = _partial_model_config()
|
||||
cfg["stage1_model"] = {
|
||||
"active": True,
|
||||
"hidden_dim": 40,
|
||||
"n_res_blocks": 1,
|
||||
"heads": {"n_sec": {"hidden_ratio": 0.25, "depth": 1}},
|
||||
}
|
||||
cfg["stage2_model"]["decoder"] = "one_shot"
|
||||
cfg["stage2_model"]["generator"] = "flow" # wgan folds the type slice; no separate type_head
|
||||
cfg["stage2_model"]["n_sec"] = {"owner": "stage1"}
|
||||
cfg["stage2_model"]["heads"] = {
|
||||
"n_sec": {"hidden_ratio": 0.25, "depth": 1},
|
||||
"type": {"hidden_ratio": 0.75, "depth": 2},
|
||||
}
|
||||
|
||||
built = build_models(cfg)
|
||||
stage1, stage2 = built["stage1"], built["stage2"]
|
||||
assert stage1 is not None
|
||||
assert stage2 is not None
|
||||
|
||||
assert stage1.n_sec_head is not None
|
||||
assert len(stage1.n_sec_head) == 1 # owner=stage1, so stage1 builds it
|
||||
assert stage2.n_sec_head is None # owner=stage1, so stage2 doesn't
|
||||
|
||||
assert isinstance(stage2, Stage2OneShot)
|
||||
assert stage2.type_head is not None
|
||||
assert len(stage2.type_head) == 3
|
||||
assert stage2.type_head[0].out_features == 6 # round(8 * 0.75)
|
||||
|
||||
|
||||
def test_build_critics_omitted_particle_type_matches_default_config():
|
||||
cfg = _partial_model_config()
|
||||
cfg["stage2_model"]["generator"] = "wgan"
|
||||
|
||||
Reference in New Issue
Block a user