File size: 5,678 Bytes
7180154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

import numpy as np
import xarray
import yaml

PROJECT_ROOT = Path(__file__).resolve().parents[1]


def load_config(path: str | Path) -> dict[str, Any]:
    with Path(path).open(encoding="utf-8") as source:
        return yaml.safe_load(source)


def resolve_path(path: str | Path) -> Path:
    candidate = Path(path).expanduser()
    return candidate if candidate.is_absolute() else PROJECT_ROOT / candidate


def configure_jax(platform: str) -> None:
    if platform != "auto" and "JAX_PLATFORM_NAME" not in os.environ:
        os.environ["JAX_PLATFORM_NAME"] = platform


def load_stats(stats_dir: str | Path) -> dict[str, xarray.Dataset]:
    directory = resolve_path(stats_dir)
    names = (
        "diffs_stddev_by_level",
        "mean_by_level",
        "stddev_by_level",
        "min_by_level",
    )
    stats = {}
    for name in names:
        path = directory / f"{name}.nc"
        if not path.exists():
            raise FileNotFoundError(f"Missing GenCast statistic: {path}")
        stats[name] = xarray.load_dataset(path).compute()
    from model.graphcast import gencast, graphcast

    inputs = set(gencast.TASK.input_variables) - set(graphcast.GENERATED_FORCING_VARS)
    targets = set(gencast.TASK.target_variables)
    required_by_stat = {
        "mean_by_level": inputs | (targets - inputs),
        "stddev_by_level": inputs | (targets - inputs),
        "diffs_stddev_by_level": targets & inputs,
        "min_by_level": {"sea_surface_temperature"},
    }
    for stat_name, dataset in stats.items():
        missing = sorted(required_by_stat[stat_name] - set(dataset.data_vars))
        if missing:
            raise ValueError(f"{stat_name} is missing GenCast variables: {missing}")
        for name, values in dataset.data_vars.items():
            array = np.asarray(values)
            if not np.all(np.isfinite(array)):
                raise ValueError(f"{stat_name}.{name} contains non-finite values")
            if "level" in values.dims and tuple(values.level.values) != tuple(
                gencast.TASK.pressure_levels
            ):
                raise ValueError(f"{stat_name}.{name} does not use GenCast WB13 order")
            if stat_name in ("stddev_by_level", "diffs_stddev_by_level") and np.any(array <= 0):
                raise ValueError(f"{stat_name}.{name} must be strictly positive")
    return stats


def save_trainer_checkpoint(
    path: str | Path,
    *,
    params: Any,
    state: Any,
    optimizer_state: Any,
    step: int,
    config: dict[str, Any],
) -> None:
    import jax

    destination = resolve_path(path)
    destination.parent.mkdir(parents=True, exist_ok=True)
    leaves, treedef = jax.tree_util.tree_flatten(
        {"params": params, "state": state, "optimizer_state": optimizer_state}
    )
    arrays = {f"leaf_{i}": np.asarray(value) for i, value in enumerate(leaves)}
    arrays["treedef"] = np.array([treedef], dtype=object)
    arrays["step"] = np.asarray(step, dtype=np.int64)
    arrays["config_json"] = np.asarray(json.dumps(config, sort_keys=True))
    temporary = destination.with_suffix(destination.suffix + ".tmp")
    with temporary.open("wb") as output:
        np.savez(output, **arrays)
    os.replace(temporary, destination)


def load_trainer_checkpoint(
    path: str | Path,
) -> tuple[Any, Any, Any, int, dict[str, Any]]:
    import jax

    source_path = resolve_path(path)
    with np.load(source_path, allow_pickle=True) as source:
        treedef = source["treedef"].item()
        leaves = [source[f"leaf_{i}"] for i in range(len(source.files) - 3)]
        tree = jax.tree_util.tree_unflatten(treedef, leaves)
        saved_config = json.loads(str(source["config_json"]))
        return (
            tree["params"], tree["state"], tree["optimizer_state"],
            int(source["step"]), saved_config,
        )


def validate_checkpoint_config(
    current: dict[str, Any],
    saved: dict[str, Any],
    *,
    scope: str = "resume",
) -> None:
    """Validate checkpoint compatibility for training resume or inference."""
    if scope not in ("resume", "inference"):
        raise ValueError("scope must be 'resume' or 'inference'")

    inference_paths = (
        ("model",), ("sampler",), ("data", "stats_dir"),
        ("data", "static_dir"), ("data", "precipitation_interval_hours"),
    )
    resume_only_paths = (
        ("training", "learning_rate"),
        ("training", "betas"), ("training", "epsilon"),
        ("training", "seed"), ("data", "data_dir"),
        ("data", "train_years"), ("data", "train_stride"),
        ("parallel", "mode"), ("parallel", "num_devices"),
        ("parallel", "global_batch_size"), ("parallel", "axis_name"),
    )
    if scope == "resume":
        if "parallel" not in saved:
            saved = dict(saved)
            saved["parallel"] = {
                "mode": "single",
                "num_devices": 1,
                "global_batch_size": 1,
                "axis_name": "devices",
            }
        paths = inference_paths + resume_only_paths
    else:
        paths = inference_paths

    for path in paths:
        current_value: Any = current
        saved_value: Any = saved
        for key in path:
            current_value = current_value[key]
            saved_value = saved_value[key]
        if current_value != saved_value:
            name = ".".join(path)
            raise ValueError(
                f"Trainer checkpoint configuration mismatch for {name} "
                f"during {scope}: "
                f"saved={saved_value!r}, current={current_value!r}"
            )