Skip to content

Commit 244ab65

Browse files
Merge pull request #226 from metno/feat/multiple_datasets
Feat/multiple datasets
2 parents 243f26c + 9bb937e commit 244ab65

53 files changed

Lines changed: 1671 additions & 2345 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/run_test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
python-version: ["3.10"]
17+
python-version: ["3.11"]
1818

1919
steps:
2020
- uses: actions/checkout@v4
@@ -61,7 +61,7 @@ jobs:
6161
6262
- name: Run tox unit tests (6G)
6363
run: |
64-
tox -e py310
64+
tox -e py311
6565
6666
- name: Run tox security check (<50M)
6767
run: |

bris/__main__.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
LOGGER,
1616
create_config,
1717
get_all_leadtimes,
18+
get_dataset_config,
19+
get_interpolator_timestep_seconds,
20+
get_model_multistep_input,
21+
get_model_timestep,
1822
parse_args,
1923
set_base_seed,
2024
set_encoder_decoder_num_chunks,
@@ -27,6 +31,7 @@ def main(arg_list: list[str] | None = None):
2731
t0 = time.perf_counter()
2832
args = parse_args(arg_list)
2933
config = create_config(args["config"], args)
34+
3035
setup_logging(config)
3136

3237
models = list(config.checkpoints.keys())
@@ -46,28 +51,20 @@ def main(arg_list: list[str] | None = None):
4651
set_base_seed()
4752

4853
# Compute timestep_seconds for each checkpoint
49-
config.checkpoints.forecaster.timestep = checkpoints[
50-
"forecaster"
51-
].config.data.timestep
54+
config.checkpoints.forecaster.timestep = get_model_timestep(
55+
checkpoints["forecaster"]
56+
)
5257
config.checkpoints.forecaster.timestep_seconds = frequency_to_seconds(
5358
config.checkpoints.forecaster.timestep
5459
)
5560

5661
if "interpolator" in checkpoints:
57-
target_times = checkpoints[
58-
"interpolator"
59-
].metadata.config.training.explicit_times.target
60-
input_times = checkpoints[
61-
"interpolator"
62-
].metadata.config.training.explicit_times.input
63-
if target_times[-1] == input_times[-1]:
64-
config.checkpoints.interpolator.timestep_seconds = int(
65-
config.checkpoints.forecaster.timestep_seconds / len(target_times)
66-
)
67-
else:
68-
config.checkpoints.interpolator.timestep_seconds = int(
69-
config.checkpoints.forecaster.timestep_seconds / (len(target_times) + 1)
62+
config.checkpoints.interpolator.timestep_seconds = (
63+
get_interpolator_timestep_seconds(
64+
checkpoints["interpolator"],
65+
config.checkpoints.forecaster.timestep_seconds,
7066
)
67+
)
7168

7269
num_members = config["hardware"].get("num_members", 1)
7370

@@ -89,11 +86,7 @@ def main(arg_list: list[str] | None = None):
8986
num_members_in_parallel = num_members
9087

9188
# Get multistep. A default of 2 to ignore multistep in start_date calculation if not set.
92-
multistep = 2
93-
try:
94-
multistep = checkpoints["forecaster"].config.training.multistep_input
95-
except KeyError:
96-
LOGGER.debug("Multistep not found in checkpoint")
89+
multistep = get_model_multistep_input(checkpoints["forecaster"])
9790

9891
# If no start_date given, calculate as end_date-((multistep-1)*timestep)
9992
if "start_date" not in config or config.start_date is None:
@@ -116,13 +109,8 @@ def main(arg_list: list[str] | None = None):
116109
),
117110
"%Y-%m-%dT%H:%M:%S",
118111
)
119-
120-
config.dataset = {
121-
"dataset": config.dataset,
122-
"start": config.start_date,
123-
"end": config.end_date,
124-
"frequency": config.frequency,
125-
}
112+
# Get dataset config with backwards comapatibility for single dataset config setup
113+
config.datasets = get_dataset_config(config)
126114

