File size: 8,640 Bytes
f4a39ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | """Official NeuralGCM implementation facade.
The upstream legacy model, encoders, decoders, dynamical core and reference
training utilities are vendored directly under this project's ``model``
namespace (``model/legacy`` and ``model/reference_code``). This file is the
single project-facing entry point; no external ``neuralgcm`` source directory
is required at runtime.
"""
from __future__ import annotations
import pickle
from pathlib import Path
from typing import Any
import numpy as np
PROFILE_GIN = {
"weather_forecast": "deterministic_0_7_deg.gin",
"climate_scale": "deterministic_1_4_deg.gin",
"forecast_2_8_deg": "deterministic_2_8_deg.gin",
"stochastic_1_4_deg": "stochastic_1_4_deg.gin",
}
MODE_ALIASES = {
"forecast": "weather_forecast",
"weather_forecast": "weather_forecast",
"climate": "climate_scale",
"climate_scale": "climate_scale",
"forecast_2_8_deg": "forecast_2_8_deg",
"stochastic_1_4_deg": "stochastic_1_4_deg",
}
class OfficialNeuralGCMUnavailable(RuntimeError):
"""Raised when the official runtime package is not available."""
class CheckpointFormatError(ValueError):
"""Raised when a file is not an official NeuralGCM checkpoint."""
def checkpoint_mode(payload: object) -> str | None:
"""Infer the project profile declared by an official-format checkpoint."""
if not isinstance(payload, dict):
return None
if payload.get("mode"):
value = str(payload["mode"])
return MODE_ALIASES.get(value, value)
text = str(payload.get("model_config_str", ""))
if "GridTL255" in text:
return "weather_forecast"
if "GridTL63" in text:
return "forecast_2_8_deg"
if "GridTL127" in text:
return "stochastic_1_4_deg" if "FIELD_SUBSET" in text else "climate_scale"
return None
def validate_checkpoint_mode(payload: object, mode: str, path: str | Path) -> None:
"""Reject a checkpoint whose grid/profile differs from the requested mode."""
stored_mode = checkpoint_mode(payload)
if stored_mode and stored_mode != mode:
raise ValueError(
f"Checkpoint {path} is for mode={stored_mode!r}, but mode={mode!r} "
"was requested. Select the matching mode or checkpoint."
)
def parameter_summary(params: Any) -> dict[str, Any]:
"""Return reproducible parameter count, storage size and dtype statistics."""
import jax
leaves = jax.tree_util.tree_leaves(params)
array_leaves = [leaf for leaf in leaves if hasattr(leaf, "shape") and hasattr(leaf, "dtype")]
count = sum(int(np.prod(leaf.shape, dtype=np.int64)) for leaf in array_leaves)
nbytes = sum(
int(np.prod(leaf.shape, dtype=np.int64)) * np.dtype(leaf.dtype).itemsize
for leaf in array_leaves
)
dtype_counts: dict[str, int] = {}
for leaf in array_leaves:
dtype = str(np.dtype(leaf.dtype))
dtype_counts[dtype] = dtype_counts.get(dtype, 0) + int(
np.prod(leaf.shape, dtype=np.int64)
)
return {
"count": count,
"nbytes": nbytes,
"leaves": len(array_leaves),
"dtypes": dtype_counts,
}
def format_parameter_summary(params: Any) -> str:
"""Format a compact ``params.count``-style model summary."""
summary = parameter_summary(params)
dtype_text = ",".join(
f"{dtype}:{count:,}" for dtype, count in sorted(summary["dtypes"].items())
)
return (
f"params.count={summary['count']:,} "
f"params.bytes={summary['nbytes']:,} "
f"params.mib={summary['nbytes'] / 2**20:.2f} "
f"params.leaves={summary['leaves']} dtypes={dtype_text}"
)
def load_checkpoint(path: str | Path):
"""Load an official checkpoint through the vendored PressureLevelModel."""
try:
from model.legacy.api import PressureLevelModel
except Exception as exc: # pragma: no cover - runtime-dependent
raise OfficialNeuralGCMUnavailable(
"Unable to import the vendored NeuralGCM implementation. Check "
"JAX, Haiku, Gin and Dinosaur dependencies in develop_base."
) from exc
path = Path(path)
if not path.exists():
raise FileNotFoundError(path)
with path.open("rb") as handle:
checkpoint = pickle.load(handle)
required = {"model_config_str", "aux_ds_dict", "params"}
if not isinstance(checkpoint, dict) or not required.issubset(checkpoint):
keys = sorted(checkpoint) if isinstance(checkpoint, dict) else type(checkpoint).__name__
raise CheckpointFormatError(
f"{path} is not an official checkpoint; expected keys "
f"{sorted(required)}, got {keys}"
)
return PressureLevelModel.from_checkpoint(checkpoint)
def official_runtime_available() -> bool:
try:
from model.legacy.api import PressureLevelModel # noqa: F401
except Exception:
return False
return True
def build_from_scratch(dataset, mode: str):
"""Build the public WhirlModel used for random parameter initialization.
Parameter initialization itself needs a concrete trajectory and is performed
by ``scripts/train.py`` through the returned model's Haiku rollout function.
This compatibility facade deliberately does not import the unreleased Google
experiment runner.
"""
return build_training_model(dataset, mode)
def build_training_model(dataset, mode: str):
"""Build an official ``WhirlModel`` from the fused Gin profile."""
if mode not in PROFILE_GIN:
raise ValueError(f"Unknown NeuralGCM mode {mode!r}")
import gin
from model.legacy import model_builder
config_path = Path(__file__).resolve().parent / "reference_code" / "paper_configs" / PROFILE_GIN[mode]
gin_text = config_path.read_text(encoding="utf-8")
# The released Gin profiles use ``orography_data_path = None`` and rely on
# the official xarray auxiliary-dataset escape hatch for static fields.
# ``get_whirl_model`` normally obtains this from dataset metadata; supply it
# explicitly for synthetic/OneScience datasets that have no metadata attrs.
from dinosaur import xarray_utils
try:
aux_features = xarray_utils.aux_features_from_xarray(dataset)
except (KeyError, AttributeError):
aux_features = {}
aux_features[xarray_utils.XARRAY_DS_KEY] = dataset
dataset = dataset.copy()
dataset.attrs = dict(dataset.attrs)
dataset.attrs[xarray_utils.XR_AUX_FEATURES_LIST_KEY] = ",".join(
key for key in aux_features if key != xarray_utils.XARRAY_DS_KEY
)
# get_whirl_model reads serializable aux variables from attrs. Injecting the
# xarray dataset directly is handled below through a temporary wrapper.
original = model_builder.xarray_utils.aux_features_from_xarray
model_builder.xarray_utils.aux_features_from_xarray = lambda _: aux_features
try:
model = model_builder.get_whirl_model(dataset, gin_text)
finally:
model_builder.xarray_utils.aux_features_from_xarray = original
# The profile's xarray conversion callbacks are configured through Gin;
# get_whirl_model returns the fully bound model object.
return model, gin_text
def make_rollout_functions(
whirl_model, trajectory_length: int, *, inner_steps: int = 1
):
"""Return Haiku init/apply functions using the official rollout helpers."""
import haiku as hk
from model.legacy import model_utils
@hk.transform
def rollout_fn(target, forcing):
model = whirl_model.model_cls()
trajectory_fn = model_utils.trajectory_with_inputs_and_forcing(
model, num_init_frames=1, start_with_input=True
)
_, predicted = trajectory_fn(
target,
forcing,
outer_steps=trajectory_length,
inner_steps=inner_steps,
)
return model_utils.compute_prediction_and_target_representations(
predicted, target, forcing, model
)
return rollout_fn
def save_official_checkpoint(path: str | Path, params: Any, dataset, model_config_str: str, *, metadata: dict[str, Any] | None = None):
"""Write a checkpoint consumable by ``PressureLevelModel.from_checkpoint``."""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"model_config_str": model_config_str,
"aux_ds_dict": dataset.to_dict(),
"params": params,
}
if metadata:
payload.update(metadata)
with path.open("wb") as handle:
pickle.dump(payload, handle, protocol=pickle.HIGHEST_PROTOCOL)
return path
NeuralGCMAdapter = load_checkpoint
|