cc9646f279
CI / Lint (ruff check) (push) Successful in 37s
CI / Format (ruff format) (push) Successful in 38s
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 27s
CI / Tests (push) Successful in 2m34s
CI / Lint (ruff check) (pull_request) Successful in 30s
CI / Format (ruff format) (pull_request) Successful in 30s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Type check (ty) (pull_request) Successful in 29s
CI / Tests (pull_request) Successful in 2m35s
giant model summary --config config.toml builds the resolved Stage1/Stage2 graph from a config with no dataset attached (pdg_vocab/mat_vocab are supplied as placeholders via --pdg-vocab/--mat-vocab, since the real training vocab is dataset-derived) and prints per-module parameter counts, trunk in/out widths, which heads exist, and which conditioning/stage1_model/stage2_model config keys actually shaped the build. The consumed-keys half uses differential probing rather than static identifier matching: build once for a fingerprint (submodule presence, every parameter's/buffer's shape+dtype, every plain scalar attribute a module stores on itself), then perturb one leaf at a time, rebuild, and compare. A changed fingerprint (or a raise) means the key is consumed; no change means it's inert *under this particular config* -- e.g. any stage1_model.router.* key when router.enabled=false. A curated _NOT_BUILD_TIME table separates keys legitimately owned by the trainer/sampler/rollout (loss weights, WGAN-GP hyperparameters, teacher-forcing schedules) from genuinely-inert ones, verified against those call sites. A few config keys branch on equality against one specific string literal (n_sec.owner=="stage1", n_sec.mode=="stop_token", particle_type.target=="physical"); a single generic sentinel probe missed all three since the config's current value and the sentinel landed in the same branch, so those three leaves get their real alternative value tried too (_STRING_ALTERNATIVES). giant.config.leaf_paths is promoted out of tests/test_config_consumed_keys.py (previously a private test-local duplicate) so both audits -- the static per-identifier one and this new runtime per-config one -- walk the exact same DEFAULT_CONFIG tree. ExpertTrunk/RoutedTrunk now also expose in_dim (out_dim already existed), needed to report trunk widths generically. Decisions made during planning: --pdg-vocab/--mat-vocab default to 300 and len(MATERIAL_PROPERTIES); the consumed-keys report is scoped to conditioning/stage1_model/stage2_model only (train/meta are out of scope for a model-only build); the module tree prints every submodule at any depth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
5.8 KiB
Python
138 lines
5.8 KiB
Python
"""Tests for `giant model summary` (gitea #46)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from giant import config as gconfig
|
|
from giant.cli import app
|
|
from giant.materials import MATERIAL_PROPERTIES
|
|
from giant.model.summary import _NOT_BUILD_TIME, _built_modules, _vocab_caveats, summarize_model
|
|
|
|
runner = CliRunner()
|
|
|
|
_PDG_VOCAB = 300
|
|
_MAT_VOCAB = len(MATERIAL_PROPERTIES)
|
|
|
|
|
|
def _cfg(overrides: dict | None = None) -> dict:
|
|
return gconfig.merge_cli_overrides(gconfig.DEFAULT_CONFIG, None, overrides or {})
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def default_summary():
|
|
return summarize_model(_cfg(), pdg_vocab=_PDG_VOCAB, mat_vocab=_MAT_VOCAB)
|
|
|
|
|
|
def test_default_config_builds_both_stages_with_a_real_tree(default_summary):
|
|
assert set(default_summary.modules) >= {"stage1", "stage2"}
|
|
for module in default_summary.modules.values():
|
|
assert sum(p.numel() for p in module.parameters()) > 0
|
|
stage1 = default_summary.modules["stage1"]
|
|
assert hasattr(stage1, "cond_enc")
|
|
assert hasattr(stage1, "trunk")
|
|
assert {"input_proj", "blocks", "out_proj"} <= {n for n, _ in stage1.trunk.named_children()}
|
|
|
|
|
|
def test_every_in_scope_leaf_is_classified(default_summary):
|
|
in_scope = {
|
|
p
|
|
for p in gconfig.leaf_paths(gconfig.DEFAULT_CONFIG)
|
|
if p.split(".", 1)[0] in ("conditioning", "stage1_model", "stage2_model")
|
|
}
|
|
classified = set(default_summary.consumed) | set(default_summary.inert) | set(default_summary.elsewhere)
|
|
assert classified == in_scope
|
|
|
|
|
|
def test_not_build_time_allow_list_has_no_stale_entries():
|
|
in_scope = set(gconfig.leaf_paths(gconfig.DEFAULT_CONFIG))
|
|
stale = set(_NOT_BUILD_TIME) - in_scope
|
|
assert not stale, f"_NOT_BUILD_TIME entries no longer in DEFAULT_CONFIG: {sorted(stale)}"
|
|
|
|
|
|
def test_router_disabled_by_default_so_its_fields_are_inert(default_summary):
|
|
assert "stage1_model.router.n_experts" in default_summary.inert
|
|
assert "stage1_model.router.temperature" in default_summary.inert
|
|
|
|
|
|
def test_markov_history_leaves_attention_dims_inert_but_history_itself_consumed(default_summary):
|
|
assert "stage2_model.autoregressive.attn_n_heads" in default_summary.inert
|
|
assert "stage2_model.autoregressive.attn_n_layers" in default_summary.inert
|
|
assert "stage2_model.autoregressive.history" in default_summary.consumed
|
|
|
|
|
|
def test_single_literal_branch_fields_are_correctly_seen_as_consumed(default_summary):
|
|
"""Regression guard: n_sec.owner ("stage2"), n_sec.mode ("head") and
|
|
particle_type.target ("onehot") each branch as `== "one specific other
|
|
literal"` in giant/model/builders.py|models.py. A naive single generic
|
|
sentinel probe lands in the same "not that literal" bucket as the
|
|
current value and never crosses the boundary that actually matters --
|
|
this is exactly what _STRING_ALTERNATIVES exists to fix."""
|
|
assert "stage2_model.n_sec.owner" in default_summary.consumed
|
|
assert "stage2_model.n_sec.mode" in default_summary.consumed
|
|
assert "stage2_model.particle_type.target" in default_summary.consumed
|
|
|
|
|
|
def test_stage1_wgan_generator_swaps_flow_time_dim_for_critic_dims():
|
|
summary = summarize_model(_cfg({"stage1_model": {"generator": "wgan"}}), pdg_vocab=_PDG_VOCAB, mat_vocab=_MAT_VOCAB)
|
|
assert "stage1_model.flow.time_dim" in summary.inert
|
|
assert "stage1_model.wgan.noise_dim" in summary.consumed
|
|
assert "stage1_model.wgan.critic_hidden_dim" in summary.consumed
|
|
|
|
|
|
def test_stage2_one_shot_decoder_makes_autoregressive_block_inert():
|
|
summary = summarize_model(
|
|
_cfg({"stage2_model": {"decoder": "one_shot"}}), pdg_vocab=_PDG_VOCAB, mat_vocab=_MAT_VOCAB
|
|
)
|
|
assert "stage2_model.autoregressive.history" in summary.inert
|
|
assert "history_encoder" not in {n for n, _ in summary.modules["stage2"].named_children()}
|
|
|
|
|
|
def test_energy_router_enabled_consumes_core_fields_but_not_process_only_fields():
|
|
summary = summarize_model(
|
|
_cfg({"stage1_model": {"router": {"enabled": True, "type": "energy", "n_experts": 4}}}),
|
|
pdg_vocab=_PDG_VOCAB,
|
|
mat_vocab=_MAT_VOCAB,
|
|
)
|
|
assert "stage1_model.router.n_experts" in summary.consumed
|
|
assert "stage1_model.router.temperature" in summary.consumed
|
|
# emb_dim/hidden_dim are pdg/process-router-only kwargs -- build_router's
|
|
# signature filter drops them for an energy router.
|
|
assert "stage1_model.router.hidden_dim" in summary.inert
|
|
assert "stage1_model.router.emb_dim" in summary.inert
|
|
|
|
|
|
def test_vocab_caveat_text_for_embedding_particle_conditioning():
|
|
cfg = _cfg({"conditioning": {"particle": {"type": "embedding"}}})
|
|
caveats = _vocab_caveats(cfg)
|
|
assert any("pdg_vocab" in c and "embedding" in c for c in caveats)
|
|
assert not any("mat_vocab" in c for c in caveats)
|
|
|
|
|
|
def test_pdg_vocab_flag_changes_embedding_table_size():
|
|
cfg = _cfg({"conditioning": {"particle": {"type": "embedding"}}})
|
|
small = _built_modules(cfg, pdg_vocab=10, mat_vocab=_MAT_VOCAB)
|
|
big = _built_modules(cfg, pdg_vocab=1000, mat_vocab=_MAT_VOCAB)
|
|
assert big["stage1"].cond_enc.pdg_emb.weight.numel() > small["stage1"].cond_enc.pdg_emb.weight.numel()
|
|
|
|
|
|
def test_invalid_combo_exits_nonzero_with_validate_config_message(tmp_path: Path):
|
|
config_path = tmp_path / "bad.toml"
|
|
config_path.write_text('[meta]\nconfig_version = 3\n\n[stage2_model.particle_type]\ntarget = "embedding"\n')
|
|
result = runner.invoke(app, ["model", "summary", "--config", str(config_path)])
|
|
assert result.exit_code == 1
|
|
assert "requires conditioning.particle.type = 'embedding'" in result.output
|
|
|
|
|
|
def test_cli_default_smoke():
|
|
result = runner.invoke(app, ["model", "summary"])
|
|
assert result.exit_code == 0, result.output
|
|
assert "stage1" in result.output
|
|
assert "stage2" in result.output
|
|
assert "parameters" in result.output
|
|
assert "trunk" in result.output
|
|
assert "inert under this config" in result.output
|