|
2 | 2 | import os |
3 | 3 | from copy import deepcopy |
4 | 4 | from functools import cached_property |
5 | | -from typing import Optional |
6 | 5 |
|
7 | 6 | import torch |
8 | 7 | from anemoi.utils.checkpoints import load_metadata |
@@ -48,7 +47,7 @@ class Metadata(DotDict): |
48 | 47 | class Checkpoint: |
49 | 48 | """This class makes accessible various information stored in Anemoi checkpoints.""" |
50 | 49 |
|
51 | | - def __init__(self, path: str, graph: Optional[str] = None): |
| 50 | + def __init__(self, path: str, graph: str | None = None): |
52 | 51 | assert os.path.exists(path), f"The given checkpoint {path} does not exist!" |
53 | 52 |
|
54 | 53 | self.path = path |
@@ -133,8 +132,50 @@ def _load_model(self) -> torch.nn.Module: |
133 | 132 | "check module versions." |
134 | 133 | ) from e |
135 | 134 | raise e |
| 135 | + if not torch.cuda.is_available(): |
| 136 | + self._apply_triton_cpu_fallback(inst) |
136 | 137 | return inst |
137 | 138 |
|
| 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 | + |
138 | 179 | @property |
139 | 180 | def graph(self) -> HeteroData: |
140 | 181 | """ |
@@ -180,7 +221,7 @@ def graph(self) -> HeteroData: |
180 | 221 | # _model_params = self._model_instance.named_parameters() |
181 | 222 | # return deepcopy(dict(_model_params)) |
182 | 223 |
|
183 | | - def update_graph(self, path: Optional[str] = None) -> HeteroData: |
| 224 | + def update_graph(self, path: str | None = None) -> HeteroData: |
184 | 225 | """ |
185 | 226 | Replaces existing graph object within model instance. |
186 | 227 | The new graph is either provided as an torch file or |
@@ -219,129 +260,10 @@ def update_graph(self, path: Optional[str] = None) -> HeteroData: |
219 | 260 | self._model_instance.load_state_dict(new_state_dict) |
220 | 261 | return self._model_instance.graph_data |
221 | 262 |
|
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 | | - |
336 | 263 | @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