| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| r"""Pseudocode for training NeuralGCM models.""" |
| from collections.abc import Iterable, Iterator, Mapping, Sequence |
| import dataclasses |
| import functools |
| import logging |
| import math |
| from typing import Any, Callable, NamedTuple, Optional, cast |
|
|
| from absl import app |
| import datasets |
| from dinosaur import typing |
| from dinosaur import xarray_utils |
| import gin |
| from google_proprietary_code import checkpoint |
| from google_proprietary_code import experiment |
| from google_proprietary_code import experiment_utils |
| |
| from google_proprietary_code import profiling_util |
| from google_proprietary_code import streaming |
| from google_proprietary_code import timing_util |
| import haiku as hk |
| import jax |
| import jax.numpy as jnp |
| import jax.sharding |
| import model.reference_code.metrics as metrics |
| import model.reference_code.metrics_base as metrics_base |
| import model.reference_code.metrics_util as metrics_util |
| from ml_collections import config_dict |
| from model.legacy import model_builder |
| from model.legacy import model_utils |
| from model.legacy import optimization |
| from model.legacy import physics_specifications |
| import numpy as np |
| import optax |
| import pandas as pd |
| import model.reference_code.reader as reader |
| import model.reference_code.stochastic_losses as stochastic_losses |
| import tensorflow as tf |
| import model.reference_code.train_utils as train_utils |
| import xarray |
|
|
| Params = typing.Params |
| PyTree = Any |
| TrajectoryRepresentations = typing.TrajectoryRepresentations |
| TrajectoryFn = Callable[ |
| [Params, jax.Array, PyTree, PyTree], |
| tuple[TrajectoryRepresentations, TrajectoryRepresentations], |
| ] |
|
|
| tree_map = jax.tree_util.tree_map |
|
|
| |
| |
|
|
|
|
| @gin.configurable(allowlist=['constructor']) |
| def get_loss_obj( |
| trajectory_spec: metrics_util.TrajectorySpec, |
| constructor: Callable[..., metrics_base.Loss] = gin.REQUIRED, |
| ) -> metrics_base.Loss: |
| """Returns configured loss_fn on first `trajectory_length` time slices.""" |
| return constructor(trajectory_spec) |
|
|
|
|
| EvaluatorDict = dict[str, metrics_base.Evaluator] |
| TrainEvalIteratorTuple = tuple[ |
| Iterator[Any], |
| train_utils.TrainStepFunction, |
| Callable[..., Any], |
| dict[str, Any], |
| ] |
|
|
|
|
| @gin.configurable(allowlist=['constructor']) |
| def get_metrics_dict( |
| trajectory_spec: metrics_util.TrajectorySpec, |
| eval_time_steps: Sequence[int], |
| loss: metrics_base.Loss, |
| constructor: Callable[..., EvaluatorDict] = metrics.default_metrics, |
| is_ensemble_data: bool = False, |
| ) -> EvaluatorDict: |
| """Returns configured loss_fn on first `trajectory_length` time slices.""" |
| return constructor( |
| trajectory_spec, eval_time_steps, loss, is_ensemble_data=is_ensemble_data |
| ) |
|
|
|
|
| |
| |
| |
| |
|
|
|
|
| @gin.configurable |
| def get_loss_fn(loss_fn): |
| raise NotImplementedError |
|
|
|
|
| @gin.register |
| def weighted_l2_cumulative_loss(weights, scale): |
| raise NotImplementedError |
|
|
|
|
| |
|
|
|
|
| def ema_params_tree(num_steps): |
| """Creates an EMAParamsTree object based on num_steps. |
| |
| Args: |
| num_steps: average number of optimization steps to include in the |
| exponential moving average of model weights. |
| |
| Returns: |
| Haiku module. |
| """ |
| |
| decay = 1 - 2 / (num_steps + 1) |
| return hk.EMAParamsTree(decay) |
|
|
|
|
| def _model_inner_steps_for_data( |
| ds: xarray.Dataset, |
| model_specs: model_builder.ModelSpecs, |
| rtol: float = 1e-6, |
| ) -> int: |
| """Calculates model inner steps based on data time step.""" |
| data_dt = xarray_utils.nondim_time_delta_from_time_axis( |
| ds.time.data, model_specs.physics_specs |
| ) |
| inner_steps = round(data_dt / model_specs.dt) |
| if abs(inner_steps * model_specs.dt - data_dt) / data_dt > rtol: |
| raise RuntimeError( |
| f'{model_specs.dt=} does not divide evenly into {data_dt=}' |
| ) |
| return inner_steps |
|
|
|
|
| def _get_datetime_forecast_starts( |
| sample_count: int, |
| first_start: pd.Timestamp, |
| last_start: pd.Timestamp, |
| ) -> pd.DatetimeIndex: |
| """Get equispaced forecast start times for evaluating against ERA5.""" |
| if first_start.hour != 0: |
| raise ValueError(f'dataset times must start at midnight: {first_start=}') |
| |
| |
| stop = last_start.ceil('1D') |
| |
| start_times = pd.date_range(first_start, stop, periods=sample_count + 1)[:-1] |
| |
| |
| parity = np.arange(sample_count) % 2 |
| return start_times.round('1D') + parity * pd.Timedelta('12H') |
|
|
|
|
| P = jax.sharding.PartitionSpec |
|
|
|
|
| class ExperimentState(NamedTuple): |
| opt_state: PyTree |
| params: PyTree |
| ema_params: PyTree |
|
|
|
|
| class Experiment(experiment.AbstractExperiment): |
| """Training experiment based on trajectory loss minimization.""" |
|
|
| def __init__( |
| self, |
| experiment_dir: str, |
| config: Optional[config_dict.ConfigDict] = None, |
| ): |
| """Creates an instance of a training scheme class. |
| |
| Args: |
| experiment_dir: Path to experiment directory. |
| config: config struct setting up the experiment. |
| """ |
| if config is None: |
| config = experiment_config.get_config() |
|
|
| super().__init__( |
| experiment_dir, |
| config.distributed_training, |
| writer_names=['train', 'eval', 'eval_ema'], |
| ) |
| logging.info('Experiment config:\n%s', config) |
| self.config = config |
|
|
| self.train_ds = xarray_utils.open_dataset(config.train_dataset_path) |
| self.eval_ds = xarray_utils.open_dataset(config.eval_dataset_path) |
|
|
| if 'sample' in self.train_ds.dims: |
| logging.warning('only using the first sample!') |
| self.train_ds = self.train_ds.isel(sample=0, drop=True) |
| self.eval_ds = self.eval_ds.isel(sample=0, drop=True) |
|
|
| train_attrs = self.train_ds.attrs |
|
|
| |
| |
| |
| gin.enter_interactive_mode() |
| self.is_nodal = self.config.is_nodal |
|
|
| |
| gin.parse_config(config.model_gin_config) |
| gin.parse_config(config.optimizer_gin_config) |
| experiment_utils.parse_config_dict(config.gin_overrides) |
|
|
| logging.info('Parsed gin config string:\n%s', gin.config_str()) |
|
|
| full_model_gin_config = gin.config_str() |
| logging.info('With overrides gin config string:\n%s', gin.config_str()) |
|
|
| self.data_coords = model_builder.coordinate_system_from_dataset( |
| self.train_ds |
| ) |
| logging.info(f'{self.model_parallel_training=}') |
|
|
| if self.model_parallel_training: |
| |
| |
| |
| if self.spatial_parallelism > 1 and self.config.batch_size_per_device > 1: |
| raise NotImplementedError( |
| f'{self.config.batch_size_per_device=} is not supported for model ' |
| 'parallel training' |
| ) |
| self.data_coords = dataclasses.replace( |
| self.data_coords, spmd_mesh=self.spmd_mesh |
| ) |
|
|
| |
| |
| try: |
| data_aux_features = xarray_utils.aux_features_from_xarray(self.train_ds) |
| except KeyError: |
| data_aux_features = {} |
|
|
| |
| if 'physics_config_str' in train_attrs: |
| physics_config_str = train_attrs['physics_config_str'] |
| experiment_utils.parse_gin_config_without_imports(physics_config_str) |
| else: |
| logging.info( |
| 'physics_config_str was not provided in the dataset, ' |
| 'hence it is expected to be specified in model_gin_config.' |
| ) |
|
|
| self.physics_specs = physics_specifications.get_physics_specs() |
| self.model_specs = model_builder.get_model_specs( |
| self.data_coords, self.physics_specs, data_aux_features |
| ) |
| logging.info(f'{self.model_specs=}') |
|
|
| self.train_inner_steps = _model_inner_steps_for_data( |
| self.train_ds, self.model_specs |
| ) |
| self.eval_inner_steps = _model_inner_steps_for_data( |
| self.eval_ds, self.model_specs |
| ) |
| if ( |
| len(self.config.train_schedule_time_steps) |
| != len(self.config.train_schedule_boundaries) + 1 |
| ): |
| raise ValueError( |
| f'{self.config.train_schedule_time_steps} should be one longer ' |
| f'than {self.config.train_schedule_boundaries} but was not.' |
| ) |
| if any( |
| t % self.train_inner_steps |
| for t in self.config.train_schedule_time_steps |
| ): |
| raise ValueError( |
| f'{self.train_inner_steps=} does not divide ' |
| f'{self.config.train_schedule_time_steps=}' |
| ) |
| if any(t % self.eval_inner_steps for t in self.config.eval_time_steps): |
| raise ValueError( |
| f'{self.eval_inner_steps=} does not divide ' |
| f'{self.config.eval_time_steps=}' |
| ) |
| if max(self.config.eval_time_steps) < max( |
| self.config.train_schedule_time_steps |
| ): |
| raise ValueError( |
| f'Training will not work since {max(self.config.eval_time_steps)=} <' |
| f' {max(self.config.train_schedule_time_steps)=}' |
| ) |
|
|
| self._eval_trajectory_length = ( |
| max(self.config.eval_time_steps) // self.eval_inner_steps + 1 |
| ) |
| self._trajectory_lengths = [ |
| self.config.num_init_frames + n // self.train_inner_steps |
| for n in self.config.train_schedule_time_steps |
| ] |
| self._max_trajectory_length = max( |
| self._trajectory_lengths + [self._eval_trajectory_length] |
| ) |
|
|
| self.reference_datetime = self.model_specs.aux_features[ |
| xarray_utils.REFERENCE_DATETIME_KEY |
| ] |
|
|
| self.whirl_model = model_builder.WhirlModel( |
| **self.model_specs, |
| input_coords=self.data_coords, |
| output_coords=self.data_coords, |
| ) |
| self.from_xarray_fn = self.whirl_model.from_xarray_fn |
|
|
| def trajectory_fwd(x, forcing_data, model, outer_steps, inner_steps): |
| trajectory_fn = model_utils.trajectory_with_inputs_and_forcing( |
| model, config.num_init_frames, start_with_input=True |
| ) |
| return trajectory_fn(x, forcing_data, outer_steps, inner_steps) |
|
|
| self._trajectory_fwd = trajectory_fwd |
|
|
| |
| self._model_dt = self.model_specs.dt |
| self._model_gin_config = full_model_gin_config |
|
|
| |
| self.optimizer = optimization.optimizer() |
|
|
| |
| ema_fn = hk.without_apply_rng( |
| hk.transform_with_state( |
| lambda x: ema_params_tree(config.ema_num_steps)(x) |
| ) |
| ) |
| self._ema_init = jax.jit(ema_fn.init) |
|
|
| def ema_update(params, ema_state): |
| return ema_fn.apply(None, ema_state, params) |
|
|
| self._ema_update = jax.jit(ema_update) |
|
|
| logging.info('Final active config string:\n%s', gin.config_str()) |
|
|
| |
| |
| |
|
|
| @functools.cached_property |
| def spmd_mesh(self) -> jax.sharding.Mesh: |
| n = self.config.model_parallelism.ensemble_shards |
| z = self.config.model_parallelism.z_shards |
| x = self.config.model_parallelism.x_shards |
| y = self.config.model_parallelism.y_shards |
| global_batch = jax.device_count() // (n * z * x * y) |
| if global_batch == 0: |
| raise ValueError( |
| f'{jax.device_count()=} is insufficient for ' |
| f'{self.config.model_parallelism=}' |
| ) |
| return train_utils.create_spmd_mesh( |
| {'batch': global_batch, 'ensemble': n, 'z': z, 'x': x, 'y': y} |
| ) |
|
|
| @functools.cached_property |
| def degree_of_model_parallelism(self) -> int: |
| return math.prod(v for k, v in self.spmd_mesh.shape.items() if k != 'batch') |
|
|
| @functools.cached_property |
| def model_parallel_training(self) -> bool: |
| return self.degree_of_model_parallelism > 1 |
|
|
| @functools.cached_property |
| def spatial_parallelism(self) -> int: |
| return math.prod(self.spmd_mesh.shape[k] for k in 'zxy') |
|
|
| def to_global_array(self, pytree: PyTree, global_batch_size: int) -> PyTree: |
| """Create a pytree of global JAX arrays from a pytree of NumPy arrays.""" |
| |
| return train_utils.make_distributed_array_from_local_arrays( |
| pytree, |
| self.spmd_mesh, |
| self.data_coords.physics_partition_spec, |
| global_batch_size, |
| ) |
|
|
| def num_eval_batches(self, large_eval: bool) -> int: |
| if large_eval: |
| return self.config.num_eval_batches[-1] |
| else: |
| return self.config.num_eval_batches[0] |
|
|
| def steps_between_evals(self, large_eval: bool) -> int: |
| if large_eval: |
| return self.config.steps_between_evals[-1] |
| else: |
| return self.config.steps_between_evals[0] |
|
|
| def eval_batch_size_per_device(self, large_eval: bool) -> int: |
| if large_eval: |
| return self.config.eval_batch_size_per_device[-1] |
| else: |
| return self.config.eval_batch_size_per_device[0] |
|
|
| @functools.cached_property |
| def global_batch_size(self) -> int: |
| return ( |
| jax.device_count() |
| // self.degree_of_model_parallelism |
| * self.config.batch_size_per_device |
| ) |
|
|
| def global_eval_batch_size(self, large_eval: bool) -> int: |
| return ( |
| jax.device_count() |
| // self.degree_of_model_parallelism |
| * self.eval_batch_size_per_device(large_eval) |
| ) |
|
|
| def local_eval_batch_size(self, large_eval: bool) -> int: |
| return jax.local_device_count() * self.eval_batch_size_per_device( |
| large_eval |
| ) |
|
|
| def _to_dataset_iter( |
| self, data: tf.data.Dataset, template: xarray.Dataset |
| ) -> Callable[[], Iterable[Any]]: |
| """Convert a tf.data.Dataset into a function that makes a data iterator.""" |
| leading_dims_set = {x.shape[:2] for x in data.element_spec.values()} |
| assert len(leading_dims_set) == 1, leading_dims_set |
| local_batch_size, time_series_length = leading_dims_set.pop() |
| template = ( |
| template.drop_vars('time') |
| .head(time=time_series_length) |
| .pipe(xarray.zeros_like) |
| .pipe(datasets.drop_static_vars) |
| .transpose('time', ...) |
| .expand_dims(batch=local_batch_size) |
| ) |
|
|
| def make_iterator(): |
| for example_dict in data.as_numpy_iterator(): |
| yield self.from_xarray_fn(template.copy(data=example_dict)) |
|
|
| return make_iterator |
|
|
| def _read_shuffled_shard( |
| self, |
| dataset: xarray.Dataset, |
| time_series_length: int, |
| min_buffer_blocks: int, |
| shard_index: int, |
| shard_count: int, |
| ) -> tf.data.Dataset: |
| sampler = reader.Windower( |
| window_size=time_series_length, |
| stride_between_windows=self.config.train_time_sample_offset, |
| ) |
| local_shard_count = max( |
| self.degree_of_model_parallelism, jax.local_device_count() |
| ) |
| seed = train_utils.combine_rng_seeds( |
| self.config.dataset_rng_seed, shard_index, time_series_length |
| ) |
| data = reader.read_shuffled_shard( |
| dataset, |
| sampler, |
| block_size_in_bytes=self.config.block_size_in_bytes / local_shard_count, |
| buffer_size_in_bytes=( |
| self.config.shuffle_buffer_size_in_bytes / local_shard_count |
| ), |
| min_buffer_blocks=min_buffer_blocks, |
| shard_index=shard_index, |
| shard_count=shard_count, |
| seed=seed, |
| ) |
| return data |
|
|
| def _get_train_dataset(self) -> xarray.Dataset: |
| train_dataset = xarray_utils.ds_with_sim_time( |
| self.train_ds, self.physics_specs, self.reference_datetime |
| ) |
|
|
| if self.config.train_dataset_time_slice: |
| time_slice = slice(*self.config.train_dataset_time_slice) |
| train_dataset = train_dataset.sel(time=time_slice) |
|
|
| if self.config.time_subsample_rate != 1: |
| raise NotImplementedError('subsampling on the fly is not supported yet') |
| if self.config.add_noise_to_input: |
| raise NotImplementedError('add_noise_to_input not supported yet') |
|
|
| return train_dataset |
|
|
| def _spatial_addressable_indices_map( |
| self, spatial_dim_sizes: tuple[int, int, int] |
| ) -> Mapping[jax.Device, tuple[slice, slice, slice, slice]]: |
| """Get slices for indexing global arrays to local devices.""" |
| spec = P('batch', *self.data_coords.physics_partition_spec) |
| sharding = jax.sharding.NamedSharding(self.spmd_mesh, spec) |
| global_shape = (self.global_batch_size,) + spatial_dim_sizes |
| indices_map = sharding.addressable_devices_indices_map(global_shape) |
| indices_map = cast( |
| Mapping[jax.Device, tuple[slice, slice, slice, slice]], indices_map |
| ) |
| return indices_map |
|
|
| def _read_model_parallel_dataset( |
| self, |
| dataset: xarray.Dataset, |
| read_shard: Callable[[xarray.Dataset, int], tf.data.Dataset], |
| ) -> tuple[tf.data.Dataset, xarray.Dataset]: |
| """Read a shard of a training dataset into tf.data.Dataset.""" |
| indices_map = self._spatial_addressable_indices_map( |
| tuple(dataset.sizes[k] for k in ['level', 'longitude', 'latitude']) |
| ) |
|
|
| shard_data: list[tf.data.Dataset] = [] |
| for device in jax.local_devices(): |
| indices = indices_map[device] |
| batch_index = indices[0].start or 0 |
| selection = dict(zip(['level', 'longitude', 'latitude'], indices[1:])) |
| shard_dataset = dataset.isel(selection) |
| shard_data.append(read_shard(shard_dataset, batch_index)) |
|
|
| choices = tf.data.Dataset.range(jax.local_device_count()).repeat() |
| data = tf.data.Dataset.choose_from_datasets(shard_data, choices) |
| template = shard_dataset |
| return data, template |
|
|
| def _build_train_inputs( |
| self, time_series_length: int |
| ) -> tuple[Callable[[], Any], dict[str, Any]]: |
| """Loads the training dataset and returns an iterator and train_attrs.""" |
| train_dataset = self._get_train_dataset() |
| local_batch_size = ( |
| |
| self.config.batch_size_per_device |
| * jax.local_device_count() |
| ) |
| if self.model_parallel_training: |
|
|
| def read_shard(shard_dataset, batch_index): |
| return self._read_shuffled_shard( |
| shard_dataset, |
| time_series_length, |
| min_buffer_blocks=1, |
| shard_index=batch_index, |
| shard_count=self.global_batch_size, |
| ) |
|
|
| data, template = self._read_model_parallel_dataset( |
| train_dataset, read_shard |
| ) |
|
|
| else: |
| data = self._read_shuffled_shard( |
| train_dataset, |
| time_series_length, |
| shard_index=jax.process_index(), |
| shard_count=jax.process_count(), |
| min_buffer_blocks=local_batch_size, |
| ) |
| template = train_dataset |
|
|
| data = data.repeat() |
| data = data.batch(local_batch_size, drop_remainder=True) |
| data = data.prefetch(tf.data.AUTOTUNE) |
| train_iter = self._to_dataset_iter(data, template) |
| data_attrs = datasets.attrs_from_dataset(train_dataset, time_series_length) |
| return train_iter, data_attrs |
|
|
| def build_train_and_eval_iterators( |
| self, |
| schedule_idx: int, |
| start_step: int, |
| large_eval: bool, |
| ) -> TrainEvalIteratorTuple: |
| """Build new iterators for training at schedule_idx. |
| |
| Args: |
| schedule_idx: Index into the rollout schedule. |
| start_step: Step at which this training run started at. This does not |
| change unless the Borg job dies and restarts. |
| large_eval: Whether this evaluation should be done over a larger set of |
| data. |
| |
| Returns: |
| TrainEvalIteratorTuple: Tuple consisting of |
| get_train_data. Iterator providing next set of training data. |
| train_step_fn. train_utils.TrainStepFunction to update weights. |
| evaluate_fn. Callable to evalate metrics and write results. |
| ckpt_kwargs. dict[str, Any] of kwargs to add to the checkpoint. |
| """ |
| num_train_time_steps = self.config.train_schedule_time_steps[schedule_idx] |
| trajectory_length = self._trajectory_lengths[schedule_idx] |
|
|
| train_traj_spec = metrics_util.TrajectorySpec( |
| trajectory_length, |
| self._max_trajectory_length, |
| self.train_inner_steps, |
| coords=self.model_specs.coords, |
| data_coords=self.data_coords, |
| ) |
|
|
| |
| get_eval_data, eval_attrs = self.build_eval_inputs( |
| self.config.eval_dataset_time_slice, |
| large_eval, |
| ) |
| get_eval_on_train, _ = self.build_eval_inputs( |
| self.config.train_dataset_time_slice, |
| large_eval, |
| ) |
|
|
| evaluate_fn = functools.partial( |
| self.evaluate, |
| eval_batch_fn=self.get_eval_batch_fn(train_traj_spec), |
| get_eval_data=get_eval_data, |
| get_train_data=get_eval_on_train, |
| large_eval=large_eval, |
| ) |
| get_train_data, train_attrs = self._build_train_inputs(trajectory_length) |
| ckpt_kwargs = { |
| 'train_attrs': train_attrs, |
| 'eval_attrs': eval_attrs, |
| } |
|
|
| train_step_fn = self.get_train_step_fn( |
| num_train_time_steps, train_traj_spec |
| ) |
|
|
| if ( |
| self.config.profile_with_xprof |
| and schedule_idx == 0 |
| and experiment_utils.is_coordinator() |
| and start_step == 0 |
| ): |
| train_step_fn = profiling_util.Traced( |
| train_step_fn, |
| trace_name='train step', |
| skip_steps=2, |
| num_trace_steps=3, |
| enable_python_tracer=True, |
| host_trace_level=3, |
| ) |
| return get_train_data(), train_step_fn, evaluate_fn, ckpt_kwargs |
|
|
| def _get_eval_dataset(self) -> xarray.Dataset: |
| return xarray_utils.ds_with_sim_time( |
| self.eval_ds, self.physics_specs, self.reference_datetime |
| ) |
|
|
| def build_eval_inputs( |
| self, |
| dataset_time_slice: tuple[str, str] | None, |
| large_eval: bool, |
| ) -> tuple[Callable[[], Any], Any]: |
| """Returns an iterable over the data and data attrs for evaluation.""" |
| eval_dataset = self._get_eval_dataset() |
|
|
| num_eval_batches = self.num_eval_batches(large_eval) |
| eval_batch_size_per_device = self.eval_batch_size_per_device(large_eval) |
| time_series_length = ( |
| self._eval_trajectory_length + self.config.num_init_frames - 1 |
| ) |
| local_batch_size = eval_batch_size_per_device * jax.local_device_count() |
|
|
| if isinstance(eval_dataset.indexes['time'], pd.DatetimeIndex): |
| |
| |
| assert 'sample' not in eval_dataset.dims |
| logging.info('Using eval data loader for build_eval_inputs') |
| sample_count = self.global_eval_batch_size(large_eval) * num_eval_batches |
| if dataset_time_slice: |
| time_source = eval_dataset.time.loc[slice(*dataset_time_slice)] |
| else: |
| time_source = eval_dataset |
| first_start = time_source.indexes['time'][0] |
| last_start = time_source.indexes['time'][-1] |
| starts = _get_datetime_forecast_starts( |
| sample_count, first_start, last_start |
| ) |
| logging.info(f'determined evaluation data for {sample_count=}: {starts=}') |
| offsets = eval_dataset.indexes['time'].get_indexer(starts) |
| sampler = reader.WindowerAtOffsets( |
| window_size=time_series_length, window_offsets=offsets |
| ) |
| if self.model_parallel_training: |
|
|
| def read_shard(shard_dataset, batch_index): |
| selector = reader.ShardSelector(batch_index, len(starts)) |
| return reader.read_timeseries(shard_dataset, sampler, selector) |
|
|
| data, template = self._read_model_parallel_dataset( |
| eval_dataset, read_shard |
| ) |
|
|
| else: |
| selector = reader.ShardSelector( |
| jax.process_index(), jax.process_count() |
| ) |
| data = reader.read_timeseries(eval_dataset, sampler, selector) |
| template = eval_dataset |
|
|
| data = data.batch(local_batch_size, drop_remainder=True) |
| data = data.cache() |
| else: |
| |
| |
| assert not self.model_parallel_training |
| if dataset_time_slice: |
| eval_dataset = eval_dataset.sel(time=slice(*dataset_time_slice)) |
| data = self._read_shuffled_shard( |
| eval_dataset, |
| time_series_length, |
| shard_index=jax.process_index(), |
| shard_count=jax.process_count(), |
| min_buffer_blocks=local_batch_size * num_eval_batches, |
| ) |
| data = data.batch(local_batch_size, drop_remainder=True) |
| data = data.take(num_eval_batches) |
| template = eval_dataset |
|
|
| logging.info(f'created eval data: {data}') |
| eval_iter = self._to_dataset_iter(data, template) |
| data_attrs = datasets.attrs_from_dataset(eval_dataset, time_series_length) |
| return eval_iter, data_attrs |
|
|
| |
| |
| |
|
|
| def _make_initial_experiment_state( |
| self, |
| rng: typing.PRNGKeyArray, |
| init_example, |
| init_forcing_data: typing.ForcingData, |
| init_params: Optional[typing.Params] = None, |
| ) -> ExperimentState: |
| """Makes initial parameters (via hk.Module.init).""" |
| if self.eval_inner_steps != self.train_inner_steps: |
| raise ValueError( |
| 'KroneckerCorrelatedL2LossModule stddev will be ill defined since ' |
| f'{self.eval_inner_steps=} != {self.train_inner_steps=}' |
| ) |
| trajectory_length = list(init_example.values())[0].shape[0] |
|
|
| @jax.jit |
| def init(rng, init_example, init_forcing_data): |
| outer_steps = (trajectory_length - self.config.num_init_frames) + 1 |
| |
| |
| if init_params is None: |
| init_fn = jax.vmap( |
| self._make_unbatched_trajectory_fn(outer_steps).init, |
| in_axes=None, |
| out_axes=0, |
| spmd_axis_name='ensemble', |
| axis_size=1, |
| ) |
| unsqueezd_params = init_fn(rng, init_example, init_forcing_data) |
| params = tree_map(lambda x: jnp.squeeze(x, axis=0), unsqueezd_params) |
| else: |
| params = init_params |
| opt_state = self.optimizer.init(params) |
| _, ema_state = self._ema_init(None, params) |
| experiment_state = ExperimentState(opt_state, params, ema_state) |
| experiment_state = train_utils.ensure_replicated( |
| experiment_state, mesh=self.spmd_mesh |
| ) |
| return experiment_state |
|
|
| return init(rng, init_example, init_forcing_data) |
|
|
| def _make_unbatched_trajectory_fn(self, outer_steps: int): |
| """Haiku transformation of func giving (prediction, target) trajectories. |
| |
| Args: |
| outer_steps: Number of outer steps the trajectory should take. |
| |
| Returns: |
| hk transformed object. The .apply member maps |
| (params, rng, target, forcing_data) --> (prediction, target) |
| """ |
| if self.train_inner_steps != self.eval_inner_steps: |
| |
| raise ValueError(f'{self.train_inner_steps=} != {self.eval_inner_steps=}') |
|
|
| @hk.transform |
| def unbatched_trajectory_fn(target, forcing_data): |
| """Compute Fwd(target[0]) on one single batch/device.""" |
| |
| model = self.whirl_model.model_cls() |
| _, predicted_trajectory = self._trajectory_fwd( |
| x=target, |
| forcing_data=forcing_data, |
| model=model, |
| outer_steps=outer_steps, |
| inner_steps=self.train_inner_steps, |
| ) |
| prediction, target = ( |
| model_utils.compute_prediction_and_target_representations( |
| predicted_trajectory, target, forcing_data, model |
| ) |
| ) |
| return prediction, target |
|
|
| return unbatched_trajectory_fn |
|
|
| def _make_batch_trajectory_fn( |
| self, |
| outer_steps: int, |
| ) -> TrajectoryFn: |
| """Target, prediction representations with shape (batch, ensemble, ...).""" |
|
|
| ensembled_fn = jax.vmap( |
| |
| self._make_unbatched_trajectory_fn(outer_steps).apply, |
| in_axes=(None, 0, None, None), |
| spmd_axis_name='ensemble', |
| ) |
|
|
| batch_ensembled_fn = jax.vmap( |
| |
| |
| |
| |
| |
| |
| ensembled_fn, |
| in_axes=(None, 0, 0, 0), |
| spmd_axis_name='batch', |
| ) |
| return batch_ensembled_fn |
|
|
| def get_train_step_fn( |
| self, |
| num_train_time_steps: int, |
| traj_spec: metrics_util.TrajectorySpec, |
| ) -> train_utils.TrainStepFunction: |
| """Makes a function to update weights via gradient descent. |
| |
| This function makes use of on-device batching. Multiple devices are combined |
| via an all reduce step whereby the average (across devices) gradient is |
| applied to each (on device) params. |
| |
| Args: |
| num_train_time_steps: Number of time steps for the trajectory in this |
| training step. |
| traj_spec: Specification of training trajectory. |
| |
| Returns: |
| all_reduced_train_step: Function to exectute one training step. |
| Params are injected into self._trajectory_fwd and Loss (if Loss requires |
| Haiku params). |
| """ |
|
|
| batch_trajectory_fn = self._make_batch_trajectory_fn( |
| |
| outer_steps=num_train_time_steps // self.train_inner_steps |
| + 1, |
| ) |
|
|
| loss_fn = get_loss_obj(traj_spec).evaluate |
| ensembled_loss_fn = jax.vmap( |
| loss_fn, |
| axis_name='ensemble', |
| spmd_axis_name='ensemble', |
| ) |
| batch_ensembled_loss_fn = jax.vmap( |
| ensembled_loss_fn, |
| axis_name='batch', |
| spmd_axis_name='batch', |
| ) |
|
|
| def batched_parameter_loss_fn(params, rng, target, forcing_data): |
| """Mean (over on-device batch members) of loss w.r.t parameters.""" |
| |
| |
| |
| |
| |
| prediction, target = batch_trajectory_fn( |
| |
| |
| params, |
| rng, |
| target, |
| forcing_data, |
| ) |
| |
| per_example_loss = batch_ensembled_loss_fn(prediction, target) |
| |
| |
| |
| |
| overall_loss = jnp.mean(per_example_loss, axis=(0, 1)) |
| assert overall_loss.ndim == 0 |
| return overall_loss |
|
|
| |
| |
| |
| @train_utils.jit_once |
| def train_step(experiment_state, rng, target_trajectory, forcing_data): |
| opt_state, params, ema_state = experiment_state |
| rng = train_utils.ensure_sharded_rng_key(rng, mesh=self.spmd_mesh) |
| loss, grad = jax.value_and_grad(batched_parameter_loss_fn)( |
| params, rng, target_trajectory, forcing_data |
| ) |
| updates, opt_state = self.optimizer.update(grad, opt_state, params) |
| params = optax.apply_updates(params, updates) |
| _, ema_state = self._ema_update(params, ema_state) |
| experiment_state = ExperimentState(opt_state, params, ema_state) |
| experiment_state = train_utils.ensure_replicated( |
| experiment_state, mesh=self.spmd_mesh |
| ) |
| return experiment_state, loss |
|
|
| return train_step |
|
|
| def get_eval_batch_fn( |
| self, |
| train_traj_spec: metrics_util.TrajectorySpec, |
| ) -> train_utils.EvalStepFunction: |
| """Makes a function that performs a single evaluation pass. |
| |
| Args: |
| train_traj_spec: TrajectorySpec for training. Used to add the "loss" |
| evaluation metrics. |
| |
| Returns: |
| Function mapping (params, rng, target, forcing_data) to dictionary of |
| scalar metric values. Parameters are injected into self._trajectory_fwd |
| and Loss (if Loss requires Haiku params). |
| """ |
| eval_traj_spec = metrics_util.TrajectorySpec( |
| self._eval_trajectory_length, |
| self._max_trajectory_length, |
| steps_per_save=self.eval_inner_steps, |
| coords=self.model_specs.coords, |
| data_coords=self.data_coords, |
| ) |
|
|
| eval_time_steps = [ |
| t // self.eval_inner_steps for t in self.config.eval_time_steps |
| ] |
| if any(t % self.eval_inner_steps for t in self.config.eval_time_steps): |
| raise ValueError( |
| f'cannot evaluate {self.config.eval_time_steps=} with ' |
| f'{self.eval_inner_steps=}' |
| ) |
|
|
| batch_trajectory_fn = self._make_batch_trajectory_fn( |
| |
| outer_steps=max( |
| self._eval_trajectory_length, |
| train_traj_spec.trajectory_length, |
| ), |
| ) |
|
|
| def unbatched_eval_fn( |
| prediction: TrajectoryRepresentations, target: TrajectoryRepresentations |
| ): |
| """Evaluate(target, Fwd(target[0])) on one single batch/device.""" |
| metrics_dict = get_metrics_dict( |
| eval_traj_spec, |
| eval_time_steps, |
| get_loss_obj(train_traj_spec), |
| is_ensemble_data=bool(self.config.ensemble_size), |
| ) |
| return train_utils.flatten_dict({ |
| k: metric.evaluate(prediction, target) |
| for k, metric in metrics_dict.items() |
| }) |
|
|
| ensembled_fn = jax.vmap( |
| unbatched_eval_fn, axis_name='ensemble', spmd_axis_name='ensemble' |
| ) |
|
|
| batch_ensembled_fn = jax.vmap( |
| ensembled_fn, |
| axis_name='batch', |
| spmd_axis_name='batch', |
| ) |
|
|
| @train_utils.jit_once |
| def batch_mean_eval_fn(params, rng, target, forcing_data): |
| """Computes mean (over batch members) of evaluation.""" |
| |
| |
| |
| |
| |
| rng = train_utils.ensure_sharded_rng_key(rng, mesh=self.spmd_mesh) |
| prediction, target = batch_trajectory_fn( |
| |
| |
| params, |
| rng, |
| target, |
| forcing_data, |
| ) |
| batch_eval_values = batch_ensembled_fn(prediction, target) |
| return tree_map(jnp.mean, batch_eval_values) |
|
|
| return batch_mean_eval_fn |
|
|
| def run_training(self): |
| """See base class.""" |
| ( |
| start_step, |
| times_restarted_on_nan, |
| step_auto_restart_began_at, |
| experiment_state, |
| ) = self.initialize_experiment( |
| initial_checkpoint_path=self.config.initial_checkpoint_path |
| ) |
|
|
| if start_step >= self.config.num_training_steps: |
| logging.warning( |
| f'Attempting to start training at {start_step=} >=' |
| f' {self.config.num_training_steps=}. Will simply return' |
| ) |
| return |
|
|
| def logging_callback(step, loss, times_restarted_on_nan): |
| loss = float(jax.device_get(loss)) |
| if step % max(self.steps_between_evals(False) // 100, 1) == 0: |
| logging.info(f'{step=}, {loss=}') |
| if ( |
| self.config.error_with_nan_loss |
| and times_restarted_on_nan > self.config.max_nan_restarts |
| ): |
| raise RuntimeError( |
| f'NaN loss detected at {step=}, after too many restarts since' |
| f' {times_restarted_on_nan=} >' |
| f' {self.config.max_nan_restarts=}. Aborting.' |
| ) |
|
|
| |
| logging_stream = streaming.SingleThreadExecutor(logging_callback) |
| logging.info('starting training from step=%s', start_step) |
| train_step_timer = timing_util.Timer() |
|
|
| rng_stream = train_utils.BatchedPRNGSequence( |
| jax.random.PRNGKey(self.config.init_rng_seed), |
| batch_shape=(self.global_batch_size, self.config.ensemble_size or 1), |
| ) |
|
|
| loss = 1.0 |
| schedule_idx = None |
| ckpt_kwargs = {} |
|
|
| step = start_step |
| while step < self.config.num_training_steps: |
| old_schedule_idx = schedule_idx |
| schedule_idx = np.sum( |
| step > np.asarray(self.config.train_schedule_boundaries) |
| ) |
| large_eval = ( |
| schedule_idx == len(self.config.train_schedule_time_steps) - 1 |
| ) |
| if schedule_idx != old_schedule_idx: |
| ( |
| train_iter, |
| train_step_fn, |
| evaluate_fn, |
| ckpt_kwargs, |
| ) = self.build_train_and_eval_iterators( |
| schedule_idx=schedule_idx, |
| start_step=start_step, |
| large_eval=large_eval, |
| ) |
|
|
| if ( |
| np.isnan(loss) |
| |
| |
| |
| and times_restarted_on_nan <= self.config.max_nan_restarts |
| ): |
| |
| logging.warning( |
| f'NaN loss encountered at {step=}. Re-initializing and incrementing' |
| f' times_restarted_on_nan to {times_restarted_on_nan + 1}' |
| ) |
| times_restarted_on_nan += 1 |
| step, _, _, experiment_state = self.initialize_experiment( |
| target_step=step |
| - times_restarted_on_nan * self.config.restart_lookback_steps |
| ) |
| step_auto_restart_began_at = step_auto_restart_began_at or step |
|
|
| |
| if step % self.steps_between_evals(large_eval) == 0: |
| self.save_checkpoint( |
| step, |
| experiment_state, |
| checkpoint_buffer_size=self.config.checkpoint_buffer_size, |
| times_restarted_on_nan=times_restarted_on_nan, |
| step_auto_restart_began_at=step_auto_restart_began_at, |
| **ckpt_kwargs, |
| ) |
| elif step % self.config.steps_between_checkpoints == 0: |
| max_lookback_step = ( |
| step |
| - self.config.max_nan_restarts * self.config.restart_lookback_steps |
| ) |
| if ( |
| |
| |
| times_restarted_on_nan |
| and max_lookback_step > step_auto_restart_began_at |
| ): |
| logging.info( |
| f'Significant progress made since {step_auto_restart_began_at=}.' |
| f' In particular, {step=} Therefore set times_restarted_on_nan' |
| ' to 0' |
| ) |
| times_restarted_on_nan = 0 |
| step_auto_restart_began_at = None |
| self.save_checkpoint( |
| step, |
| experiment_state, |
| update_latest_only=True, |
| checkpoint_buffer_size=self.config.checkpoint_buffer_size, |
| times_restarted_on_nan=times_restarted_on_nan, |
| step_auto_restart_began_at=step_auto_restart_began_at, |
| **ckpt_kwargs, |
| ) |
|
|
| |
| if (step + 1) % self.steps_between_evals(large_eval) == 0: |
| if step > start_step: |
| with train_step_timer: |
| |
| |
| experiment_state = jax.block_until_ready(experiment_state) |
| eval_interval = self.steps_between_evals(large_eval) |
| training_time = train_step_timer.total |
| logging.info( |
| f'training for {eval_interval} steps took ' |
| f'{training_time:.1f} seconds' |
| ) |
| self.record_scalar( |
| 'train', |
| tag='seconds_per_train_step', |
| step=step, |
| value=training_time / eval_interval, |
| ) |
| train_step_timer = timing_util.Timer() |
|
|
| if isinstance(train_step_fn, profiling_util.Traced): |
| memory_usage = train_step_fn.tracer.memory_usage |
| if memory_usage is not None: |
| self.record_scalar( |
| 'train', |
| tag='peak_memory_usage_mib', |
| step=step, |
| value=memory_usage, |
| ) |
|
|
| with timing_util.Timer() as eval_timer: |
| evaluate_fn(step, experiment_state, seed=step) |
| self.record_scalar( |
| 'train', |
| tag='seconds_per_evaluation', |
| step=step, |
| value=eval_timer.average, |
| ) |
| logging.info('evaluation pass took %.1f seconds', eval_timer.average) |
| self.flush_writers() |
|
|
| |
| with train_step_timer: |
| |
| with jax.profiler.StepTraceAnnotation('train', step_num=step): |
| batch, forcing_data = self.to_global_array( |
| next(train_iter), self.global_batch_size |
| ) |
| |
| |
| with self.spmd_mesh: |
| experiment_state, loss = train_step_fn( |
| experiment_state, next(rng_stream), batch, forcing_data |
| ) |
| |
| |
| |
| |
| loss = jax.device_get(loss) |
| logging_stream.wait() |
| logging_stream.put(step, loss, times_restarted_on_nan) |
| step += 1 |
| |
|
|
| evaluate_fn(self.config.num_training_steps, experiment_state) |
| self.finalize_training( |
| self.config.num_training_steps, |
| experiment_state, |
| times_restarted_on_nan=times_restarted_on_nan, |
| step_auto_restart_began_at=step_auto_restart_began_at, |
| **ckpt_kwargs, |
| ) |
|
|
| def make_dummy_inputs(self) -> tuple[Any, Any]: |
| train_dataset = xarray_utils.ds_with_sim_time( |
| self.train_ds, self.physics_specs, self.reference_datetime |
| ) |
| dummy_ds = ( |
| train_dataset.drop_vars('time') |
| .head(time=self.config.num_init_frames) |
| .pipe(xarray.zeros_like) |
| .pipe(datasets.drop_static_vars) |
| .transpose('time', ...) |
| ) |
| return self.from_xarray_fn(dummy_ds) |
|
|
| def initialize_experiment( |
| self, |
| initial_checkpoint_path: Optional[str] = None, |
| target_step: Optional[int] = None, |
| ) -> tuple[int, int, int | None, ExperimentState]: |
| """Returns training step and experiment state from checkpoint or init.""" |
|
|
| if target_step: |
| ckpt = self.load_buffered_checkpoint(target_step=target_step) |
| if ckpt is None: |
| logging.info( |
| 'No acceptable buffered checkpoint found for {target_step=}' |
| ) |
| else: |
| logging.info( |
| f'Using buffered checkpoint, which has step={ckpt.step}. Ideally ' |
| f'would have used {target_step=}' |
| ) |
| else: |
| ckpt = self.load_latest_checkpoint() |
| if ckpt is None: |
| logging.info('No latest checkpoint found') |
| else: |
| logging.info(f'Using latest checkpoint, which has step={ckpt.step}') |
|
|
| init_params = None |
| if ckpt is None and initial_checkpoint_path is not None: |
| ckpt = checkpoint.load_checkpoint(initial_checkpoint_path) |
| if self.config.reset_initial_optimizer_state: |
| init_params = ckpt.eval_params |
| ckpt = None |
|
|
| if ckpt is not None: |
| start_step = ckpt.step |
| logging.info(f'resuming from checkpoint at step={start_step}') |
| times_restarted_on_nan = getattr(ckpt, 'times_restarted_on_nan', 0) |
| step_auto_restart_began_at = getattr( |
| ckpt, 'step_auto_restart_began_at', None |
| ) |
| experiment_state = ExperimentState( |
| ckpt.opt_state, ckpt.train_params, ckpt.ema_state |
| ) |
| else: |
| logging.info('starting training with new weights') |
| start_step = 0 |
| times_restarted_on_nan = 0 |
| step_auto_restart_began_at = None |
| rng = jax.random.PRNGKey(self.config.init_rng_seed) |
| init_example, init_forcing_data = self.make_dummy_inputs() |
| experiment_state = self._make_initial_experiment_state( |
| rng, init_example, init_forcing_data, init_params=init_params |
| ) |
|
|
| return ( |
| start_step, |
| times_restarted_on_nan, |
| step_auto_restart_began_at, |
| experiment_state, |
| ) |
|
|
| def _checkpoint_state( |
| self, |
| step: int, |
| experiment_state: ExperimentState, |
| times_restarted_on_nan: int, |
| step_auto_restart_began_at: int, |
| train_attrs: dict[str, Any], |
| eval_attrs: dict[str, Any], |
| ) -> checkpoint.CheckpointState: |
| """Returns a checkpoint state for a given experiment_state.""" |
| opt_state, params, ema_state = experiment_state |
| ema_params, _ = self._ema_update(params, ema_state) |
| ckpt_state = checkpoint.CheckpointState( |
| train_params=params, |
| eval_params=ema_params, |
| opt_state=opt_state, |
| ema_state=ema_state, |
| step=step, |
| model_time_step=self._model_dt, |
| model_config_str=self._model_gin_config, |
| train_dataset_path=self.config.train_dataset_path, |
| eval_dataset_path=self.config.eval_dataset_path, |
| times_restarted_on_nan=times_restarted_on_nan, |
| step_auto_restart_began_at=step_auto_restart_began_at, |
| train_attrs=train_attrs, |
| eval_attrs=eval_attrs, |
| ) |
| return ckpt_state |
|
|
| def evaluate( |
| self, |
| step, |
| experiment_state, |
| eval_batch_fn, |
| get_eval_data, |
| get_train_data, |
| large_eval: bool, |
| seed=0, |
| ): |
| """Evaluates the model on train and eval data and writes summaries. |
| |
| Args: |
| step: global training step. |
| experiment_state: tuple of replicated step, optimizer state and EMA |
| (exponentially moving average) state for model parameters. |
| eval_batch_fn: function that, given parameters; rng; batch of data, |
| computes evaluation metric of interest on the given samples. |
| get_eval_data: callable that returns an iterator over evaluation data that |
| is used for produce summaries on unseen evaluation data. |
| get_train_data: callable that returns an iterator over training data that |
| is used for produce summaries on training data. |
| large_eval: Whether this evaluation is on the larger size eval data. |
| seed: seed for the random number generator to be used for evaluation. |
| """ |
| num_eval_batches = self.num_eval_batches(large_eval) |
| if num_eval_batches == 0: |
| logging.warning(f'skipping evaluation: {num_eval_batches=}') |
| return |
|
|
| _, params, ema_state = experiment_state |
| ema_params, _ = self._ema_update(params, ema_state) |
|
|
| global_batch_size = self.global_eval_batch_size(large_eval) |
| rng_stream = train_utils.BatchedPRNGSequence( |
| jax.random.PRNGKey(seed), |
| batch_shape=(global_batch_size, self.config.ensemble_size or 1), |
| ) |
| to_global_array = functools.partial( |
| self.to_global_array, global_batch_size=global_batch_size |
| ) |
|
|
| |
| |
| |
| with self.spmd_mesh: |
| logging.info('evaluating on train dataset') |
| metrics_ = train_utils.streaming_mean( |
| rng_stream, |
| map(to_global_array, get_train_data()), |
| functools.partial(eval_batch_fn, params), |
| ) |
| for tag, value in metrics_.items(): |
| self.record_scalar('train', tag=tag, value=value, step=step) |
|
|
| logging.info('evaluating on test dataset') |
| metrics_ = train_utils.streaming_mean( |
| rng_stream, |
| map(to_global_array, get_eval_data()), |
| functools.partial(eval_batch_fn, params), |
| ) |
| for tag, value in metrics_.items(): |
| self.record_scalar('eval', tag=tag, value=value, step=step) |
|
|
| logging.info('evaluating EMA model on test dataset') |
| metrics_ = train_utils.streaming_mean( |
| rng_stream, |
| map(to_global_array, get_eval_data()), |
| functools.partial(eval_batch_fn, ema_params), |
| ) |
| for tag, value in metrics_.items(): |
| self.record_scalar('eval_ema', tag=tag, value=value, step=step) |
|
|
|
|
| if __name__ == '__main__': |
| app.run(functools.partial(run_training.main, Experiment)) |
|
|