127115
datamodule = DataModule(
128116
config=config,

bris/checkpoint.py

Lines changed: 50 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import os
33
from copy import deepcopy
44
from functools import cached_property
5-
from typing import Optional
65

76
import torch
87
from anemoi.utils.checkpoints import load_metadata
@@ -48,7 +47,7 @@ class Metadata(DotDict):
4847
class Checkpoint:
4948
"""This class makes accessible various information stored in Anemoi checkpoints."""
5049

51-
def __init__(self, path: str, graph: Optional[str] = None):
50+
def __init__(self, path: str, graph: str | None = None):
5251
assert os.path.exists(path), f"The given checkpoint {path} does not exist!"
5352

5453
self.path = path
@@ -133,8 +132,50 @@ def _load_model(self) -> torch.nn.Module:
133132
"check module versions."
134133
) from e
135134
raise e
135+
if not torch.cuda.is_available():
136+
self._apply_triton_cpu_fallback(inst)
136137
return inst
137138

139+
def _apply_triton_cpu_fallback(self, model: torch.nn.Module) -> None:
140+
"""Replace Triton graph attention with the PyG backend when running on CPU.
141+
142+
anemoi-models checks is_triton_available() at model construction time and falls
143+
back to the PyG backend automatically. However, when a model is loaded from a
144+
checkpoint via torch.load() (weights_only=False), __init__ is not called —
145+
pickle restores __dict__ directly — so the Triton function reference is
146+
preserved even when no GPU is available. This method applies the same fallback
147+
after loading.
148+
149+
GraphTransformerConv has no trainable parameters, so the swap is safe.
150+
"""
151+
try:
152+
from anemoi.models.layers.block import GraphTransformerBaseBlock
153+
from anemoi.models.layers.conv import GraphTransformerConv
154+
except ImportError:
155+
LOGGER.warning(
156+
"Could not import anemoi.models layers to apply Triton->PyG CPU fallback."
157+
)
158+
return
159+
160+
patched = 0
161+
for module in model.modules():
162+
if (
163+
isinstance(module, GraphTransformerBaseBlock)
164+
and module.graph_attention_backend == "triton"
165+
):
166+
module.graph_attention_backend = "pyg"
167+
module.conv = GraphTransformerConv(
168+
out_channels=module.out_channels_conv
169+
)
170+
patched += 1
171+
172+
if patched:
173+
LOGGER.warning(
174+
"Checkpoint was saved with the Triton graph attention backend but no GPU "
175+
"is available. Fell back to the PyG backend for %d block(s).",
176+
patched,
177+
)
178+
138179
@property
139180
def graph(self) -> HeteroData:
140181
"""
@@ -180,7 +221,7 @@ def graph(self) -> HeteroData:
180221
# _model_params = self._model_instance.named_parameters()
181222
# return deepcopy(dict(_model_params))
182223

183-
def update_graph(self, path: Optional[str] = None) -> HeteroData:
224+
def update_graph(self, path: str | None = None) -> HeteroData:
184225
"""
185226
Replaces existing graph object within model instance.
186227
The new graph is either provided as an torch file or
@@ -219,129 +260,10 @@ def update_graph(self, path: Optional[str] = None) -> HeteroData:
219260
self._model_instance.load_state_dict(new_state_dict)
220261
return self._model_instance.graph_data
221262

