4692cee699
CI / Lint (ruff check) (push) Successful in 30s
CI / Format (ruff format) (push) Successful in 30s
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 43s
CI / Lint (ruff check) (pull_request) Successful in 40s
CI / Format (ruff format) (pull_request) Successful in 43s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Type check (ty) (pull_request) Successful in 45s
CI / Tests (pull_request) Successful in 3m24s
CI / Tests (push) Successful in 3m33s
The conditioning arrays' column order was written down three times — twice
in giant/data/transforms.py (build_cond_features and build_features each
built cond_cont and cond_cat from scratch) and again in
giant/model/encoders.py (cat_col_layout, plus hand-written
COND_DIM_BASE + PARTICLE_PHYS_DIM slicing in ConditionEncoder). The three
were held in sync only by parallel comments, so a wrong column order
produced silently mis-indexed features rather than an exception.
The drift had already happened, twice, both times in build_features:
- 5b63dfd added per-axis vocab-lookup strictness (an out-of-vocab
pdg/material must not KeyError under "physical"/"onehot", where the
index is never read) to build_cond_features only.
- _cond_normalizer_transform's legacy-normalizer padding, which keeps a
pre-physical-conditioning 8-wide cond normalizer loadable, was likewise
only wired into build_cond_features — so `giant predict` on such a
checkpoint died with a broadcast error.
New giant/cond_layout.py holds a frozen CondLayout built from the
(particle, material) mode pair, exposing named cond_cont slices
(base/particle_phys/material_phys) and cond_cat columns
(PDG_COL/MAT_COL/particle_topn_col/material_topn_col/cat_dim). Both
builders now share one _build_cond_arrays, ConditionEncoder reads its
slices off the same object, and PdgRouter/ProcessRouter use the named
dense-vocab columns instead of literal 0/1. CondLayout also absorbs the
two duplicated axis-type validations, keeping their message text verbatim.
Decisions taken while planning:
- Scope is CondLayout only. The issue's second half — a
CONDITIONING_AXIS_REGISTRY registering (feature_columns, encoder_module)
as a pair — is deferred: it would force ConditioningConfig's fixed
particle/material fields into a dynamic axis map and ripple through
pipeline.py, checkpoint_io.py and rollout.py, i.e. a config-schema break
with no consumer yet.
- The two divergences above are unified onto build_cond_features'
behaviour rather than preserved as parameters, so the new single source
of truth doesn't carry the old split forward. Each gets a regression
test that fails before this commit.
- cat_col_layout is replaced outright (deleted, dropped from network.py's
__all__, its four tests rewritten against CondLayout) rather than kept
as a wrapper — two spellings of the same fact is the defect itself.
cond_cat's width is now the layout's call rather than "did the caller pass
a map", so an "onehot" axis without its top-N map raises instead of
yielding a narrower array that ConditionEncoder would index out of bounds.
pipeline.py's normalizer-fitting pass reads only cond_cont but had to be
handed the maps to satisfy that.
No parameter, buffer or state_dict change; existing checkpoints load
unchanged, and the protected migration surfaces are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
4.3 KiB
Python
104 lines
4.3 KiB
Python
"""Single source of truth for the conditioning arrays' column layout (gitea #37).
|
|
|
|
`cond_cont` and `cond_cat` are built in `giant.data.transforms` and consumed in
|
|
`giant.model.encoders` / `giant.model.routers`. Their column order used to be
|
|
written down independently on each side, kept in sync only by parallel comments
|
|
— so getting it wrong produced silently mis-indexed columns rather than an
|
|
exception, and adding a conditioning axis meant a coordinated multi-file edit.
|
|
|
|
`CondLayout` owns that order. Both sides construct one from the same
|
|
`conditioning.particle.type` / `conditioning.material.type` pair and read named
|
|
slices off it, so the layout is stated exactly once. This module depends only on
|
|
`giant.constants`, so both the data and model packages can import it.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import ClassVar
|
|
|
|
from giant.constants import COND_DIM, COND_DIM_BASE, MATERIAL_PHYS_DIM, PARTICLE_PHYS_DIM
|
|
|
|
# The three per-axis conditioning modes. Mirrors giant.config.Conditioning,
|
|
# which this module deliberately does not import (giant.config pulls in the
|
|
# whole model package).
|
|
AXIS_TYPES = ("physical", "embedding", "onehot")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CondLayout:
|
|
"""Column layout of `cond_cont`/`cond_cat` for one (particle, material) mode pair.
|
|
|
|
`cond_cont` is unconditionally `COND_DIM` wide regardless of mode: the base
|
|
block, then the particle physical block, then the material physical block.
|
|
An axis that isn't `"physical"` gets its block zero-filled and never reads
|
|
it (see `giant.data.transforms._physical_cond_columns`), so the widths are
|
|
mode-independent and only the *meaning* of a block changes.
|
|
|
|
`cond_cat` is 2 to 4 wide. Columns `PDG_COL`/`MAT_COL` are always the dense
|
|
training-vocab index; an axis in `"onehot"` mode appends one more column
|
|
holding its top-N-plus-other class index, particle before material.
|
|
"""
|
|
|
|
particle_type: str
|
|
material_type: str
|
|
|
|
# cond_cat's dense-vocab columns, present in every mode. Under
|
|
# "physical"/"onehot" they are a reporting/router convenience the
|
|
# ConditionEncoder never reads; under "embedding" they are the signal.
|
|
PDG_COL: ClassVar[int] = 0
|
|
MAT_COL: ClassVar[int] = 1
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.particle_type not in AXIS_TYPES:
|
|
raise ValueError(f"unknown conditioning.particle.type {self.particle_type!r}")
|
|
if self.material_type not in AXIS_TYPES:
|
|
raise ValueError(f"unknown conditioning.material.type {self.material_type!r}")
|
|
|
|
@classmethod
|
|
def from_types(cls, particle_type: str, material_type: str) -> "CondLayout":
|
|
"""Named constructor — the entry point both sides use."""
|
|
return cls(particle_type=particle_type, material_type=material_type)
|
|
|
|
# --- cond_cont ---------------------------------------------------------
|
|
|
|
@property
|
|
def base(self) -> slice:
|
|
"""pre_pos(3), log(pre_E)(1), pre_dir(3), layer_id(1)."""
|
|
return slice(0, COND_DIM_BASE)
|
|
|
|
@property
|
|
def particle_phys(self) -> slice:
|
|
"""log(mass), charge — see `giant.particles`."""
|
|
return slice(COND_DIM_BASE, COND_DIM_BASE + PARTICLE_PHYS_DIM)
|
|
|
|
@property
|
|
def material_phys(self) -> slice:
|
|
"""Z_eff, A_eff, log(density), log(X0), log(lambda_int) — see `giant.materials`."""
|
|
start = COND_DIM_BASE + PARTICLE_PHYS_DIM
|
|
return slice(start, start + MATERIAL_PHYS_DIM)
|
|
|
|
@property
|
|
def cont_dim(self) -> int:
|
|
return COND_DIM
|
|
|
|
# --- cond_cat ----------------------------------------------------------
|
|
|
|
@property
|
|
def particle_topn_col(self) -> int | None:
|
|
"""Column of the particle top-N class index, or `None` if not `"onehot"`."""
|
|
return self.MAT_COL + 1 if self.particle_type == "onehot" else None
|
|
|
|
@property
|
|
def material_topn_col(self) -> int | None:
|
|
"""Column of the material top-N class index, or `None` if not `"onehot"`.
|
|
|
|
Comes after the particle top-N column when both axes are `"onehot"`.
|
|
"""
|
|
if self.material_type != "onehot":
|
|
return None
|
|
return self.MAT_COL + (2 if self.particle_type == "onehot" else 1)
|
|
|
|
@property
|
|
def cat_dim(self) -> int:
|
|
"""Total `cond_cat` width: 2, plus one column per `"onehot"` axis."""
|
|
return self.MAT_COL + 1 + (self.particle_type == "onehot") + (self.material_type == "onehot")
|