Compare commits
2 Commits
01acbfed61
...
6a21c3b908
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a21c3b908 | |||
| 2bfb1ab056 |
+74
-201
@@ -653,141 +653,57 @@ def train(
|
|||||||
)
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
cli_train = {
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"epochs": epochs,
|
|
||||||
"batch_size": batch_size_value,
|
|
||||||
"lr": lr,
|
|
||||||
"weight_decay": weight_decay,
|
|
||||||
"ema_decay": ema_decay,
|
|
||||||
"warmup_epochs": warmup_epochs,
|
|
||||||
"val_fraction": val_fraction,
|
|
||||||
"num_workers": num_workers,
|
|
||||||
"seed": seed,
|
|
||||||
"validate_every": validate_every,
|
|
||||||
"validate_steps": validate_steps,
|
|
||||||
"max_val_batches": max_val_batches,
|
|
||||||
"wandb": wandb,
|
|
||||||
"wandb_project": wandb_project,
|
|
||||||
"wandb_run_name": wandb_run_name,
|
|
||||||
"wandb_log_every": wandb_log_every,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
# --hidden-dim/--n-blocks/--dropout are stage-1-only shorthands kept for
|
|
||||||
# backward compatibility (they predate stage2_model having its own
|
|
||||||
# flags); --stage1-*/--stage2-* below are the explicit, discoverable
|
|
||||||
# per-stage flags, and take precedence when both are given.
|
|
||||||
cli_stage1_model: dict[str, object] = {
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"hidden_dim": hidden_dim,
|
|
||||||
"n_res_blocks": n_blocks,
|
|
||||||
"dropout": dropout,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
cli_stage1_model.update(
|
|
||||||
{
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"hidden_dim": stage1_hidden_dim,
|
|
||||||
"n_res_blocks": stage1_n_res_blocks,
|
|
||||||
"dropout": stage1_dropout,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cli_stage2_model: dict[str, object] = {
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"hidden_dim": stage2_hidden_dim,
|
|
||||||
"n_res_blocks": stage2_n_res_blocks,
|
|
||||||
"dropout": stage2_dropout,
|
|
||||||
"decoder": stage2_decoder.value if stage2_decoder is not None else None,
|
|
||||||
"k_max": stage2_k_max,
|
|
||||||
"context_dim": stage2_context_dim,
|
|
||||||
"stage1_context": stage2_stage1_context.value if stage2_stage1_context is not None else None,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
cli_router = _router_cli_overrides(router, router_type, n_experts, router_axis)
|
cli_router = _router_cli_overrides(router, router_type, n_experts, router_axis)
|
||||||
if cli_router:
|
flag_values: dict[str, object] = {
|
||||||
cli_stage1_model["router"] = cli_router
|
"epochs": epochs,
|
||||||
|
"batch_size": batch_size_value,
|
||||||
# --emb-dim/--conditioning set both conditioning axes (v0.2 had one
|
"lr": lr,
|
||||||
# shared value for particle+material).
|
"weight_decay": weight_decay,
|
||||||
cli_conditioning: dict[str, dict] = {}
|
"ema_decay": ema_decay,
|
||||||
if emb_dim is not None:
|
"warmup_epochs": warmup_epochs,
|
||||||
cli_conditioning["particle"] = {"emb_dim": emb_dim}
|
"val_fraction": val_fraction,
|
||||||
cli_conditioning["material"] = {"emb_dim": emb_dim}
|
"num_workers": num_workers,
|
||||||
if conditioning is not None:
|
"seed": seed,
|
||||||
cli_conditioning.setdefault("particle", {})["type"] = conditioning.value
|
"validate_every": validate_every,
|
||||||
cli_conditioning.setdefault("material", {})["type"] = conditioning.value
|
"validate_steps": validate_steps,
|
||||||
|
"max_val_batches": max_val_batches,
|
||||||
overrides: dict[str, dict] = {}
|
"wandb": wandb,
|
||||||
if cli_train:
|
"wandb_project": wandb_project,
|
||||||
overrides["train"] = cli_train
|
"wandb_run_name": wandb_run_name,
|
||||||
if cli_stage1_model:
|
"wandb_log_every": wandb_log_every,
|
||||||
overrides["stage1_model"] = cli_stage1_model
|
"hidden_dim": hidden_dim,
|
||||||
if cli_stage2_model:
|
"n_blocks": n_blocks,
|
||||||
overrides["stage2_model"] = cli_stage2_model
|
"dropout": dropout,
|
||||||
if cli_conditioning:
|
"stage1_hidden_dim": stage1_hidden_dim,
|
||||||
overrides["conditioning"] = cli_conditioning
|
"stage1_n_res_blocks": stage1_n_res_blocks,
|
||||||
|
"stage1_dropout": stage1_dropout,
|
||||||
# --mode/--n-critic/--gp-weight/--critic-lr/--noise-dim apply to BOTH
|
"stage2_hidden_dim": stage2_hidden_dim,
|
||||||
# stages by default (v0.2 had one global mode/wgan config shared by both
|
"stage2_n_res_blocks": stage2_n_res_blocks,
|
||||||
# — see giant.config.migrate_config's train.mode /
|
"stage2_dropout": stage2_dropout,
|
||||||
# train.{n_critic,gp_weight,critic_lr} precedent); the --stage1-*/
|
"stage2_decoder": stage2_decoder.value if stage2_decoder is not None else None,
|
||||||
# --stage2-* variants below override a single stage independently (decision
|
"stage2_k_max": stage2_k_max,
|
||||||
# 7), which is what actually enables e.g. `--stage1-generator flow
|
"stage2_context_dim": stage2_context_dim,
|
||||||
# --stage2-generator wgan`.
|
"stage2_stage1_context": stage2_stage1_context.value if stage2_stage1_context is not None else None,
|
||||||
if mode is not None:
|
"mode": mode.value if mode is not None else None,
|
||||||
overrides.setdefault("stage1_model", {})["generator"] = mode.value
|
"stage1_generator": stage1_generator.value if stage1_generator is not None else None,
|
||||||
overrides.setdefault("stage2_model", {})["generator"] = mode.value
|
"stage2_generator": stage2_generator.value if stage2_generator is not None else None,
|
||||||
if stage1_generator is not None:
|
"conditioning": conditioning.value if conditioning is not None else None,
|
||||||
overrides.setdefault("stage1_model", {})["generator"] = stage1_generator.value
|
"emb_dim": emb_dim,
|
||||||
if stage2_generator is not None:
|
"router_config": cli_router or None,
|
||||||
overrides.setdefault("stage2_model", {})["generator"] = stage2_generator.value
|
"n_critic": n_critic,
|
||||||
|
"gp_weight": gp_weight,
|
||||||
shared_wgan_overrides = {
|
"noise_dim": noise_dim,
|
||||||
k: v
|
"critic_lr": critic_lr,
|
||||||
for k, v in {
|
"stage1_n_critic": stage1_n_critic,
|
||||||
"n_critic": n_critic,
|
"stage1_gp_weight": stage1_gp_weight,
|
||||||
"gp_weight": gp_weight,
|
"stage1_noise_dim": stage1_noise_dim,
|
||||||
"noise_dim": noise_dim,
|
"stage1_critic_lr": stage1_critic_lr,
|
||||||
"critic_lr": critic_lr,
|
"stage2_n_critic": stage2_n_critic,
|
||||||
}.items()
|
"stage2_gp_weight": stage2_gp_weight,
|
||||||
if v is not None
|
"stage2_noise_dim": stage2_noise_dim,
|
||||||
|
"stage2_critic_lr": stage2_critic_lr,
|
||||||
}
|
}
|
||||||
stage1_wgan_overrides = {
|
overrides = gconfig.overrides_from_flags(flag_values)
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"n_critic": stage1_n_critic,
|
|
||||||
"gp_weight": stage1_gp_weight,
|
|
||||||
"noise_dim": stage1_noise_dim,
|
|
||||||
"critic_lr": stage1_critic_lr,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
stage2_wgan_overrides = {
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"n_critic": stage2_n_critic,
|
|
||||||
"gp_weight": stage2_gp_weight,
|
|
||||||
"noise_dim": stage2_noise_dim,
|
|
||||||
"critic_lr": stage2_critic_lr,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
for stage_name, stage_specific in (
|
|
||||||
("stage1_model", stage1_wgan_overrides),
|
|
||||||
("stage2_model", stage2_wgan_overrides),
|
|
||||||
):
|
|
||||||
stage_wgan = {**shared_wgan_overrides, **stage_specific}
|
|
||||||
if stage_wgan:
|
|
||||||
overrides.setdefault(stage_name, {}).setdefault("wgan", {}).update(stage_wgan)
|
|
||||||
|
|
||||||
cfg = gconfig.merge_cli_overrides(gconfig.DEFAULT_CONFIG, config, overrides)
|
cfg = gconfig.merge_cli_overrides(gconfig.DEFAULT_CONFIG, config, overrides)
|
||||||
gconfig.validate_config(cfg)
|
gconfig.validate_config(cfg)
|
||||||
@@ -912,75 +828,32 @@ def new_run(
|
|||||||
(with the full dataset-derived meta section), so this scaffold's meta
|
(with the full dataset-derived meta section), so this scaffold's meta
|
||||||
section is just a placeholder recording what was asked for and when.
|
section is just a placeholder recording what was asked for and when.
|
||||||
"""
|
"""
|
||||||
cli_train = {
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"epochs": epochs,
|
|
||||||
"batch_size": batch_size,
|
|
||||||
"lr": lr,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
cli_stage1_model: dict[str, object] = {
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"hidden_dim": hidden_dim,
|
|
||||||
"n_res_blocks": n_blocks,
|
|
||||||
"dropout": dropout,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
cli_stage1_model.update(
|
|
||||||
{
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"hidden_dim": stage1_hidden_dim,
|
|
||||||
"n_res_blocks": stage1_n_res_blocks,
|
|
||||||
"dropout": stage1_dropout,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cli_stage2_model: dict[str, object] = {
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"hidden_dim": stage2_hidden_dim,
|
|
||||||
"n_res_blocks": stage2_n_res_blocks,
|
|
||||||
"dropout": stage2_dropout,
|
|
||||||
"decoder": stage2_decoder.value if stage2_decoder is not None else None,
|
|
||||||
"k_max": stage2_k_max,
|
|
||||||
"context_dim": stage2_context_dim,
|
|
||||||
"stage1_context": stage2_stage1_context.value if stage2_stage1_context is not None else None,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
cli_router = _router_cli_overrides(router, router_type, n_experts, router_axis)
|
cli_router = _router_cli_overrides(router, router_type, n_experts, router_axis)
|
||||||
if cli_router:
|
flag_values: dict[str, object] = {
|
||||||
cli_stage1_model["router"] = cli_router
|
"epochs": epochs,
|
||||||
cli_conditioning: dict[str, dict] = {}
|
"batch_size": batch_size,
|
||||||
if emb_dim is not None:
|
"lr": lr,
|
||||||
cli_conditioning["particle"] = {"emb_dim": emb_dim}
|
"hidden_dim": hidden_dim,
|
||||||
cli_conditioning["material"] = {"emb_dim": emb_dim}
|
"n_blocks": n_blocks,
|
||||||
if conditioning is not None:
|
"dropout": dropout,
|
||||||
cli_conditioning.setdefault("particle", {})["type"] = conditioning.value
|
"stage1_hidden_dim": stage1_hidden_dim,
|
||||||
cli_conditioning.setdefault("material", {})["type"] = conditioning.value
|
"stage1_n_res_blocks": stage1_n_res_blocks,
|
||||||
|
"stage1_dropout": stage1_dropout,
|
||||||
overrides: dict[str, dict] = {}
|
"stage2_hidden_dim": stage2_hidden_dim,
|
||||||
if cli_train:
|
"stage2_n_res_blocks": stage2_n_res_blocks,
|
||||||
overrides["train"] = cli_train
|
"stage2_dropout": stage2_dropout,
|
||||||
if cli_stage1_model:
|
"stage2_decoder": stage2_decoder.value if stage2_decoder is not None else None,
|
||||||
overrides["stage1_model"] = cli_stage1_model
|
"stage2_k_max": stage2_k_max,
|
||||||
if cli_stage2_model:
|
"stage2_context_dim": stage2_context_dim,
|
||||||
overrides["stage2_model"] = cli_stage2_model
|
"stage2_stage1_context": stage2_stage1_context.value if stage2_stage1_context is not None else None,
|
||||||
if cli_conditioning:
|
"mode": mode.value if mode is not None else None,
|
||||||
overrides["conditioning"] = cli_conditioning
|
"stage1_generator": stage1_generator.value if stage1_generator is not None else None,
|
||||||
if mode is not None:
|
"stage2_generator": stage2_generator.value if stage2_generator is not None else None,
|
||||||
overrides.setdefault("stage1_model", {})["generator"] = mode.value
|
"conditioning": conditioning.value if conditioning is not None else None,
|
||||||
overrides.setdefault("stage2_model", {})["generator"] = mode.value
|
"emb_dim": emb_dim,
|
||||||
if stage1_generator is not None:
|
"router_config": cli_router or None,
|
||||||
overrides.setdefault("stage1_model", {})["generator"] = stage1_generator.value
|
}
|
||||||
if stage2_generator is not None:
|
overrides = gconfig.overrides_from_flags(flag_values)
|
||||||
overrides.setdefault("stage2_model", {})["generator"] = stage2_generator.value
|
|
||||||
|
|
||||||
cfg = gconfig.merge_cli_overrides(gconfig.DEFAULT_CONFIG, config, overrides)
|
cfg = gconfig.merge_cli_overrides(gconfig.DEFAULT_CONFIG, config, overrides)
|
||||||
gconfig.validate_config(cfg)
|
gconfig.validate_config(cfg)
|
||||||
|
|||||||
+108
@@ -893,6 +893,114 @@ def _deep_merge(base: dict, override: dict) -> dict:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FlagSpec:
|
||||||
|
"""One CLI flag's mapping into the config-overrides tree.
|
||||||
|
|
||||||
|
`paths` lists every dotted config path this flag writes (>1 means fan-out
|
||||||
|
to multiple stages/axes, e.g. `--mode` -> both stages' `generator`).
|
||||||
|
`precedence` controls write order when two flags target the same path:
|
||||||
|
specs are applied in ascending precedence, so a higher-precedence (more
|
||||||
|
specific) flag overwrites a lower-precedence (shared/shorthand) one —
|
||||||
|
this is the "build a shared dict, then let a more specific dict win"
|
||||||
|
pattern `giant train`/`giant new-run` need (e.g. `--hidden-dim` vs
|
||||||
|
`--stage1-hidden-dim`, or `--n-critic` vs `--stage1-n-critic`),
|
||||||
|
generalized to one mechanism instead of three different ad hoc ones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
paths: tuple[str, ...]
|
||||||
|
precedence: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
# Flag -> config-path table shared by `giant train`/`giant new-run`
|
||||||
|
# (giant/cli.py) so both commands resolve CLI overrides identically. See
|
||||||
|
# issues.md Issue 3: this replaces ~140 lines of hand-written, imperative
|
||||||
|
# dict-building in cli.py with one declarative table plus
|
||||||
|
# `overrides_from_flags` below.
|
||||||
|
FLAG_SPECS: tuple[FlagSpec, ...] = (
|
||||||
|
# train block -- flat pass-through, unique paths, precedence irrelevant.
|
||||||
|
FlagSpec("epochs", ("train.epochs",)),
|
||||||
|
FlagSpec("batch_size", ("train.batch_size",)),
|
||||||
|
FlagSpec("lr", ("train.lr",)),
|
||||||
|
FlagSpec("weight_decay", ("train.weight_decay",)),
|
||||||
|
FlagSpec("ema_decay", ("train.ema_decay",)),
|
||||||
|
FlagSpec("warmup_epochs", ("train.warmup_epochs",)),
|
||||||
|
FlagSpec("val_fraction", ("train.val_fraction",)),
|
||||||
|
FlagSpec("num_workers", ("train.num_workers",)),
|
||||||
|
FlagSpec("seed", ("train.seed",)),
|
||||||
|
FlagSpec("validate_every", ("train.validate_every",)),
|
||||||
|
FlagSpec("validate_steps", ("train.validate_steps",)),
|
||||||
|
FlagSpec("max_val_batches", ("train.max_val_batches",)),
|
||||||
|
FlagSpec("wandb", ("train.wandb",)),
|
||||||
|
FlagSpec("wandb_project", ("train.wandb_project",)),
|
||||||
|
FlagSpec("wandb_run_name", ("train.wandb_run_name",)),
|
||||||
|
FlagSpec("wandb_log_every", ("train.wandb_log_every",)),
|
||||||
|
# --hidden-dim/--n-blocks/--dropout are stage-1-only backward-compat
|
||||||
|
# shorthands (they predate stage2_model having its own flags);
|
||||||
|
# --stage1-* wins when both are given.
|
||||||
|
FlagSpec("hidden_dim", ("stage1_model.hidden_dim",), precedence=0),
|
||||||
|
FlagSpec("stage1_hidden_dim", ("stage1_model.hidden_dim",), precedence=1),
|
||||||
|
FlagSpec("n_blocks", ("stage1_model.n_res_blocks",), precedence=0),
|
||||||
|
FlagSpec("stage1_n_res_blocks", ("stage1_model.n_res_blocks",), precedence=1),
|
||||||
|
FlagSpec("dropout", ("stage1_model.dropout",), precedence=0),
|
||||||
|
FlagSpec("stage1_dropout", ("stage1_model.dropout",), precedence=1),
|
||||||
|
# stage2-only knobs.
|
||||||
|
FlagSpec("stage2_hidden_dim", ("stage2_model.hidden_dim",)),
|
||||||
|
FlagSpec("stage2_n_res_blocks", ("stage2_model.n_res_blocks",)),
|
||||||
|
FlagSpec("stage2_dropout", ("stage2_model.dropout",)),
|
||||||
|
FlagSpec("stage2_decoder", ("stage2_model.decoder",)),
|
||||||
|
FlagSpec("stage2_k_max", ("stage2_model.k_max",)),
|
||||||
|
FlagSpec("stage2_context_dim", ("stage2_model.context_dim",)),
|
||||||
|
FlagSpec("stage2_stage1_context", ("stage2_model.stage1_context",)),
|
||||||
|
# --mode applies to both stages by default (v0.2 had one shared
|
||||||
|
# mode/wgan config); --stage{1,2}-generator override a single stage.
|
||||||
|
FlagSpec("mode", ("stage1_model.generator", "stage2_model.generator"), precedence=0),
|
||||||
|
FlagSpec("stage1_generator", ("stage1_model.generator",), precedence=1),
|
||||||
|
FlagSpec("stage2_generator", ("stage2_model.generator",), precedence=1),
|
||||||
|
# --emb-dim/--conditioning set both conditioning axes (v0.2 had one
|
||||||
|
# shared value for particle+material).
|
||||||
|
FlagSpec("conditioning", ("conditioning.particle.type", "conditioning.material.type")),
|
||||||
|
FlagSpec("emb_dim", ("conditioning.particle.emb_dim", "conditioning.material.emb_dim")),
|
||||||
|
# Pre-aggregated router override dict (built by `_router_cli_overrides`
|
||||||
|
# in cli.py from --router/--router-type/--n-experts/--router-axis).
|
||||||
|
# Router overrides only ever land on stage1_model -- this asymmetry is
|
||||||
|
# deliberate (see cli.py) and must not be "fixed" into a fan-out here.
|
||||||
|
FlagSpec("router_config", ("stage1_model.router",)),
|
||||||
|
# WGAN: shared knobs apply to both stages by default (v0.2 had one
|
||||||
|
# shared wgan config); --stage{1,2}-* override a single stage.
|
||||||
|
FlagSpec("n_critic", ("stage1_model.wgan.n_critic", "stage2_model.wgan.n_critic"), precedence=0),
|
||||||
|
FlagSpec("stage1_n_critic", ("stage1_model.wgan.n_critic",), precedence=1),
|
||||||
|
FlagSpec("stage2_n_critic", ("stage2_model.wgan.n_critic",), precedence=1),
|
||||||
|
FlagSpec("gp_weight", ("stage1_model.wgan.gp_weight", "stage2_model.wgan.gp_weight"), precedence=0),
|
||||||
|
FlagSpec("stage1_gp_weight", ("stage1_model.wgan.gp_weight",), precedence=1),
|
||||||
|
FlagSpec("stage2_gp_weight", ("stage2_model.wgan.gp_weight",), precedence=1),
|
||||||
|
FlagSpec("noise_dim", ("stage1_model.wgan.noise_dim", "stage2_model.wgan.noise_dim"), precedence=0),
|
||||||
|
FlagSpec("stage1_noise_dim", ("stage1_model.wgan.noise_dim",), precedence=1),
|
||||||
|
FlagSpec("stage2_noise_dim", ("stage2_model.wgan.noise_dim",), precedence=1),
|
||||||
|
FlagSpec("critic_lr", ("stage1_model.wgan.critic_lr", "stage2_model.wgan.critic_lr"), precedence=0),
|
||||||
|
FlagSpec("stage1_critic_lr", ("stage1_model.wgan.critic_lr",), precedence=1),
|
||||||
|
FlagSpec("stage2_critic_lr", ("stage2_model.wgan.critic_lr",), precedence=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def overrides_from_flags(values: dict[str, object]) -> dict:
|
||||||
|
"""Build the nested, section-keyed config-overrides dict
|
||||||
|
`merge_cli_overrides` expects, from `{flag_name: value}`.
|
||||||
|
|
||||||
|
Flags absent from `values`, or mapped to `None` (= not given on the
|
||||||
|
CLI), are skipped. See `FlagSpec`/`FLAG_SPECS` above for the precedence
|
||||||
|
rule applied when two flags target the same path.
|
||||||
|
"""
|
||||||
|
overrides: dict = {}
|
||||||
|
for spec in sorted(FLAG_SPECS, key=lambda s: s.precedence):
|
||||||
|
if spec.name not in values or values[spec.name] is None:
|
||||||
|
continue
|
||||||
|
for path in spec.paths:
|
||||||
|
_set_path(overrides, path, values[spec.name])
|
||||||
|
return overrides
|
||||||
|
|
||||||
|
|
||||||
# v0.2 [train] keys that pass through to v0.3 [train] unchanged (same name,
|
# v0.2 [train] keys that pass through to v0.3 [train] unchanged (same name,
|
||||||
# same meaning) when present in the loaded file — everything model-shaped
|
# same meaning) when present in the loaded file — everything model-shaped
|
||||||
# moved to the stage/conditioning blocks instead (see the rest of
|
# moved to the stage/conditioning blocks instead (see the rest of
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ architecture matrix grows, not about rot or breakage.
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 1 | Config defaults are declared twice; `DEFAULT_CONFIG` and consumers already disagree | **High** | Medium | **Fixed** (`9bf5874`) |
|
| 1 | Config defaults are declared twice; `DEFAULT_CONFIG` and consumers already disagree | **High** | Medium | **Fixed** (`9bf5874`) |
|
||||||
| 2 | No unknown-key validation — a typo in `config.toml` silently trains the wrong model | **High** | Small | **Fixed** |
|
| 2 | No unknown-key validation — a typo in `config.toml` silently trains the wrong model | **High** | Small | **Fixed** |
|
||||||
| 3 | `cli.py:train()` is a 58-parameter, 510-line fat controller | **High** | Medium | Open |
|
| 3 | `cli.py:train()` is a 58-parameter, 510-line fat controller | **High** | Medium | **Fixed** (`2bfb1ab`) |
|
||||||
| 4 | `cli.py` is at 35.8 % coverage and holds untested override-precedence logic | **High** | Medium | Open |
|
| 4 | `cli.py` is at 35.8 % coverage and holds untested override-precedence logic | **High** | Medium | **Fixed** (`2bfb1ab`, partial — see status note) |
|
||||||
| 5 | Inference bootstrap is duplicated verbatim between `predict` and `rollout` | **High** | Small | Open |
|
| 5 | Inference bootstrap is duplicated verbatim between `predict` and `rollout` | **High** | Small | Open |
|
||||||
| 6 | Two independent v0.2→v0.3 migration surfaces encode the same knowledge | Medium | Medium | Open |
|
| 6 | Two independent v0.2→v0.3 migration surfaces encode the same knowledge | Medium | Medium | Open |
|
||||||
| 7 | Positional tuple contracts (9-tuple, 7-tuple) between data, model and training layers | Medium | Small | Open |
|
| 7 | Positional tuple contracts (9-tuple, 7-tuple) between data, model and training layers | Medium | Small | Open |
|
||||||
@@ -373,6 +373,26 @@ rollout` against every model trained so far.
|
|||||||
|
|
||||||
## Issue 3 — `cli.py:train()` is a 58-parameter, 510-line fat controller
|
## Issue 3 — `cli.py:train()` is a 58-parameter, 510-line fat controller
|
||||||
|
|
||||||
|
> **Status: Fixed, commit `2bfb1ab` on `v0.3.0-stage2-autoregressive`.** `giant/config.py`
|
||||||
|
> now declares `FlagSpec` (a frozen dataclass: `name`, `paths` — one or more dotted config
|
||||||
|
> paths, `precedence`) and a `FLAG_SPECS` table covering every flag `train`/`new-run` map
|
||||||
|
> into the config tree, plus `overrides_from_flags(values: dict[str, object]) -> dict`,
|
||||||
|
> which applies specs in ascending precedence order (so a more-specific flag overwrites a
|
||||||
|
> shared/shorthand one written earlier at the same path) — one mechanism replacing the
|
||||||
|
> three different ad hoc "more specific wins" patterns identified below
|
||||||
|
> (`.update()`-call-order, `setdefault(...)[...] =` overwrite-order, and
|
||||||
|
> `{**shared, **specific}` merge). `train()`'s ~140-line override-building block
|
||||||
|
> (`cli.py:656-790` pre-fix) is now a single flat `flag_values` dict (mostly enum `.value`
|
||||||
|
> unwrapping) plus one call to `overrides_from_flags`; `new_run()`'s near-verbatim copy
|
||||||
|
> (`cli.py:915-983` pre-fix) collapsed the same way, reusing the identical table. Router
|
||||||
|
> overrides (`_router_cli_overrides`, unchanged, still shared by both commands) feed into
|
||||||
|
> the table as a single pre-aggregated `router_config` entry mapped only to
|
||||||
|
> `stage1_model.router` — the stage1-only asymmetry noted below is preserved exactly, with
|
||||||
|
> a regression test. No flag was added, removed, or renamed, and no precedence semantics
|
||||||
|
> changed: `giant train --help`/`giant new-run --help` are byte-identical before and after,
|
||||||
|
> verified by diffing both. Everything below this point describes the pre-fix state and is
|
||||||
|
> kept for historical context.
|
||||||
|
|
||||||
**Severity: High. Effort: Medium.**
|
**Severity: High. Effort: Medium.**
|
||||||
|
|
||||||
**Location:** `giant/cli.py:329-839`.
|
**Location:** `giant/cli.py:329-839`.
|
||||||
@@ -488,6 +508,31 @@ separate inline comment blocks to reconstruct.
|
|||||||
|
|
||||||
## Issue 4 — `cli.py` sits at 35.8 % coverage and holds untested override-precedence logic
|
## Issue 4 — `cli.py` sits at 35.8 % coverage and holds untested override-precedence logic
|
||||||
|
|
||||||
|
> **Status: Fixed for the override-precedence logic (Issue 3's scope); the
|
||||||
|
> `predict`/`rollout` inference-bootstrap portion described below is still open — that is
|
||||||
|
> Issue 5, deliberately not attempted here.** Commit `2bfb1ab` on
|
||||||
|
> `v0.3.0-stage2-autoregressive` adds direct, `CliRunner`-free unit tests for
|
||||||
|
> `overrides_from_flags` in `tests/test_config.py` — one per precedence rule, including
|
||||||
|
> several with previously **zero** coverage: both legs of `--stage{1,2}-generator`
|
||||||
|
> overriding `--mode` (only the stage1 leg had a test before), all three stage1
|
||||||
|
> shorthand-vs-`--stage1-*` pairs (previously only `hidden_dim`), all four WGAN knobs'
|
||||||
|
> shared-vs-per-stage precedence (previously only `n_critic`/`gp_weight`), the
|
||||||
|
> `--emb-dim`/`--conditioning` dual-axis fan-out (previously untested via `train` at all),
|
||||||
|
> and an explicit regression test that `router_config` only ever writes
|
||||||
|
> `stage1_model.router`. `tests/test_cli_train_overrides.py` keeps its original 4
|
||||||
|
> `CliRunner` smoke tests unmodified, plus 4 new ones covering the `--batch-size auto`
|
||||||
|
> parse-error and success paths and all three `out_dir` resolution branches
|
||||||
|
> (`--out`/`--resume`/default) — previously entirely uncovered. The percentage barely moves
|
||||||
|
> (36 % → 35 %), because the extraction *deleted* more statements from `cli.py` (539→478)
|
||||||
|
> than the new tests cover elsewhere in the file, but covered statements rose in absolute
|
||||||
|
> terms (157→169) and every override-precedence line the original issue called out by
|
||||||
|
> number is now covered. Coverage of `cli.py`'s other listed gap — the `predict`/
|
||||||
|
> `rollout` inference bootstrap (`cli.py:1104-1423`/`1528-1702` in the pre-fix numbering) —
|
||||||
|
> is unchanged, since fixing that requires the `load_for_inference` extraction described in
|
||||||
|
> Issue 5, which is explicitly out of scope for this fix (done separately, if at all).
|
||||||
|
> Everything below this point describes the pre-fix state and is kept for historical
|
||||||
|
> context.
|
||||||
|
|
||||||
**Severity: High. Effort: Medium.**
|
**Severity: High. Effort: Medium.**
|
||||||
|
|
||||||
**Location:** `giant/cli.py` (1865 lines).
|
**Location:** `giant/cli.py` (1865 lines).
|
||||||
|
|||||||
@@ -94,3 +94,83 @@ def test_wgan_knobs_split_per_stage(monkeypatch, tmp_path):
|
|||||||
assert cfg["stage1_model"]["wgan"]["gp_weight"] == 10.0
|
assert cfg["stage1_model"]["wgan"]["gp_weight"] == 10.0
|
||||||
assert cfg["stage2_model"]["wgan"]["n_critic"] == 5
|
assert cfg["stage2_model"]["wgan"]["n_critic"] == 5
|
||||||
assert cfg["stage2_model"]["wgan"]["gp_weight"] == 2.5
|
assert cfg["stage2_model"]["wgan"]["gp_weight"] == 2.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_size_invalid_string_errors(monkeypatch, tmp_path):
|
||||||
|
monkeypatch.setattr(cli, "run_train_job", lambda *a, **kw: None)
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.app,
|
||||||
|
["train", "dummy.parquet", "--out", str(tmp_path / "run"), "--batch-size", "not-a-number"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "--batch-size must be an integer or 'auto'" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_out_dir_resolution_prefers_explicit_out_over_resume(monkeypatch, tmp_path):
|
||||||
|
captured: dict = {}
|
||||||
|
|
||||||
|
def _fake_run_train_job(*, data, cfg, out_dir, **kwargs):
|
||||||
|
captured["out_dir"] = out_dir
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli, "run_train_job", _fake_run_train_job)
|
||||||
|
|
||||||
|
resume_dir = tmp_path / "resumed_run"
|
||||||
|
resume_dir.mkdir()
|
||||||
|
(resume_dir / "last.pt").touch()
|
||||||
|
explicit_out = tmp_path / "explicit_run"
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.app,
|
||||||
|
["train", "dummy.parquet", "--out", str(explicit_out), "--resume", str(resume_dir / "last.pt")],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert captured["out_dir"] == explicit_out
|
||||||
|
|
||||||
|
|
||||||
|
def test_out_dir_resolution_falls_back_to_resume_parent(monkeypatch, tmp_path):
|
||||||
|
captured: dict = {}
|
||||||
|
|
||||||
|
def _fake_run_train_job(*, data, cfg, out_dir, **kwargs):
|
||||||
|
captured["out_dir"] = out_dir
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli, "run_train_job", _fake_run_train_job)
|
||||||
|
|
||||||
|
resume_dir = tmp_path / "resumed_run"
|
||||||
|
resume_dir.mkdir()
|
||||||
|
(resume_dir / "last.pt").touch()
|
||||||
|
|
||||||
|
result = runner.invoke(cli.app, ["train", "dummy.parquet", "--resume", str(resume_dir / "last.pt")])
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert captured["out_dir"] == resume_dir
|
||||||
|
|
||||||
|
|
||||||
|
def test_out_dir_resolution_defaults_when_neither_out_nor_resume_given(monkeypatch, tmp_path):
|
||||||
|
captured: dict = {}
|
||||||
|
|
||||||
|
def _fake_run_train_job(*, data, cfg, out_dir, **kwargs):
|
||||||
|
captured["out_dir"] = out_dir
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli, "run_train_job", _fake_run_train_job)
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
|
||||||
|
result = runner.invoke(cli.app, ["train", "dummy.parquet"])
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert captured["out_dir"] == Path("checkpoints") / cli.gconfig.default_out_dir_name(cli.gconfig.DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_size_auto_estimates_and_echoes(monkeypatch, tmp_path):
|
||||||
|
captured: dict = {}
|
||||||
|
|
||||||
|
def _fake_run_train_job(*, data, cfg, out_dir, num_workers, **kwargs):
|
||||||
|
captured["batch_size"] = cfg["train"]["batch_size"]
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli, "run_train_job", _fake_run_train_job)
|
||||||
|
monkeypatch.setattr(cli.gconfig, "estimate_batch_size", lambda hidden_dim, n_blocks, device: 123)
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.app,
|
||||||
|
["train", "dummy.parquet", "--out", str(tmp_path / "run"), "--batch-size", "auto"],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert captured["batch_size"] == 123
|
||||||
|
assert "batch_size: 123 (auto-estimated from free GPU memory)" in result.output
|
||||||
|
|||||||
@@ -889,6 +889,125 @@ def test_merge_cli_overrides_real_config_fixtures_pass_key_validation(fixture_na
|
|||||||
gconfig.merge_cli_overrides(gconfig.DEFAULT_CONFIG, _CONFIGS_DIR / fixture_name, {}) # must not raise
|
gconfig.merge_cli_overrides(gconfig.DEFAULT_CONFIG, _CONFIGS_DIR / fixture_name, {}) # must not raise
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# overrides_from_flags (issues.md Issue 3): the flag -> config-path table
|
||||||
|
# shared by `giant train`/`giant new-run`. Each test below pins one
|
||||||
|
# precedence rule directly, without CliRunner — see also
|
||||||
|
# tests/test_cli_train_overrides.py for the thin end-to-end smoke coverage.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_from_flags_empty_values_yield_empty_overrides():
|
||||||
|
assert gconfig.overrides_from_flags({}) == {}
|
||||||
|
assert gconfig.overrides_from_flags({"epochs": None, "hidden_dim": None}) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_from_flags_train_block_passthrough():
|
||||||
|
overrides = gconfig.overrides_from_flags({"epochs": 5, "lr": 1e-3, "hidden_dim": None})
|
||||||
|
assert overrides == {"train": {"epochs": 5, "lr": 1e-3}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("shorthand", "explicit", "path_key"),
|
||||||
|
[
|
||||||
|
("hidden_dim", "stage1_hidden_dim", "hidden_dim"),
|
||||||
|
("n_blocks", "stage1_n_res_blocks", "n_res_blocks"),
|
||||||
|
("dropout", "stage1_dropout", "dropout"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_overrides_from_flags_stage1_explicit_overrides_shorthand(shorthand, explicit, path_key):
|
||||||
|
overrides = gconfig.overrides_from_flags({shorthand: 1, explicit: 2})
|
||||||
|
assert overrides["stage1_model"][path_key] == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("shorthand", "path_key"),
|
||||||
|
[("hidden_dim", "hidden_dim"), ("n_blocks", "n_res_blocks"), ("dropout", "dropout")],
|
||||||
|
)
|
||||||
|
def test_overrides_from_flags_stage1_shorthand_alone(shorthand, path_key):
|
||||||
|
overrides = gconfig.overrides_from_flags({shorthand: 7})
|
||||||
|
assert overrides["stage1_model"][path_key] == 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_from_flags_stage2_only_knobs():
|
||||||
|
overrides = gconfig.overrides_from_flags(
|
||||||
|
{
|
||||||
|
"stage2_hidden_dim": 32,
|
||||||
|
"stage2_n_res_blocks": 4,
|
||||||
|
"stage2_dropout": 0.1,
|
||||||
|
"stage2_decoder": "one_shot",
|
||||||
|
"stage2_k_max": 8,
|
||||||
|
"stage2_context_dim": 16,
|
||||||
|
"stage2_stage1_context": "sampled",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert overrides["stage2_model"] == {
|
||||||
|
"hidden_dim": 32,
|
||||||
|
"n_res_blocks": 4,
|
||||||
|
"dropout": 0.1,
|
||||||
|
"decoder": "one_shot",
|
||||||
|
"k_max": 8,
|
||||||
|
"context_dim": 16,
|
||||||
|
"stage1_context": "sampled",
|
||||||
|
}
|
||||||
|
assert "stage1_model" not in overrides
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_from_flags_mode_fans_to_both_stages():
|
||||||
|
overrides = gconfig.overrides_from_flags({"mode": "wgan"})
|
||||||
|
assert overrides["stage1_model"]["generator"] == "wgan"
|
||||||
|
assert overrides["stage2_model"]["generator"] == "wgan"
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_from_flags_stage1_generator_overrides_mode_for_stage1_only():
|
||||||
|
overrides = gconfig.overrides_from_flags({"mode": "wgan", "stage1_generator": "flow"})
|
||||||
|
assert overrides["stage1_model"]["generator"] == "flow"
|
||||||
|
assert overrides["stage2_model"]["generator"] == "wgan"
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_from_flags_stage2_generator_overrides_mode_for_stage2_only():
|
||||||
|
overrides = gconfig.overrides_from_flags({"mode": "wgan", "stage2_generator": "flow"})
|
||||||
|
assert overrides["stage1_model"]["generator"] == "wgan"
|
||||||
|
assert overrides["stage2_model"]["generator"] == "flow"
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_from_flags_emb_dim_sets_both_conditioning_axes():
|
||||||
|
overrides = gconfig.overrides_from_flags({"emb_dim": 24})
|
||||||
|
assert overrides["conditioning"]["particle"]["emb_dim"] == 24
|
||||||
|
assert overrides["conditioning"]["material"]["emb_dim"] == 24
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_from_flags_conditioning_sets_both_axes_type():
|
||||||
|
overrides = gconfig.overrides_from_flags({"conditioning": "onehot"})
|
||||||
|
assert overrides["conditioning"]["particle"]["type"] == "onehot"
|
||||||
|
assert overrides["conditioning"]["material"]["type"] == "onehot"
|
||||||
|
|
||||||
|
|
||||||
|
def test_overrides_from_flags_router_config_only_touches_stage1():
|
||||||
|
overrides = gconfig.overrides_from_flags({"router_config": {"enabled": True, "type": "energy"}})
|
||||||
|
assert overrides["stage1_model"]["router"] == {"enabled": True, "type": "energy"}
|
||||||
|
assert "stage2_model" not in overrides
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("shared", "stage1_specific", "stage2_specific", "path_key"),
|
||||||
|
[
|
||||||
|
("n_critic", "stage1_n_critic", "stage2_n_critic", "n_critic"),
|
||||||
|
("gp_weight", "stage1_gp_weight", "stage2_gp_weight", "gp_weight"),
|
||||||
|
("noise_dim", "stage1_noise_dim", "stage2_noise_dim", "noise_dim"),
|
||||||
|
("critic_lr", "stage1_critic_lr", "stage2_critic_lr", "critic_lr"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_overrides_from_flags_wgan_knobs_split_per_stage(shared, stage1_specific, stage2_specific, path_key):
|
||||||
|
overrides = gconfig.overrides_from_flags({shared: 5.0, stage1_specific: 3.0})
|
||||||
|
assert overrides["stage1_model"]["wgan"][path_key] == 3.0
|
||||||
|
assert overrides["stage2_model"]["wgan"][path_key] == 5.0
|
||||||
|
|
||||||
|
overrides = gconfig.overrides_from_flags({shared: 5.0, stage2_specific: 2.5})
|
||||||
|
assert overrides["stage1_model"]["wgan"][path_key] == 5.0
|
||||||
|
assert overrides["stage2_model"]["wgan"][path_key] == 2.5
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# checkpoint config-mismatch warnings (unchanged surface, still exercised)
|
# checkpoint config-mismatch warnings (unchanged surface, still exercised)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user