Store a quantile grid instead of a raw reservoir sample in the setup cache
CI / Format (ruff format) (push) Successful in 26s
CI / Lint (ruff check) (push) Successful in 28s
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 24s
CI / Tests (push) Successful in 58s
CI / Format (ruff format) (pull_request) Successful in 28s
CI / Lint (ruff check) (pull_request) Successful in 28s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Type check (ty) (pull_request) Successful in 28s
CI / Tests (pull_request) Successful in 59s

NormalizerEntry.energy_reservoir_sample kept 100k raw energy values purely
to seed EnergyRouter centers via np.quantile at load time, which alone
accounted for most of the setup cache sidecar's ~2MB size (float32 values
round-tripped through Python floats serialize at full double precision).
Only a handful of quantile levels are ever read back, so collapse the
sample to a fixed 1001-point quantile grid at save time and interpolate
arbitrary levels from it at use time instead — about 100x smaller with
negligible (<0.001) error on the levels that matter. Bumps the cache
format version since old sidecars have no such grid to fall back on.
This commit is contained in:
2026-07-30 13:32:46 +02:00
parent de5db25e3f
commit d656cf3109
4 changed files with 87 additions and 26 deletions
+34 -7
View File
@@ -31,7 +31,10 @@ from giant.data.transforms import Normalizer, sorted_membership
# v2: event_id is now offset per-file (see loader.event_id_offset) to avoid
# cross-file collisions, so a v1 sidecar's event_index/normalizers were
# computed against collided ids and must not be reused.
_CACHE_FORMAT_VERSION = 2
# v3: NormalizerEntry.energy_reservoir_sample (100k raw values) replaced by
# energy_quantiles (a fixed ENERGY_QUANTILE_LEVELS-point quantile grid) — a
# v2 sidecar has no such grid to fall back on, so it must be recomputed.
_CACHE_FORMAT_VERSION = 3
_DIMS = {
"COND_DIM": COND_DIM,
@@ -41,6 +44,29 @@ _DIMS = {
"SEC_SLOT_DIM": SEC_SLOT_DIM,
}
# Resolution of the stored energy-quantile summary (see NormalizerEntry).
# Only a handful of quantile *levels* (one per EnergyRouter expert) are ever
# consumed (see pipeline.py), so a dense fixed grid of quantile values is
# enough to reconstruct any level via interpolation (energy_quantile_at) —
# at roughly 1/100th the storage of the raw 100k-value reservoir sample it
# replaces, with negligible loss of resolution for that use.
ENERGY_QUANTILE_LEVELS = 1001
def energy_quantiles_from_sample(sample: np.ndarray) -> np.ndarray:
"""Collapse a raw reservoir sample into the fixed grid stored on disk."""
if sample.size == 0:
return np.empty(0, dtype=np.float32)
levels = np.linspace(0.0, 1.0, ENERGY_QUANTILE_LEVELS)
return np.quantile(sample, levels).astype(np.float32)
def energy_quantile_at(energy_quantiles: np.ndarray, levels: np.ndarray) -> np.ndarray:
"""Interpolate quantile values at arbitrary probability `levels` from the
stored grid (e.g. `np.linspace(0, 1, n_experts)` for router centers)."""
grid_levels = np.linspace(0.0, 1.0, len(energy_quantiles))
return np.interp(levels, grid_levels, energy_quantiles).astype(np.float32)
def sidecar_path(data: str | Path) -> Path:
"""The cache sidecar for `data`, always a sibling of `data` itself.
@@ -81,7 +107,10 @@ class NormalizerEntry:
tgt_norm: Normalizer
sec_phys_norm: Normalizer
n_train_steps: int
energy_reservoir_sample: np.ndarray
energy_quantiles: np.ndarray
"""Fixed ENERGY_QUANTILE_LEVELS-point quantile grid of the raw (pre-
normalization) pre-step energy column — see energy_quantiles_from_sample
/ energy_quantile_at."""
def to_json(self) -> dict:
return {
@@ -89,8 +118,8 @@ class NormalizerEntry:
"tgt_norm": self.tgt_norm.to_dict(),
"sec_phys_norm": self.sec_phys_norm.to_dict(),
"n_train_steps": self.n_train_steps,
"energy_reservoir_sample": np.asarray(
self.energy_reservoir_sample, dtype=np.float32
"energy_quantiles": np.asarray(
self.energy_quantiles, dtype=np.float32
).tolist(),
}
@@ -101,9 +130,7 @@ class NormalizerEntry:
tgt_norm=Normalizer.from_dict(d["tgt_norm"]),
sec_phys_norm=Normalizer.from_dict(d["sec_phys_norm"]),
n_train_steps=int(d["n_train_steps"]),
energy_reservoir_sample=np.array(
d["energy_reservoir_sample"], dtype=np.float32
),
energy_quantiles=np.array(d["energy_quantiles"], dtype=np.float32),
)