Merge pull request 'Let dwarf warm-cache take --config so it can't under-warm a config's cache keys (gitea #59)' (#60) from fix/issue-59 into master
CI / Lint (ruff check) (push) Successful in 27s
CI / Format (ruff format) (push) Successful in 27s
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 23s
CI / Tests (push) Successful in 1m59s

Reviewed-on: #60
This commit was merged in pull request #60.
This commit is contained in:
2026-08-17 09:24:42 +02:00
3 changed files with 197 additions and 66 deletions
+66 -23
View File
@@ -442,43 +442,65 @@ def warm_cache(
Path,
typer.Argument(help="Parquet file, directory, or .manifest — same as `giant train`'s"),
],
config: Annotated[
Optional[Path],
typer.Option(
"--config",
"-c",
help="TOML config file to warm for — same file the `giant train` run(s) will use. "
"Mutually exclusive with the flags below (put val-fraction/seed/conditioning/router "
"settings in the file itself, so warming and training can't disagree on them)",
),
] = None,
val_fraction: Annotated[
float,
Optional[float],
typer.Option(
"--val-fraction",
"-f",
help="Must match the `giant train` run(s) to warm for",
help="Must match the `giant train` run(s) to warm for. Not allowed together with --config",
),
] = 0.1,
] = None,
seed: Annotated[
int,
typer.Option("--seed", "-s", help="Must match the `giant train` run(s) to warm for"),
] = 0,
Optional[int],
typer.Option(
"--seed",
"-s",
help="Must match the `giant train` run(s) to warm for. Not allowed together with --config",
),
] = None,
particle_conditioning: Annotated[
Conditioning,
Optional[Conditioning],
typer.Option(
"--particle-conditioning",
help="Must match the `giant train` run(s)' conditioning.particle.type to warm for",
help="Must match the `giant train` run(s)' conditioning.particle.type to warm for. "
"Not allowed together with --config",
),
] = Conditioning.physical,
] = None,
material_conditioning: Annotated[
Conditioning,
Optional[Conditioning],
typer.Option(
"--material-conditioning",
help="Must match the `giant train` run(s)' conditioning.material.type "
"to warm for — independent of --particle-conditioning "
"(the two axes may differ)",
"(the two axes may differ). Not allowed together with --config",
),
] = Conditioning.physical,
] = None,
router: Annotated[
bool,
Optional[bool],
typer.Option(
"--router/--no-router",
help="Warm the process vocabulary too (only takes effect with --router-type process)",
help="Warm the process vocabulary too (only takes effect with --router-type process). "
"Not allowed together with --config",
),
] = False,
router_type: Annotated[str, typer.Option("--router-type", help="Router implementation name")] = "energy",
n_experts: Annotated[int, typer.Option("--n-experts", help="Number of routed experts")] = 4,
] = None,
router_type: Annotated[
Optional[str],
typer.Option("--router-type", help="Router implementation name. Not allowed together with --config"),
] = None,
n_experts: Annotated[
Optional[int],
typer.Option("--n-experts", help="Number of routed experts. Not allowed together with --config"),
] = None,
rebuild: Annotated[
bool,
typer.Option("--rebuild", help="Ignore any existing sidecar and recompute every section"),
@@ -487,17 +509,38 @@ def warm_cache(
"""Precompute `giant train`'s setup-stage sidecar for `data` ahead of time.
Warms the vocab maps, event-id split index, and the normalizer entry for
the given --val-fraction/--seed/--particle-conditioning/
--material-conditioning, so a later `giant train` run (or a `dwarf
hparam-scan` sweep, which shares one such entry across every run) skips
straight to training. See giant/data/setup_cache.py.
either --config, or the given --val-fraction/--seed/
--particle-conditioning/--material-conditioning/--router* flags, so a
later `giant train` run (or a `dwarf hparam-scan` sweep, which shares one
such entry across every run) skips straight to training. See
giant/data/setup_cache.py.
"""
flag_overrides = {
"--val-fraction": val_fraction,
"--seed": seed,
"--particle-conditioning": particle_conditioning,
"--material-conditioning": material_conditioning,
"--router/--no-router": router,
"--router-type": router_type,
"--n-experts": n_experts,
}
if config is not None:
given = [name for name, value in flag_overrides.items() if value is not None]
if given:
typer.echo(
f"error: --config cannot be combined with {', '.join(given)} "
"— put these settings in the config file instead",
err=True,
)
raise typer.Exit(1)
run_warm_setup_cache(
data=str(data),
config_path=config,
val_fraction=val_fraction,
seed=seed,
particle_conditioning=particle_conditioning.value,
material_conditioning=material_conditioning.value,
particle_conditioning=particle_conditioning.value if particle_conditioning is not None else None,
material_conditioning=material_conditioning.value if material_conditioning is not None else None,
router_enabled=router,
router_type=router_type,
n_experts=n_experts,
+66 -43
View File
@@ -10,63 +10,86 @@ for the sidecar itself.
from pathlib import Path
from giant import config as gconfig
from giant.constants import K_MAX
from giant.pipeline import run_setup_stage
def run_warm_setup_cache(
data: str,
val_fraction: float = 0.1,
seed: int = 0,
particle_conditioning: str = "physical",
material_conditioning: str = "physical",
router_enabled: bool = False,
router_type: str = "energy",
n_experts: int = 4,
config_path: Path | None = None,
val_fraction: float | None = None,
seed: int | None = None,
particle_conditioning: str | None = None,
material_conditioning: str | None = None,
router_enabled: bool | None = None,
router_type: str | None = None,
n_experts: int | None = None,
rebuild: bool = False,
echo=print,
) -> None:
"""Populate (or refresh) the setup cache sidecar for `data`.
`val_fraction`/`seed`/`particle_conditioning`/`material_conditioning`
select the normalizer cache entry
(`giant.data.setup_cache.normalizer_key`) — pass the same values a later
`giant train` invocation will use so it hits this warmed entry. The two
conditioning axes are independent and may differ.
`router_enabled`/`router_type`/`n_experts` only matter for
`router_type == "process"` (warms that `n_experts`'s process map); the
energy-router quantile summary is always collected regardless, so a
later `--router-type energy` run never needs to rescan just to seed
centers.
Two mutually exclusive ways to select what to warm for (enforced by the
caller, `giant.tools.dwarf.warm_cache` — this function just trusts
whichever combination it's given):
- `config_path`: the same TOML `giant train --config` takes. Every value
`run_setup_stage` needs (`train.val_fraction`/`seed`,
`conditioning.particle`/`material.type`, both stages' `router`,
`stage2_model.particle_type.n_classes`, ...) is read from the one
resulting merged `cfg`, so a later `giant train --config <same file>`
run resolves to exactly the same cache keys — see gitea #59.
- The individual flags below: `val_fraction`/`seed`/
`particle_conditioning`/`material_conditioning` select the normalizer
cache entry (`giant.data.setup_cache.normalizer_key`) — pass the same
values a later `giant train` invocation will use so it hits this
warmed entry. The two conditioning axes are independent and may
differ. `router_enabled`/`router_type`/`n_experts` only matter for
`router_type == "process"` (warms that `n_experts`'s process map); the
energy-router quantile summary is always collected regardless, so a
later `--router-type energy` run never needs to rescan just to seed
centers.
Any flag left `None` is omitted from the merge, so it falls back to
`DEFAULT_CONFIG`'s own value (or the config file's, if `config_path` is
given) instead of silently overriding it — see gitea #59.
"""
router_cfg = {
"enabled": router_enabled,
"type": router_type,
"n_experts": n_experts,
}
# Merged against DEFAULT_CONFIG (not a hand-rolled partial dict) so
# run_setup_stage always sees every key it might read (e.g.
# conditioning.particle.emb_dim, stage2_model.particle_type.target) at
# its real default, not silently missing/None — see issues.md Issue 1.
overrides: dict = {}
conditioning_overrides: dict = {}
if particle_conditioning is not None:
conditioning_overrides["particle"] = {"type": particle_conditioning}
if material_conditioning is not None:
conditioning_overrides["material"] = {"type": material_conditioning}
if conditioning_overrides:
overrides["conditioning"] = conditioning_overrides
# This CLI only ever configures one router (matching today's single
# --router-type flag), so it's placed on stage1_model; stage2_model's
# stays disabled.
cfg = gconfig.merge_cli_overrides(
gconfig.DEFAULT_CONFIG,
None,
{
"conditioning": {
"particle": {"type": particle_conditioning},
"material": {"type": material_conditioning},
},
"stage1_model": {"router": router_cfg},
"stage2_model": {"router": {"enabled": False}, "k_max": K_MAX},
},
)
# --router-type flag), so it's placed on stage1_model; stage2_model's is
# left to DEFAULT_CONFIG/the config file rather than forced disabled.
router_overrides: dict = {}
if router_enabled is not None:
router_overrides["enabled"] = router_enabled
if router_type is not None:
router_overrides["type"] = router_type
if n_experts is not None:
router_overrides["n_experts"] = n_experts
if router_overrides:
overrides["stage1_model"] = {"router": router_overrides}
train_overrides: dict = {}
if val_fraction is not None:
train_overrides["val_fraction"] = val_fraction
if seed is not None:
train_overrides["seed"] = seed
if train_overrides:
overrides["train"] = train_overrides
cfg = gconfig.merge_cli_overrides(gconfig.DEFAULT_CONFIG, config_path, overrides)
gconfig.validate_config(cfg)
run_setup_stage(
Path(data),
val_fraction=val_fraction,
seed=seed,
val_fraction=cfg["train"]["val_fraction"],
seed=cfg["train"]["seed"],
cfg=cfg,
cache_setup=True,
rebuild_setup_cache=rebuild,
+65
View File
@@ -124,6 +124,11 @@ def test_warm_cache_router_process_warms_proc_map(tmp_path):
[
"warm-cache",
str(data),
# router.type="process" is incompatible with the default
# conditioning.particle.type="physical" (validate_config, now
# enforced by warm-cache too — see gitea #59).
"--particle-conditioning",
"embedding",
"--router",
"--router-type",
"process",
@@ -167,3 +172,63 @@ def test_warm_cache_different_val_fraction_is_separate_entry(tmp_path):
assert loaded is not None
assert "valfrac=0.1_seed=0_pcond=physical_mcond=physical" in loaded.normalizers
assert "valfrac=0.3_seed=0_pcond=physical_mcond=physical" in loaded.normalizers
def test_warm_cache_config_warms_particle_type_n_classes(tmp_path):
"""gitea #59: a config setting stage2_model.particle_type.n_classes away
from its 0 (= inherit conditioning.particle.emb_dim) default must warm
the pdg top-N map under that n_classes, not the emb_dim default, so a
later `giant train --config <same file>` run hits it instead of quietly
re-scanning every parquet file."""
data = _make_synthetic_steps(tmp_path / "data.parquet", n_events=20)
config_path = tmp_path / "config.toml"
config_path.write_text("[meta]\nconfig_version = 3\n\n[stage2_model.particle_type]\nn_classes = 32\n")
runner.invoke(app, ["warm-cache", str(data), "--config", str(config_path)])
result = runner.invoke(app, ["warm-cache", str(data), "--config", str(config_path)])
assert result.exit_code == 0, result.output
assert "pdg top-N map: cache hit" in result.output
assert "32 classes" in result.output
def test_warm_cache_config_rejects_val_fraction_flag(tmp_path):
data = _make_synthetic_steps(tmp_path / "data.parquet", n_events=20)
config_path = tmp_path / "config.toml"
config_path.write_text("[meta]\nconfig_version = 3\n")
result = runner.invoke(
app,
["warm-cache", str(data), "--config", str(config_path), "--val-fraction", "0.2"],
)
assert result.exit_code != 0
assert "--config" in result.output
assert "--val-fraction" in result.output
def test_warm_cache_config_rejects_router_flags(tmp_path):
data = _make_synthetic_steps(tmp_path / "data.parquet", n_events=20)
config_path = tmp_path / "config.toml"
config_path.write_text("[meta]\nconfig_version = 3\n")
result = runner.invoke(
app,
[
"warm-cache",
str(data),
"--config",
str(config_path),
"--router",
"--router-type",
"process",
"--n-experts",
"3",
],
)
assert result.exit_code != 0
assert "--config" in result.output
assert "--router/--no-router" in result.output
assert "--router-type" in result.output
assert "--n-experts" in result.output