222-
@property
223-
def name_to_index(self) -> tuple[dict[str, int], ...]:
224-
"""
225-
Mapping between name and their corresponding variable index.
226-
Returns a tuple. If the model is a multiencoder-decoder model
227-
the tuple will contain two dicts, one for each decoder. If not
228-
the tuple will contain a single dict.
229-
"""
230-
_data_indices = self._model_instance.data_indices
231-
if isinstance(_data_indices, (tuple, list)) and len(_data_indices) >= 2:
232-
return tuple(
233-
_data_indices[k].name_to_index for k in range(len(_data_indices))
234-
)
235-
236-
return (_data_indices.name_to_index,)
237-
238-
@property
239-
def index_to_name(self) -> tuple[dict[int, str], ...]:
240-
"""
241-
Mapping between index and their corresponding variable name.
242-
Returns a tuple. If the model is a multiencoder-decoder model
243-
the tuple will contain two dicts, one for each decoder. If not
244-
the tuple will contain a single dict.
245-
"""
246-
_data_indices = self._model_instance.data_indices
247-
if isinstance(_data_indices, (tuple, list)) and len(_data_indices) >= 2:
248-
return tuple(
249-
{
250-
index: var
251-
for (var, index) in self.name_to_index[decoder_index].items()
252-
}
253-
for decoder_index in range(len(self.name_to_index))
254-
)
255-
return ({index: name for name, index in _data_indices.name_to_index.items()},)
256-
257-
def _make_indices_mapping(self, indices_from, indices_to):
258-
"""
259-
Creates a mapping for a given model and data output
260-
or model input and data input, and vice versa.
261-
262-
args:
263-
indices_from (dict)
264-
indices_to (dict)
265-
return
266-
a mapping between indices_from and indices_to
267-
"""
268-
assert len(indices_from) == len(indices_to)
269-
return dict(zip(indices_from, indices_to))
270-
271-
@property
272-
def model_output_index_to_name(self) -> tuple[dict[int, str], ...]:
273-
"""
274-
A mapping from model output to data output. This
275-
dict returns index and name pairs according to model.output.full to
276-
data.output.full
277-
278-
args:
279-
None
280-
return:
281-
tuple of dicts where the tuple index represents decoder index. Each
282-
tuple dicts contains a dict with a mapping between model.output.full and
283-
data.output.full
284-
Example:
285-
-> if the parameter skt has index 27 in name_to_index or data.output.full it will
286-
have index 17 in model.output.full. This dict will yield that index 17 is skt
287-
"""
288-
if (
289-
isinstance(self._metadata.data_indices, (tuple, list))
290-
and len(self._metadata.data_indices) >= 2
291-
):
292-
mapping = {
293-
grid_index: self._make_indices_mapping(
294-
self._metadata.data_indices[grid_index].model.output.full,
295-
self._metadata.data_indices[grid_index].data.output.full,
296-
)
297-
for grid_index in range(len(self._metadata.data_indices))
298-
}
299-
return tuple(
300-
{name: self.index_to_name[k][index] for (name, index) in v.items()}
301-
for (k, v) in mapping.items()
302-
)
303-
304-
mapping = self._make_indices_mapping(
305-
self._metadata.data_indices.model.output.full,
306-
self._metadata.data_indices.data.output.full,
307-
)
308-
return ({k: self._metadata.dataset.variables[v] for k, v in mapping.items()},)
309-
310-
@property
311-
def model_output_name_to_index(self) -> tuple[dict[str, int], ...]:
312-
"""
313-
A mapping from model output to data output. This
314-
dict returns name and index pairs according to model.output.full to
315-
data.output.full
316-
317-
args:
318-
None
319-
return:
320-
Identical tuple[dict] from model_output_index_to_name but key
321-
value pairs are switched.
322-
"""
323-
if (
324-
isinstance(self._metadata.data_indices, (tuple, list))
325-
and len(self._metadata.data_indices) >= 2
326-
):
327-
return tuple(
328-
{name: index for (index, name) in v.items()}
329-
for (k, v) in enumerate(self.model_output_index_to_name)
330-
)
331-
332-
return (
333-
{name: index for index, name in self.model_output_index_to_name[0].items()},
334-
)
335-
336263
@cached_property
337-
def data_indices(self) -> tuple[IndexCollection, ...]:
338-
"""
339-
Wrapper for model.data_indices. Returns a tuple of dict and None or two dicts.
340-
"""
341-
342-
# If Multiencdec checkpoint
343-
if isinstance(self._model_instance.data_indices, (tuple, list)):
344-
return tuple(self._model_instance.data_indices)
345-
346-
# If simple checkpoint
347-
return (self._model_instance.data_indices,)
264+
def data_indices(self) -> dict[str, IndexCollection]:
265+
_data_indices = self._model_instance.data_indices
266+
if isinstance(_data_indices, IndexCollection): # Backwards compatibility
267+
return {"data": _data_indices}
268+
else:
269+
return _data_indices

0 commit comments

Comments
 (0)