Upload folder using huggingface_hub
Browse files- README.md +185 -0
- conf/config.yaml +186 -0
- configuration.json +1 -0
- model/graph_cast_net.py +805 -0
- model/graph_cast_processor.py +238 -0
- scripts/compute_time_diff_std.py +48 -0
- scripts/fake_data.py +95 -0
- scripts/finetune.py +331 -0
- scripts/get_data_json.py +24 -0
- scripts/inference.py +122 -0
- scripts/result.py +196 -0
- scripts/train.py +276 -0
- weight/.gitkeep +0 -0
README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
frameworks: PyTorch
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
license: apache-2.0
|
| 7 |
+
tags:
|
| 8 |
+
- OneScience
|
| 9 |
+
- Earth Science
|
| 10 |
+
- Weather Forecast
|
| 11 |
+
- Short-to-Medium-Range Weather Forecast
|
| 12 |
+
- ERA5
|
| 13 |
+
tasks: []
|
| 14 |
+
datasets:
|
| 15 |
+
- OneScience/ERA5
|
| 16 |
+
---
|
| 17 |
+
<p align="center">
|
| 18 |
+
<strong>
|
| 19 |
+
<span style="font-size: 30px;">GraphCast</span>
|
| 20 |
+
</strong>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
+
# Model Introduction
|
| 24 |
+
|
| 25 |
+
GraphCast is a global medium-range weather forecast model developed by the Google DeepMind team, with its core paper published in the top-tier international journal *Science*.
|
| 26 |
+
|
| 27 |
+
Paper: GraphCast: Learning skillful medium-range global weather forecasting
|
| 28 |
+
|
| 29 |
+
https://arxiv.org/abs/2212.12794
|
| 30 |
+
|
| 31 |
+
# Model Description
|
| 32 |
+
|
| 33 |
+
GraphCast is a global medium-range weather forecast model built on a Graph Neural Network (GNN). It is trained on the ERA5 global atmospheric reanalysis dataset (1979–2017) provided by ECMWF.
|
| 34 |
+
|
| 35 |
+
# Use Cases
|
| 36 |
+
|
| 37 |
+
| Scenario | Description |
|
| 38 |
+
| :---: | :--- |
|
| 39 |
+
| Global Weather Forecast Research | Train a GraphCast-style GNN forecast model using annual ERA5 HDF5 data. |
|
| 40 |
+
| Local Quick Validation | Use synthetic data to verify data loading, auxiliary file generation, training entry points, and result scripts. |
|
| 41 |
+
| ModelScope / OneCode Execution | Download as a standalone model package, install dependencies, and run scripts directly. |
|
| 42 |
+
| Multi-GPU Training | Launch multi-process training via `torchrun`. |
|
| 43 |
+
|
| 44 |
+
# Usage Guide
|
| 45 |
+
|
| 46 |
+
## 1. OneCode Usage
|
| 47 |
+
|
| 48 |
+
Experience intelligent one-click AI4S programming through the OneCode online environment:
|
| 49 |
+
|
| 50 |
+
[Click to Experience Intelligent One-Click AI4S Programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 51 |
+
|
| 52 |
+
## 2. Manual Installation and Usage
|
| 53 |
+
|
| 54 |
+
**Hardware Requirements**
|
| 55 |
+
|
| 56 |
+
- A GPU or DCU is recommended.
|
| 57 |
+
- CPU can be used for import and small-scale connectivity verification; full training and inference will be slow.
|
| 58 |
+
- DCU users must install DTK in advance. DTK 25.04.2 or above, or the OneScience recommended version matching your cluster, is recommended.
|
| 59 |
+
|
| 60 |
+
### Download the Model Package
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
modelscope download --model OneScience/GraphCast --local_dir ./GraphCast
|
| 64 |
+
cd GraphCast
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### Install the Runtime Environment
|
| 68 |
+
|
| 69 |
+
**DCU Environment**
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
# Please activate DTK and CONDA first
|
| 73 |
+
conda create -n onescience311 python=3.11 -y
|
| 74 |
+
conda activate onescience311
|
| 75 |
+
# uv installation is supported
|
| 76 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
**GPU Environment**
|
| 80 |
+
```bash
|
| 81 |
+
# Please activate CONDA first
|
| 82 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 83 |
+
conda activate onescience311
|
| 84 |
+
# uv installation is supported
|
| 85 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
### Training Data Introduction
|
| 89 |
+
|
| 90 |
+
The OneScience community provides ERA5 data for training (due to file size limits, the current repository contains a slice of the full dataset). Users can download it with the command below and confirm that the data path in `conf/config.yaml` is set correctly:
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
modelscope download --dataset OneScience/ERA5 --local_dir ./data
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Generate Auxiliary Files
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
python scripts/get_data_json.py
|
| 100 |
+
python scripts/compute_time_diff_std.py
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
Generated files:
|
| 104 |
+
|
| 105 |
+
- `data.json`
|
| 106 |
+
- `time_diff_std.npy`
|
| 107 |
+
|
| 108 |
+
### Training
|
| 109 |
+
|
| 110 |
+
Single GPU:
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
python scripts/train.py
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
Multi-GPU:
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train.py
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
Training outputs:
|
| 123 |
+
|
| 124 |
+
```text
|
| 125 |
+
data/checkpoints/model_bak.pth
|
| 126 |
+
data/checkpoints/trloss.npy
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
### Training Weights
|
| 130 |
+
|
| 131 |
+
This repository provides weights trained on ERA5 data from 1979 to 2017 in the `weights/` folder. The weight files will be uploaded soon and are expected to be available in the near future.
|
| 132 |
+
|
| 133 |
+
### Fine-tuning
|
| 134 |
+
|
| 135 |
+
Before fine-tuning, you must first complete training and generate `data/checkpoints/model_bak.pth`.
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
python scripts/finetune.py
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Fine-tuning outputs:
|
| 142 |
+
|
| 143 |
+
```text
|
| 144 |
+
data/checkpoints/model_finetune_bak.pth
|
| 145 |
+
data/checkpoints/ft_trloss.npy
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### Inference
|
| 149 |
+
|
| 150 |
+
Inference reads `data/checkpoints/model_finetune_bak.pth` by default:
|
| 151 |
+
|
| 152 |
+
```bash
|
| 153 |
+
python scripts/inference.py
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
Prediction results are output to:
|
| 157 |
+
|
| 158 |
+
```text
|
| 159 |
+
result/output/
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
### Evaluation and Visualization
|
| 163 |
+
|
| 164 |
+
```bash
|
| 165 |
+
python scripts/result.py
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
Output contents include:
|
| 169 |
+
|
| 170 |
+
- `result/rmse.npy`
|
| 171 |
+
- `result/acc.npy`
|
| 172 |
+
- `result/loss.png`
|
| 173 |
+
- Forecast comparison plots for specified dates and variables
|
| 174 |
+
|
| 175 |
+
# OneScience Official Information
|
| 176 |
+
|
| 177 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 178 |
+
| --- | --- | --- |
|
| 179 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 180 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 181 |
+
|
| 182 |
+
# Citation & License
|
| 183 |
+
|
| 184 |
+
- Apache License 2.0. The code is open source, permitting both commercial and non-commercial use.
|
| 185 |
+
- The weights are permitted for non-commercial use only.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
processor_layers: 16
|
| 3 |
+
hidden_dim: 512
|
| 4 |
+
mesh_level: 6
|
| 5 |
+
multimesh: true
|
| 6 |
+
num_channels_climate: 227
|
| 7 |
+
use_cos_zenith: true
|
| 8 |
+
use_time_of_year_index: true
|
| 9 |
+
num_history: 0
|
| 10 |
+
num_channels_static: 5
|
| 11 |
+
num_steps: 1
|
| 12 |
+
checkpoint_encoder: true
|
| 13 |
+
checkpoint_decoder: false
|
| 14 |
+
full_bf16: true
|
| 15 |
+
processor_type: MessagePassing
|
| 16 |
+
khop_neighbors: 32
|
| 17 |
+
num_attention_heads: 4
|
| 18 |
+
norm_type: LayerNorm
|
| 19 |
+
segments: 1
|
| 20 |
+
force_single_checkpoint: false
|
| 21 |
+
checkpoint_processor: false
|
| 22 |
+
force_single_checkpoint_finetune: false
|
| 23 |
+
checkpoint_encoder_finetune: true
|
| 24 |
+
checkpoint_processor_finetune: true
|
| 25 |
+
checkpoint_decoder_finetune: true
|
| 26 |
+
concat_trick: true
|
| 27 |
+
cugraphops_encoder: false
|
| 28 |
+
cugraphops_processor: false
|
| 29 |
+
cugraphops_decoder: false
|
| 30 |
+
recompute_activation: false
|
| 31 |
+
use_apex: true
|
| 32 |
+
patience: 50
|
| 33 |
+
start_epoch: 0
|
| 34 |
+
max_epoch: 9999
|
| 35 |
+
num_val_steps: 8
|
| 36 |
+
dt: 6.0
|
| 37 |
+
grad_clip_norm: 32.0
|
| 38 |
+
lr: 1e-3
|
| 39 |
+
lr_step3: 3e-7
|
| 40 |
+
num_iters_step1: 1000
|
| 41 |
+
num_iters_step2: 299000
|
| 42 |
+
num_iters_step3: 11000
|
| 43 |
+
step_change_freq: 1000
|
| 44 |
+
val_freq: 5
|
| 45 |
+
checkpoint_dir : "./data/checkpoints"
|
| 46 |
+
|
| 47 |
+
# 整个数据读取流程
|
| 48 |
+
datapipe:
|
| 49 |
+
name: "ERA5"
|
| 50 |
+
task: "weather_forecasting"
|
| 51 |
+
|
| 52 |
+
# dataset设定
|
| 53 |
+
dataset:
|
| 54 |
+
type: "hdf5"
|
| 55 |
+
data_dir: './data/' # "$ONESCIENCE_DATASETS_DIR/ERA5/newh5/"
|
| 56 |
+
dataset_metadata_path: './data.json' # Path to the dataset metadata, containing channel names.
|
| 57 |
+
time_diff_std_path: './time_diff_std.npy' # Path to the .npy file with standard deviation of normalized per-variable per-pressure level time differences.
|
| 58 |
+
train_time: [1951, 1952]
|
| 59 |
+
val_time: [1953]
|
| 60 |
+
test_time: [1954]
|
| 61 |
+
img_size: [721, 1440]
|
| 62 |
+
verbose: true
|
| 63 |
+
cache: false
|
| 64 |
+
# vars
|
| 65 |
+
channels: ['10m_u_component_of_wind', '10m_v_component_of_wind', '2m_temperature', 'mean_sea_level_pressure', 'total_precipitation',
|
| 66 |
+
|
| 67 |
+
'geopotential_1', 'geopotential_2', 'geopotential_3', 'geopotential_5', 'geopotential_7',
|
| 68 |
+
'geopotential_10', 'geopotential_20', 'geopotential_30', 'geopotential_50', 'geopotential_70',
|
| 69 |
+
'geopotential_100', 'geopotential_125', 'geopotential_150', 'geopotential_175', 'geopotential_200',
|
| 70 |
+
'geopotential_225', 'geopotential_250', 'geopotential_300', 'geopotential_350', 'geopotential_400',
|
| 71 |
+
'geopotential_450', 'geopotential_500', 'geopotential_550', 'geopotential_600', 'geopotential_650',
|
| 72 |
+
'geopotential_700', 'geopotential_750', 'geopotential_775', 'geopotential_800', 'geopotential_825',
|
| 73 |
+
'geopotential_850', 'geopotential_875', 'geopotential_900', 'geopotential_925', 'geopotential_950',
|
| 74 |
+
'geopotential_975', 'geopotential_1000',
|
| 75 |
+
|
| 76 |
+
'relative_humidity_1', 'relative_humidity_2', 'relative_humidity_3', 'relative_humidity_5', 'relative_humidity_7',
|
| 77 |
+
'relative_humidity_10', 'relative_humidity_20', 'relative_humidity_30', 'relative_humidity_50', 'relative_humidity_70',
|
| 78 |
+
'relative_humidity_100', 'relative_humidity_125', 'relative_humidity_150', 'relative_humidity_175', 'relative_humidity_200',
|
| 79 |
+
'relative_humidity_225', 'relative_humidity_250', 'relative_humidity_300', 'relative_humidity_350', 'relative_humidity_400',
|
| 80 |
+
'relative_humidity_450', 'relative_humidity_500', 'relative_humidity_550', 'relative_humidity_600', 'relative_humidity_650',
|
| 81 |
+
'relative_humidity_700', 'relative_humidity_750', 'relative_humidity_775', 'relative_humidity_800', 'relative_humidity_825',
|
| 82 |
+
'relative_humidity_850', 'relative_humidity_875', 'relative_humidity_900', 'relative_humidity_925', 'relative_humidity_950',
|
| 83 |
+
'relative_humidity_975', 'relative_humidity_1000',
|
| 84 |
+
|
| 85 |
+
'u_component_of_wind_1', 'u_component_of_wind_2', 'u_component_of_wind_3', 'u_component_of_wind_5', 'u_component_of_wind_7',
|
| 86 |
+
'u_component_of_wind_10', 'u_component_of_wind_20', 'u_component_of_wind_30', 'u_component_of_wind_50', 'u_component_of_wind_70',
|
| 87 |
+
'u_component_of_wind_100', 'u_component_of_wind_125', 'u_component_of_wind_150', 'u_component_of_wind_175', 'u_component_of_wind_200',
|
| 88 |
+
'u_component_of_wind_225', 'u_component_of_wind_250', 'u_component_of_wind_300', 'u_component_of_wind_350', 'u_component_of_wind_400',
|
| 89 |
+
'u_component_of_wind_450', 'u_component_of_wind_500', 'u_component_of_wind_550', 'u_component_of_wind_600', 'u_component_of_wind_650',
|
| 90 |
+
'u_component_of_wind_700', 'u_component_of_wind_750', 'u_component_of_wind_775', 'u_component_of_wind_800', 'u_component_of_wind_825',
|
| 91 |
+
'u_component_of_wind_850', 'u_component_of_wind_875', 'u_component_of_wind_900', 'u_component_of_wind_925', 'u_component_of_wind_950',
|
| 92 |
+
'u_component_of_wind_975', 'u_component_of_wind_1000',
|
| 93 |
+
|
| 94 |
+
'v_component_of_wind_1', 'v_component_of_wind_2', 'v_component_of_wind_3', 'v_component_of_wind_5', 'v_component_of_wind_7',
|
| 95 |
+
'v_component_of_wind_10', 'v_component_of_wind_20', 'v_component_of_wind_30', 'v_component_of_wind_50', 'v_component_of_wind_70',
|
| 96 |
+
'v_component_of_wind_100', 'v_component_of_wind_125', 'v_component_of_wind_150', 'v_component_of_wind_175', 'v_component_of_wind_200',
|
| 97 |
+
'v_component_of_wind_225', 'v_component_of_wind_250', 'v_component_of_wind_300', 'v_component_of_wind_350', 'v_component_of_wind_400',
|
| 98 |
+
'v_component_of_wind_450', 'v_component_of_wind_500', 'v_component_of_wind_550', 'v_component_of_wind_600', 'v_component_of_wind_650',
|
| 99 |
+
'v_component_of_wind_700', 'v_component_of_wind_750', 'v_component_of_wind_775', 'v_component_of_wind_800', 'v_component_of_wind_825',
|
| 100 |
+
'v_component_of_wind_850', 'v_component_of_wind_875', 'v_component_of_wind_900', 'v_component_of_wind_925', 'v_component_of_wind_950',
|
| 101 |
+
'v_component_of_wind_975', 'v_component_of_wind_1000',
|
| 102 |
+
|
| 103 |
+
'temperature_1', 'temperature_2', 'temperature_3', 'temperature_5', 'temperature_7',
|
| 104 |
+
'temperature_10', 'temperature_20', 'temperature_30', 'temperature_50', 'temperature_70',
|
| 105 |
+
'temperature_100', 'temperature_125', 'temperature_150', 'temperature_175', 'temperature_200',
|
| 106 |
+
'temperature_225', 'temperature_250', 'temperature_300', 'temperature_350', 'temperature_400',
|
| 107 |
+
'temperature_450', 'temperature_500', 'temperature_550', 'temperature_600', 'temperature_650',
|
| 108 |
+
'temperature_700', 'temperature_750', 'temperature_775', 'temperature_800', 'temperature_825',
|
| 109 |
+
'temperature_850', 'temperature_875', 'temperature_900', 'temperature_925', 'temperature_950',
|
| 110 |
+
'temperature_975', 'temperature_1000',
|
| 111 |
+
|
| 112 |
+
'vertical_wind_speed_1', 'vertical_wind_speed_2', 'vertical_wind_speed_3', 'vertical_wind_speed_5', 'vertical_wind_speed_7',
|
| 113 |
+
'vertical_wind_speed_10', 'vertical_wind_speed_20', 'vertical_wind_speed_30', 'vertical_wind_speed_50', 'vertical_wind_speed_70',
|
| 114 |
+
'vertical_wind_speed_100', 'vertical_wind_speed_125', 'vertical_wind_speed_150', 'vertical_wind_speed_175', 'vertical_wind_speed_200',
|
| 115 |
+
'vertical_wind_speed_225', 'vertical_wind_speed_250', 'vertical_wind_speed_300', 'vertical_wind_speed_350', 'vertical_wind_speed_400',
|
| 116 |
+
'vertical_wind_speed_450', 'vertical_wind_speed_500', 'vertical_wind_speed_550', 'vertical_wind_speed_600', 'vertical_wind_speed_650',
|
| 117 |
+
'vertical_wind_speed_700', 'vertical_wind_speed_750', 'vertical_wind_speed_775', 'vertical_wind_speed_800', 'vertical_wind_speed_825',
|
| 118 |
+
'vertical_wind_speed_850', 'vertical_wind_speed_875', 'vertical_wind_speed_900', 'vertical_wind_speed_925', 'vertical_wind_speed_950',
|
| 119 |
+
'vertical_wind_speed_975', 'vertical_wind_speed_1000',
|
| 120 |
+
]
|
| 121 |
+
|
| 122 |
+
variables:
|
| 123 |
+
- "u10" # 10m U wind component
|
| 124 |
+
- "v10" # 10m V wind component
|
| 125 |
+
- "t2m" # 2m temperature
|
| 126 |
+
- "msl" # Mean sea level pressure
|
| 127 |
+
- "z500" # Geopotential at 500 hPa
|
| 128 |
+
- "t850" # Temperature at 850 hPa
|
| 129 |
+
|
| 130 |
+
# 时间配置
|
| 131 |
+
time_range: ["2000-01-01", "2020-12-31"]
|
| 132 |
+
time_steps: 1
|
| 133 |
+
time_res: 6
|
| 134 |
+
|
| 135 |
+
# 空间配置
|
| 136 |
+
spatial_resolution: [0.25, 0.25]
|
| 137 |
+
|
| 138 |
+
# 采样配置
|
| 139 |
+
num_samples: -1 # -1 表示使用全部数据
|
| 140 |
+
shuffle: true
|
| 141 |
+
random_seed: 42
|
| 142 |
+
|
| 143 |
+
# 领域特定配置
|
| 144 |
+
extra:
|
| 145 |
+
levels: [500, 850, 1000]
|
| 146 |
+
lat_range: [-90, 90]
|
| 147 |
+
lon_range: [0, 360]
|
| 148 |
+
|
| 149 |
+
# 数据转换配置
|
| 150 |
+
transforms:
|
| 151 |
+
- type: "Normalize"
|
| 152 |
+
params:
|
| 153 |
+
mean: [0.0, 0.0, 288.0, 101325.0, 50000.0, 270.0]
|
| 154 |
+
std: [5.0, 5.0, 15.0, 1000.0, 5000.0, 10.0]
|
| 155 |
+
keys: ["input", "target"]
|
| 156 |
+
|
| 157 |
+
- type: "ToTensor"
|
| 158 |
+
params:
|
| 159 |
+
keys: null # null表示转换所有numpy数组
|
| 160 |
+
|
| 161 |
+
# 其他配置
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# DataLoader配置
|
| 165 |
+
dataloader:
|
| 166 |
+
mask_dtype: "float32"
|
| 167 |
+
batch_size: 1
|
| 168 |
+
num_workers: 1
|
| 169 |
+
pin_memory: true
|
| 170 |
+
drop_last: true
|
| 171 |
+
shuffle: false # 使用sampler时设为false
|
| 172 |
+
prefetch_factor: 2
|
| 173 |
+
persistent_workers: true
|
| 174 |
+
|
| 175 |
+
# 分布式配置
|
| 176 |
+
distributed:
|
| 177 |
+
enabled: true
|
| 178 |
+
sampler: "DistributedSampler"
|
| 179 |
+
rank: 0
|
| 180 |
+
world_size: 4
|
| 181 |
+
shuffle: true
|
| 182 |
+
seed: 42
|
| 183 |
+
drop_last: true
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/graph_cast_net.py
ADDED
|
@@ -0,0 +1,805 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import warnings
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import Any, Optional
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from torch import Tensor
|
| 8 |
+
from torch import nn
|
| 9 |
+
try:
|
| 10 |
+
from typing import Self
|
| 11 |
+
except ImportError:
|
| 12 |
+
# for Python versions < 3.11
|
| 13 |
+
from typing_extensions import Self
|
| 14 |
+
|
| 15 |
+
from onescience.modules.decoder.mesh_graph_decoder import MeshGraphDecoder
|
| 16 |
+
from onescience.modules.embedding.graphcast_embedder import (
|
| 17 |
+
GraphCastDecoderEmbedder,
|
| 18 |
+
GraphCastEncoderEmbedder,
|
| 19 |
+
)
|
| 20 |
+
from onescience.modules.encoder.mesh_graph_encoder import MeshGraphEncoder
|
| 21 |
+
from onescience.modules.mlp.mesh_graph_mlp import MeshGraphMLP
|
| 22 |
+
from onescience.modules.utils.gnnlayer_utils import CuGraphCSC, set_checkpoint_fn
|
| 23 |
+
from onescience.modules.layer.activations import get_activation
|
| 24 |
+
from onescience.modules.utils.graphcast.graph import Graph
|
| 25 |
+
|
| 26 |
+
from onescience.models.meta import ModelMetaData
|
| 27 |
+
|
| 28 |
+
from .graph_cast_processor import (
|
| 29 |
+
GraphCastProcessor,
|
| 30 |
+
GraphCastProcessorGraphTransformer,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
logger = logging.getLogger(__name__)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def get_lat_lon_partition_separators(partition_size: int):
|
| 37 |
+
"""Utility Function to get separation intervals for lat-lon
|
| 38 |
+
grid for partition_sizes of interest.
|
| 39 |
+
|
| 40 |
+
Parameters
|
| 41 |
+
----------
|
| 42 |
+
partition_size : int
|
| 43 |
+
size of graph partition
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def _divide(num_lat_chunks: int, num_lon_chunks: int):
|
| 47 |
+
# divide lat-lon grid into equally-sizes chunks along both latitude and longitude
|
| 48 |
+
if (num_lon_chunks * num_lat_chunks) != partition_size:
|
| 49 |
+
raise ValueError(
|
| 50 |
+
"Can't divide lat-lon grid into grid {num_lat_chunks} x {num_lon_chunks} chunks for partition_size={partition_size}."
|
| 51 |
+
)
|
| 52 |
+
# divide latitutude into num_lat_chunks of size 180 / num_lat_chunks
|
| 53 |
+
# divide longitude into chunks of size 360 / (partition_size / num_lat_chunks)
|
| 54 |
+
lat_bin_width = 180.0 / num_lat_chunks
|
| 55 |
+
lon_bin_width = 360.0 / num_lon_chunks
|
| 56 |
+
|
| 57 |
+
lat_ranges = []
|
| 58 |
+
lon_ranges = []
|
| 59 |
+
|
| 60 |
+
for p_lat in range(num_lat_chunks):
|
| 61 |
+
for p_lon in range(num_lon_chunks):
|
| 62 |
+
lat_ranges += [
|
| 63 |
+
(lat_bin_width * p_lat - 90.0, lat_bin_width * (p_lat + 1) - 90.0)
|
| 64 |
+
]
|
| 65 |
+
lon_ranges += [
|
| 66 |
+
(lon_bin_width * p_lon - 180.0, lon_bin_width * (p_lon + 1) - 180.0)
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
lat_ranges[-1] = (lat_ranges[-1][0], None)
|
| 70 |
+
lon_ranges[-1] = (lon_ranges[-1][0], None)
|
| 71 |
+
|
| 72 |
+
return lat_ranges, lon_ranges
|
| 73 |
+
|
| 74 |
+
# use two closest factors of partition_size
|
| 75 |
+
lat_chunks, lon_chunks, i = 1, partition_size, 0
|
| 76 |
+
while lat_chunks < lon_chunks:
|
| 77 |
+
i += 1
|
| 78 |
+
if partition_size % i == 0:
|
| 79 |
+
lat_chunks = i
|
| 80 |
+
lon_chunks = partition_size // lat_chunks
|
| 81 |
+
|
| 82 |
+
lat_ranges, lon_ranges = _divide(lat_chunks, lon_chunks)
|
| 83 |
+
|
| 84 |
+
# mainly for debugging
|
| 85 |
+
if (lat_ranges is None) or (lon_ranges is None):
|
| 86 |
+
raise ValueError("unexpected error, abort")
|
| 87 |
+
|
| 88 |
+
min_seps = []
|
| 89 |
+
max_seps = []
|
| 90 |
+
|
| 91 |
+
for i in range(partition_size):
|
| 92 |
+
lat = lat_ranges[i]
|
| 93 |
+
lon = lon_ranges[i]
|
| 94 |
+
min_seps.append([lat[0], lon[0]])
|
| 95 |
+
max_seps.append([lat[1], lon[1]])
|
| 96 |
+
|
| 97 |
+
return min_seps, max_seps
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@dataclass
|
| 101 |
+
class MetaData(ModelMetaData):
|
| 102 |
+
name: str = "GraphCastNet"
|
| 103 |
+
# Optimization
|
| 104 |
+
jit: bool = False
|
| 105 |
+
cuda_graphs: bool = False
|
| 106 |
+
amp_cpu: bool = False
|
| 107 |
+
amp_gpu: bool = True
|
| 108 |
+
torch_fx: bool = False
|
| 109 |
+
# Data type
|
| 110 |
+
bf16: bool = True
|
| 111 |
+
# Inference
|
| 112 |
+
onnx: bool = False
|
| 113 |
+
# Physics informed
|
| 114 |
+
func_torch: bool = False
|
| 115 |
+
auto_grad: bool = False
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class GraphCastNet(nn.Module):
|
| 119 |
+
def __init__(
|
| 120 |
+
self,
|
| 121 |
+
mesh_level: Optional[int] = 6,
|
| 122 |
+
multimesh_level: Optional[int] = None,
|
| 123 |
+
multimesh: bool = True,
|
| 124 |
+
input_res: tuple = (721, 1440),
|
| 125 |
+
input_dim_grid_nodes: int = 237,
|
| 126 |
+
input_dim_mesh_nodes: int = 3,
|
| 127 |
+
input_dim_edges: int = 4,
|
| 128 |
+
output_dim_grid_nodes: int = 227,
|
| 129 |
+
processor_type: str = "MessagePassing",
|
| 130 |
+
khop_neighbors: int = 32,
|
| 131 |
+
num_attention_heads: int = 4,
|
| 132 |
+
processor_layers: int = 16,
|
| 133 |
+
hidden_layers: int = 1,
|
| 134 |
+
hidden_dim: int = 512,
|
| 135 |
+
aggregation: str = "sum",
|
| 136 |
+
activation_fn: str = "silu",
|
| 137 |
+
norm_type: str = "LayerNorm",
|
| 138 |
+
use_cugraphops_encoder: bool = False,
|
| 139 |
+
use_cugraphops_processor: bool = False,
|
| 140 |
+
use_cugraphops_decoder: bool = False,
|
| 141 |
+
do_concat_trick: bool = False,
|
| 142 |
+
recompute_activation: bool = False,
|
| 143 |
+
partition_size: int = 1,
|
| 144 |
+
partition_group_name: Optional[str] = None,
|
| 145 |
+
use_lat_lon_partitioning: bool = False,
|
| 146 |
+
expect_partitioned_input: bool = False,
|
| 147 |
+
global_features_on_rank_0: bool = False,
|
| 148 |
+
produce_aggregated_output: bool = True,
|
| 149 |
+
produce_aggregated_output_on_all_ranks: bool = True,
|
| 150 |
+
):
|
| 151 |
+
super().__init__()
|
| 152 |
+
|
| 153 |
+
# 'multimesh_level' deprecation handling
|
| 154 |
+
if multimesh_level is not None:
|
| 155 |
+
warnings.warn(
|
| 156 |
+
"'multimesh_level' is deprecated and will be removed in a future version. Use 'mesh_level' instead.",
|
| 157 |
+
DeprecationWarning,
|
| 158 |
+
stacklevel=2,
|
| 159 |
+
)
|
| 160 |
+
mesh_level = multimesh_level
|
| 161 |
+
|
| 162 |
+
self.processor_type = processor_type
|
| 163 |
+
if self.processor_type == "MessagePassing":
|
| 164 |
+
khop_neighbors = 0
|
| 165 |
+
self.is_distributed = False
|
| 166 |
+
if partition_size > 1:
|
| 167 |
+
self.is_distributed = True
|
| 168 |
+
self.expect_partitioned_input = expect_partitioned_input
|
| 169 |
+
self.global_features_on_rank_0 = global_features_on_rank_0
|
| 170 |
+
self.produce_aggregated_output = produce_aggregated_output
|
| 171 |
+
self.produce_aggregated_output_on_all_ranks = (
|
| 172 |
+
produce_aggregated_output_on_all_ranks
|
| 173 |
+
)
|
| 174 |
+
self.partition_group_name = partition_group_name
|
| 175 |
+
|
| 176 |
+
# create the lat_lon_grid
|
| 177 |
+
self.latitudes = torch.linspace(-90, 90, steps=input_res[0])
|
| 178 |
+
self.longitudes = torch.linspace(-180, 180, steps=input_res[1] + 1)[1:]
|
| 179 |
+
self.lat_lon_grid = torch.stack(
|
| 180 |
+
torch.meshgrid(self.latitudes, self.longitudes, indexing="ij"), dim=-1
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
# Set activation function
|
| 184 |
+
activation_fn = get_activation(activation_fn)
|
| 185 |
+
|
| 186 |
+
# construct the graph
|
| 187 |
+
self.graph = Graph(self.lat_lon_grid, mesh_level, multimesh, khop_neighbors)
|
| 188 |
+
|
| 189 |
+
self.mesh_graph, self.attn_mask = self.graph.create_mesh_graph(verbose=False)
|
| 190 |
+
self.g2m_graph = self.graph.create_g2m_graph(verbose=False)
|
| 191 |
+
self.m2g_graph = self.graph.create_m2g_graph(verbose=False)
|
| 192 |
+
|
| 193 |
+
self.g2m_edata = self.g2m_graph.edata["x"]
|
| 194 |
+
self.m2g_edata = self.m2g_graph.edata["x"]
|
| 195 |
+
self.mesh_ndata = self.mesh_graph.ndata["x"]
|
| 196 |
+
if self.processor_type == "MessagePassing":
|
| 197 |
+
self.mesh_edata = self.mesh_graph.edata["x"]
|
| 198 |
+
elif self.processor_type == "GraphTransformer":
|
| 199 |
+
# Dummy tensor to avoid breaking the API
|
| 200 |
+
self.mesh_edata = torch.zeros((1, input_dim_edges))
|
| 201 |
+
else:
|
| 202 |
+
raise ValueError(f"Invalid processor type {processor_type}")
|
| 203 |
+
|
| 204 |
+
if use_cugraphops_encoder or self.is_distributed:
|
| 205 |
+
kwargs = {}
|
| 206 |
+
if use_lat_lon_partitioning:
|
| 207 |
+
min_seps, max_seps = get_lat_lon_partition_separators(partition_size)
|
| 208 |
+
kwargs = {
|
| 209 |
+
"src_coordinates": self.g2m_graph.srcdata["lat_lon"],
|
| 210 |
+
"dst_coordinates": self.g2m_graph.dstdata["lat_lon"],
|
| 211 |
+
"coordinate_separators_min": min_seps,
|
| 212 |
+
"coordinate_separators_max": max_seps,
|
| 213 |
+
}
|
| 214 |
+
self.g2m_graph, edge_perm = CuGraphCSC.from_dgl(
|
| 215 |
+
graph=self.g2m_graph,
|
| 216 |
+
partition_size=partition_size,
|
| 217 |
+
partition_group_name=partition_group_name,
|
| 218 |
+
partition_by_bbox=use_lat_lon_partitioning,
|
| 219 |
+
**kwargs,
|
| 220 |
+
)
|
| 221 |
+
self.g2m_edata = self.g2m_edata[edge_perm]
|
| 222 |
+
|
| 223 |
+
if self.is_distributed:
|
| 224 |
+
self.g2m_edata = self.g2m_graph.get_edge_features_in_partition(
|
| 225 |
+
self.g2m_edata
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
if use_cugraphops_decoder or self.is_distributed:
|
| 229 |
+
kwargs = {}
|
| 230 |
+
if use_lat_lon_partitioning:
|
| 231 |
+
min_seps, max_seps = get_lat_lon_partition_separators(partition_size)
|
| 232 |
+
kwargs = {
|
| 233 |
+
"src_coordinates": self.m2g_graph.srcdata["lat_lon"],
|
| 234 |
+
"dst_coordinates": self.m2g_graph.dstdata["lat_lon"],
|
| 235 |
+
"coordinate_separators_min": min_seps,
|
| 236 |
+
"coordinate_separators_max": max_seps,
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
self.m2g_graph, edge_perm = CuGraphCSC.from_dgl(
|
| 240 |
+
graph=self.m2g_graph,
|
| 241 |
+
partition_size=partition_size,
|
| 242 |
+
partition_group_name=partition_group_name,
|
| 243 |
+
partition_by_bbox=use_lat_lon_partitioning,
|
| 244 |
+
**kwargs,
|
| 245 |
+
)
|
| 246 |
+
self.m2g_edata = self.m2g_edata[edge_perm]
|
| 247 |
+
|
| 248 |
+
if self.is_distributed:
|
| 249 |
+
self.m2g_edata = self.m2g_graph.get_edge_features_in_partition(
|
| 250 |
+
self.m2g_edata
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
if use_cugraphops_processor or self.is_distributed:
|
| 254 |
+
kwargs = {}
|
| 255 |
+
if use_lat_lon_partitioning:
|
| 256 |
+
min_seps, max_seps = get_lat_lon_partition_separators(partition_size)
|
| 257 |
+
kwargs = {
|
| 258 |
+
"src_coordinates": self.mesh_graph.ndata["lat_lon"],
|
| 259 |
+
"dst_coordinates": self.mesh_graph.ndata["lat_lon"],
|
| 260 |
+
"coordinate_separators_min": min_seps,
|
| 261 |
+
"coordinate_separators_max": max_seps,
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
self.mesh_graph, edge_perm = CuGraphCSC.from_dgl(
|
| 265 |
+
graph=self.mesh_graph,
|
| 266 |
+
partition_size=partition_size,
|
| 267 |
+
partition_group_name=partition_group_name,
|
| 268 |
+
partition_by_bbox=use_lat_lon_partitioning,
|
| 269 |
+
**kwargs,
|
| 270 |
+
)
|
| 271 |
+
self.mesh_edata = self.mesh_edata[edge_perm]
|
| 272 |
+
if self.is_distributed:
|
| 273 |
+
self.mesh_edata = self.mesh_graph.get_edge_features_in_partition(
|
| 274 |
+
self.mesh_edata
|
| 275 |
+
)
|
| 276 |
+
self.mesh_ndata = self.mesh_graph.get_dst_node_features_in_partition(
|
| 277 |
+
self.mesh_ndata
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
self.input_dim_grid_nodes = input_dim_grid_nodes
|
| 281 |
+
self.output_dim_grid_nodes = output_dim_grid_nodes
|
| 282 |
+
self.input_res = input_res
|
| 283 |
+
|
| 284 |
+
# by default: don't checkpoint at all
|
| 285 |
+
self.model_checkpoint_fn = set_checkpoint_fn(False)
|
| 286 |
+
self.encoder_checkpoint_fn = set_checkpoint_fn(False)
|
| 287 |
+
self.decoder_checkpoint_fn = set_checkpoint_fn(False)
|
| 288 |
+
|
| 289 |
+
# initial feature embedder
|
| 290 |
+
self.encoder_embedder = GraphCastEncoderEmbedder(
|
| 291 |
+
input_dim_grid_nodes=input_dim_grid_nodes,
|
| 292 |
+
input_dim_mesh_nodes=input_dim_mesh_nodes,
|
| 293 |
+
input_dim_edges=input_dim_edges,
|
| 294 |
+
output_dim=hidden_dim,
|
| 295 |
+
hidden_dim=hidden_dim,
|
| 296 |
+
hidden_layers=hidden_layers,
|
| 297 |
+
activation_fn=activation_fn,
|
| 298 |
+
norm_type=norm_type,
|
| 299 |
+
recompute_activation=recompute_activation,
|
| 300 |
+
)
|
| 301 |
+
self.decoder_embedder = GraphCastDecoderEmbedder(
|
| 302 |
+
input_dim_edges=input_dim_edges,
|
| 303 |
+
output_dim=hidden_dim,
|
| 304 |
+
hidden_dim=hidden_dim,
|
| 305 |
+
hidden_layers=hidden_layers,
|
| 306 |
+
activation_fn=activation_fn,
|
| 307 |
+
norm_type=norm_type,
|
| 308 |
+
recompute_activation=recompute_activation,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
# grid2mesh encoder
|
| 312 |
+
self.encoder = MeshGraphEncoder(
|
| 313 |
+
aggregation=aggregation,
|
| 314 |
+
input_dim_src_nodes=hidden_dim,
|
| 315 |
+
input_dim_dst_nodes=hidden_dim,
|
| 316 |
+
input_dim_edges=hidden_dim,
|
| 317 |
+
output_dim_src_nodes=hidden_dim,
|
| 318 |
+
output_dim_dst_nodes=hidden_dim,
|
| 319 |
+
output_dim_edges=hidden_dim,
|
| 320 |
+
hidden_dim=hidden_dim,
|
| 321 |
+
hidden_layers=hidden_layers,
|
| 322 |
+
activation_fn=activation_fn,
|
| 323 |
+
norm_type=norm_type,
|
| 324 |
+
do_concat_trick=do_concat_trick,
|
| 325 |
+
recompute_activation=recompute_activation,
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
# icosahedron processor
|
| 329 |
+
if processor_layers <= 2:
|
| 330 |
+
raise ValueError("Expected at least 3 processor layers")
|
| 331 |
+
if processor_type == "MessagePassing":
|
| 332 |
+
self.processor_encoder = GraphCastProcessor(
|
| 333 |
+
aggregation=aggregation,
|
| 334 |
+
processor_layers=1,
|
| 335 |
+
input_dim_nodes=hidden_dim,
|
| 336 |
+
input_dim_edges=hidden_dim,
|
| 337 |
+
hidden_dim=hidden_dim,
|
| 338 |
+
hidden_layers=hidden_layers,
|
| 339 |
+
activation_fn=activation_fn,
|
| 340 |
+
norm_type=norm_type,
|
| 341 |
+
do_concat_trick=do_concat_trick,
|
| 342 |
+
recompute_activation=recompute_activation,
|
| 343 |
+
)
|
| 344 |
+
self.processor = GraphCastProcessor(
|
| 345 |
+
aggregation=aggregation,
|
| 346 |
+
processor_layers=processor_layers - 2,
|
| 347 |
+
input_dim_nodes=hidden_dim,
|
| 348 |
+
input_dim_edges=hidden_dim,
|
| 349 |
+
hidden_dim=hidden_dim,
|
| 350 |
+
hidden_layers=hidden_layers,
|
| 351 |
+
activation_fn=activation_fn,
|
| 352 |
+
norm_type=norm_type,
|
| 353 |
+
do_concat_trick=do_concat_trick,
|
| 354 |
+
recompute_activation=recompute_activation,
|
| 355 |
+
)
|
| 356 |
+
self.processor_decoder = GraphCastProcessor(
|
| 357 |
+
aggregation=aggregation,
|
| 358 |
+
processor_layers=1,
|
| 359 |
+
input_dim_nodes=hidden_dim,
|
| 360 |
+
input_dim_edges=hidden_dim,
|
| 361 |
+
hidden_dim=hidden_dim,
|
| 362 |
+
hidden_layers=hidden_layers,
|
| 363 |
+
activation_fn=activation_fn,
|
| 364 |
+
norm_type=norm_type,
|
| 365 |
+
do_concat_trick=do_concat_trick,
|
| 366 |
+
recompute_activation=recompute_activation,
|
| 367 |
+
)
|
| 368 |
+
else:
|
| 369 |
+
self.processor_encoder = torch.nn.Identity()
|
| 370 |
+
self.processor = GraphCastProcessorGraphTransformer(
|
| 371 |
+
attention_mask=self.attn_mask,
|
| 372 |
+
num_attention_heads=num_attention_heads,
|
| 373 |
+
processor_layers=processor_layers,
|
| 374 |
+
input_dim_nodes=hidden_dim,
|
| 375 |
+
hidden_dim=hidden_dim,
|
| 376 |
+
)
|
| 377 |
+
self.processor_decoder = torch.nn.Identity()
|
| 378 |
+
|
| 379 |
+
# mesh2grid decoder
|
| 380 |
+
self.decoder = MeshGraphDecoder(
|
| 381 |
+
aggregation=aggregation,
|
| 382 |
+
input_dim_src_nodes=hidden_dim,
|
| 383 |
+
input_dim_dst_nodes=hidden_dim,
|
| 384 |
+
input_dim_edges=hidden_dim,
|
| 385 |
+
output_dim_dst_nodes=hidden_dim,
|
| 386 |
+
output_dim_edges=hidden_dim,
|
| 387 |
+
hidden_dim=hidden_dim,
|
| 388 |
+
hidden_layers=hidden_layers,
|
| 389 |
+
activation_fn=activation_fn,
|
| 390 |
+
norm_type=norm_type,
|
| 391 |
+
do_concat_trick=do_concat_trick,
|
| 392 |
+
recompute_activation=recompute_activation,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
# final MLP
|
| 396 |
+
self.finale = MeshGraphMLP(
|
| 397 |
+
input_dim=hidden_dim,
|
| 398 |
+
output_dim=output_dim_grid_nodes,
|
| 399 |
+
hidden_dim=hidden_dim,
|
| 400 |
+
hidden_layers=hidden_layers,
|
| 401 |
+
activation_fn=activation_fn,
|
| 402 |
+
norm_type=None,
|
| 403 |
+
recompute_activation=recompute_activation,
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
def set_checkpoint_model(self, checkpoint_flag: bool):
|
| 407 |
+
"""Sets checkpoint function for the entire model.
|
| 408 |
+
|
| 409 |
+
This function returns the appropriate checkpoint function based on the
|
| 410 |
+
provided `checkpoint_flag` flag. If `checkpoint_flag` is True, the
|
| 411 |
+
function returns the checkpoint function from PyTorch's
|
| 412 |
+
`torch.utils.checkpoint`. In this case, all the other gradient checkpoitings
|
| 413 |
+
will be disabled. Otherwise, it returns an identity function
|
| 414 |
+
that simply passes the inputs through the given layer.
|
| 415 |
+
|
| 416 |
+
Parameters
|
| 417 |
+
----------
|
| 418 |
+
checkpoint_flag : bool
|
| 419 |
+
Whether to use checkpointing for gradient computation. Checkpointing
|
| 420 |
+
can reduce memory usage during backpropagation at the cost of
|
| 421 |
+
increased computation time.
|
| 422 |
+
|
| 423 |
+
Returns
|
| 424 |
+
-------
|
| 425 |
+
Callable
|
| 426 |
+
The selected checkpoint function to use for gradient computation.
|
| 427 |
+
"""
|
| 428 |
+
# force a single checkpoint for the whole model
|
| 429 |
+
self.model_checkpoint_fn = set_checkpoint_fn(checkpoint_flag)
|
| 430 |
+
if checkpoint_flag:
|
| 431 |
+
self.processor.set_checkpoint_segments(-1)
|
| 432 |
+
self.encoder_checkpoint_fn = set_checkpoint_fn(False)
|
| 433 |
+
self.decoder_checkpoint_fn = set_checkpoint_fn(False)
|
| 434 |
+
|
| 435 |
+
def set_checkpoint_processor(self, checkpoint_segments: int):
|
| 436 |
+
"""Sets checkpoint function for the processor excluding the first and last
|
| 437 |
+
layers.
|
| 438 |
+
|
| 439 |
+
This function returns the appropriate checkpoint function based on the
|
| 440 |
+
provided `checkpoint_segments` flag. If `checkpoint_segments` is positive,
|
| 441 |
+
the function returns the checkpoint function from PyTorch's
|
| 442 |
+
`torch.utils.checkpoint`, with number of checkpointing segments equal to
|
| 443 |
+
`checkpoint_segments`. Otherwise, it returns an identity function
|
| 444 |
+
that simply passes the inputs through the given layer.
|
| 445 |
+
|
| 446 |
+
Parameters
|
| 447 |
+
----------
|
| 448 |
+
checkpoint_segments : int
|
| 449 |
+
Number of checkpointing segments for gradient computation. Checkpointing
|
| 450 |
+
can reduce memory usage during backpropagation at the cost of
|
| 451 |
+
increased computation time.
|
| 452 |
+
|
| 453 |
+
Returns
|
| 454 |
+
-------
|
| 455 |
+
Callable
|
| 456 |
+
The selected checkpoint function to use for gradient computation.
|
| 457 |
+
"""
|
| 458 |
+
self.processor.set_checkpoint_segments(checkpoint_segments)
|
| 459 |
+
|
| 460 |
+
def set_checkpoint_encoder(self, checkpoint_flag: bool):
|
| 461 |
+
"""Sets checkpoint function for the embedder, encoder, and the first of
|
| 462 |
+
the processor.
|
| 463 |
+
|
| 464 |
+
This function returns the appropriate checkpoint function based on the
|
| 465 |
+
provided `checkpoint_flag` flag. If `checkpoint_flag` is True, the
|
| 466 |
+
function returns the checkpoint function from PyTorch's
|
| 467 |
+
`torch.utils.checkpoint`. Otherwise, it returns an identity function
|
| 468 |
+
that simply passes the inputs through the given layer.
|
| 469 |
+
|
| 470 |
+
Parameters
|
| 471 |
+
----------
|
| 472 |
+
checkpoint_flag : bool
|
| 473 |
+
Whether to use checkpointing for gradient computation. Checkpointing
|
| 474 |
+
can reduce memory usage during backpropagation at the cost of
|
| 475 |
+
increased computation time.
|
| 476 |
+
|
| 477 |
+
Returns
|
| 478 |
+
-------
|
| 479 |
+
Callable
|
| 480 |
+
The selected checkpoint function to use for gradient computation.
|
| 481 |
+
"""
|
| 482 |
+
self.encoder_checkpoint_fn = set_checkpoint_fn(checkpoint_flag)
|
| 483 |
+
|
| 484 |
+
def set_checkpoint_decoder(self, checkpoint_flag: bool):
|
| 485 |
+
"""Sets checkpoint function for the last layer of the processor, the decoder,
|
| 486 |
+
and the final MLP.
|
| 487 |
+
|
| 488 |
+
This function returns the appropriate checkpoint function based on the
|
| 489 |
+
provided `checkpoint_flag` flag. If `checkpoint_flag` is True, the
|
| 490 |
+
function returns the checkpoint function from PyTorch's
|
| 491 |
+
`torch.utils.checkpoint`. Otherwise, it returns an identity function
|
| 492 |
+
that simply passes the inputs through the given layer.
|
| 493 |
+
|
| 494 |
+
Parameters
|
| 495 |
+
----------
|
| 496 |
+
checkpoint_flag : bool
|
| 497 |
+
Whether to use checkpointing for gradient computation. Checkpointing
|
| 498 |
+
can reduce memory usage during backpropagation at the cost of
|
| 499 |
+
increased computation time.
|
| 500 |
+
|
| 501 |
+
Returns
|
| 502 |
+
-------
|
| 503 |
+
Callable
|
| 504 |
+
The selected checkpoint function to use for gradient computation.
|
| 505 |
+
"""
|
| 506 |
+
self.decoder_checkpoint_fn = set_checkpoint_fn(checkpoint_flag)
|
| 507 |
+
|
| 508 |
+
def encoder_forward(
|
| 509 |
+
self,
|
| 510 |
+
grid_nfeat: Tensor,
|
| 511 |
+
) -> Tensor:
|
| 512 |
+
"""Forward method for the embedder, encoder, and the first of the processor.
|
| 513 |
+
|
| 514 |
+
Parameters
|
| 515 |
+
----------
|
| 516 |
+
grid_nfeat : Tensor
|
| 517 |
+
Node features for the latitude-longitude grid.
|
| 518 |
+
|
| 519 |
+
Returns
|
| 520 |
+
-------
|
| 521 |
+
mesh_efeat_processed: Tensor
|
| 522 |
+
Processed edge features for the multimesh.
|
| 523 |
+
mesh_nfeat_processed: Tensor
|
| 524 |
+
Processed node features for the multimesh.
|
| 525 |
+
grid_nfeat_encoded: Tensor
|
| 526 |
+
Encoded node features for the latitude-longitude grid.
|
| 527 |
+
"""
|
| 528 |
+
|
| 529 |
+
# embedd graph features
|
| 530 |
+
(
|
| 531 |
+
grid_nfeat_embedded,
|
| 532 |
+
mesh_nfeat_embedded,
|
| 533 |
+
g2m_efeat_embedded,
|
| 534 |
+
mesh_efeat_embedded,
|
| 535 |
+
) = self.encoder_embedder(
|
| 536 |
+
grid_nfeat,
|
| 537 |
+
self.mesh_ndata,
|
| 538 |
+
self.g2m_edata,
|
| 539 |
+
self.mesh_edata
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
# encode lat/lon to multimesh
|
| 543 |
+
grid_nfeat_encoded, mesh_nfeat_encoded = self.encoder(
|
| 544 |
+
g2m_efeat_embedded,
|
| 545 |
+
grid_nfeat_embedded,
|
| 546 |
+
mesh_nfeat_embedded,
|
| 547 |
+
self.g2m_graph,
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
# process multimesh graph
|
| 551 |
+
if self.processor_type == "MessagePassing":
|
| 552 |
+
mesh_efeat_processed, mesh_nfeat_processed = self.processor_encoder(
|
| 553 |
+
mesh_efeat_embedded,
|
| 554 |
+
mesh_nfeat_encoded,
|
| 555 |
+
self.mesh_graph,
|
| 556 |
+
)
|
| 557 |
+
else:
|
| 558 |
+
mesh_nfeat_processed = self.processor_encoder(
|
| 559 |
+
mesh_nfeat_encoded,
|
| 560 |
+
)
|
| 561 |
+
mesh_efeat_processed = None
|
| 562 |
+
return mesh_efeat_processed, mesh_nfeat_processed, grid_nfeat_encoded
|
| 563 |
+
|
| 564 |
+
def decoder_forward(
|
| 565 |
+
self,
|
| 566 |
+
mesh_efeat_processed: Tensor,
|
| 567 |
+
mesh_nfeat_processed: Tensor,
|
| 568 |
+
grid_nfeat_encoded: Tensor,
|
| 569 |
+
) -> Tensor:
|
| 570 |
+
"""Forward method for the last layer of the processor, the decoder,
|
| 571 |
+
and the final MLP.
|
| 572 |
+
|
| 573 |
+
Parameters
|
| 574 |
+
----------
|
| 575 |
+
mesh_efeat_processed : Tensor
|
| 576 |
+
Multimesh edge features processed by the processor.
|
| 577 |
+
mesh_nfeat_processed : Tensor
|
| 578 |
+
Multi-mesh node features processed by the processor.
|
| 579 |
+
grid_nfeat_encoded : Tensor
|
| 580 |
+
The encoded node features for the latitude-longitude grid.
|
| 581 |
+
|
| 582 |
+
Returns
|
| 583 |
+
-------
|
| 584 |
+
grid_nfeat_finale: Tensor
|
| 585 |
+
The final node features for the latitude-longitude grid.
|
| 586 |
+
"""
|
| 587 |
+
|
| 588 |
+
# process multimesh graph
|
| 589 |
+
if self.processor_type == "MessagePassing":
|
| 590 |
+
_, mesh_nfeat_processed = self.processor_decoder(
|
| 591 |
+
mesh_efeat_processed,
|
| 592 |
+
mesh_nfeat_processed,
|
| 593 |
+
self.mesh_graph,
|
| 594 |
+
)
|
| 595 |
+
else:
|
| 596 |
+
mesh_nfeat_processed = self.processor_decoder(
|
| 597 |
+
mesh_nfeat_processed,
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
m2g_efeat_embedded = self.decoder_embedder(self.m2g_edata)
|
| 601 |
+
|
| 602 |
+
# decode multimesh to lat/lon
|
| 603 |
+
grid_nfeat_decoded = self.decoder(
|
| 604 |
+
m2g_efeat_embedded, grid_nfeat_encoded, mesh_nfeat_processed, self.m2g_graph
|
| 605 |
+
)
|
| 606 |
+
|
| 607 |
+
# map to the target output dimension
|
| 608 |
+
grid_nfeat_finale = self.finale(
|
| 609 |
+
grid_nfeat_decoded,
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
return grid_nfeat_finale
|
| 613 |
+
|
| 614 |
+
def custom_forward(self, grid_nfeat: Tensor) -> Tensor:
|
| 615 |
+
"""GraphCast forward method with support for gradient checkpointing.
|
| 616 |
+
|
| 617 |
+
Parameters
|
| 618 |
+
----------
|
| 619 |
+
grid_nfeat : Tensor
|
| 620 |
+
Node features of the latitude-longitude graph.
|
| 621 |
+
|
| 622 |
+
Returns
|
| 623 |
+
-------
|
| 624 |
+
grid_nfeat_finale: Tensor
|
| 625 |
+
Predicted node features of the latitude-longitude graph.
|
| 626 |
+
"""
|
| 627 |
+
(
|
| 628 |
+
mesh_efeat_processed,
|
| 629 |
+
mesh_nfeat_processed,
|
| 630 |
+
grid_nfeat_encoded,
|
| 631 |
+
) = self.encoder_checkpoint_fn(
|
| 632 |
+
self.encoder_forward,
|
| 633 |
+
grid_nfeat,
|
| 634 |
+
use_reentrant=False,
|
| 635 |
+
preserve_rng_state=False,
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
# checkpoint of processor done in processor itself
|
| 639 |
+
if self.processor_type == "MessagePassing":
|
| 640 |
+
mesh_efeat_processed, mesh_nfeat_processed = self.processor(
|
| 641 |
+
mesh_efeat_processed,
|
| 642 |
+
mesh_nfeat_processed,
|
| 643 |
+
self.mesh_graph,
|
| 644 |
+
)
|
| 645 |
+
else:
|
| 646 |
+
mesh_nfeat_processed = self.processor(
|
| 647 |
+
mesh_nfeat_processed,
|
| 648 |
+
)
|
| 649 |
+
mesh_efeat_processed = None
|
| 650 |
+
|
| 651 |
+
grid_nfeat_finale = self.decoder_checkpoint_fn(
|
| 652 |
+
self.decoder_forward,
|
| 653 |
+
mesh_efeat_processed,
|
| 654 |
+
mesh_nfeat_processed,
|
| 655 |
+
grid_nfeat_encoded,
|
| 656 |
+
use_reentrant=False,
|
| 657 |
+
preserve_rng_state=False,
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
return grid_nfeat_finale
|
| 661 |
+
|
| 662 |
+
def forward(
|
| 663 |
+
self,
|
| 664 |
+
grid_nfeat: Tensor,
|
| 665 |
+
) -> Tensor:
|
| 666 |
+
invar = self.prepare_input(
|
| 667 |
+
grid_nfeat, self.expect_partitioned_input, self.global_features_on_rank_0
|
| 668 |
+
)
|
| 669 |
+
outvar = self.model_checkpoint_fn(
|
| 670 |
+
self.custom_forward,
|
| 671 |
+
invar,
|
| 672 |
+
use_reentrant=False,
|
| 673 |
+
preserve_rng_state=False,
|
| 674 |
+
)
|
| 675 |
+
outvar = self.prepare_output(
|
| 676 |
+
outvar,
|
| 677 |
+
self.produce_aggregated_output,
|
| 678 |
+
self.produce_aggregated_output_on_all_ranks,
|
| 679 |
+
)
|
| 680 |
+
return outvar
|
| 681 |
+
|
| 682 |
+
def prepare_input(
|
| 683 |
+
self,
|
| 684 |
+
invar: Tensor,
|
| 685 |
+
expect_partitioned_input: bool,
|
| 686 |
+
global_features_on_rank_0: bool,
|
| 687 |
+
) -> Tensor:
|
| 688 |
+
"""Prepares the input to the model in the required shape.
|
| 689 |
+
|
| 690 |
+
Parameters
|
| 691 |
+
----------
|
| 692 |
+
invar : Tensor
|
| 693 |
+
Input in the shape [N, C, H, W].
|
| 694 |
+
|
| 695 |
+
expect_partitioned_input : bool
|
| 696 |
+
flag indicating whether input is partioned according to graph partitioning scheme
|
| 697 |
+
|
| 698 |
+
global_features_on_rank_0 : bool
|
| 699 |
+
Flag indicating whether input is in its "global" form only on group_rank 0 which
|
| 700 |
+
requires a scatter operation beforehand. Note that only either this flag or
|
| 701 |
+
expect_partitioned_input can be set at a time.
|
| 702 |
+
|
| 703 |
+
Returns
|
| 704 |
+
-------
|
| 705 |
+
Tensor
|
| 706 |
+
Reshaped input.
|
| 707 |
+
"""
|
| 708 |
+
if global_features_on_rank_0 and expect_partitioned_input:
|
| 709 |
+
raise ValueError(
|
| 710 |
+
"global_features_on_rank_0 and expect_partitioned_input cannot be set at the same time."
|
| 711 |
+
)
|
| 712 |
+
|
| 713 |
+
if not self.is_distributed:
|
| 714 |
+
if invar.size(0) != 1:
|
| 715 |
+
raise ValueError("GraphCast does not support batch size > 1")
|
| 716 |
+
invar = invar[0].view(self.input_dim_grid_nodes, -1).permute(1, 0)
|
| 717 |
+
|
| 718 |
+
else:
|
| 719 |
+
# is_distributed
|
| 720 |
+
if not expect_partitioned_input:
|
| 721 |
+
# global_features_on_rank_0
|
| 722 |
+
if invar.size(0) != 1:
|
| 723 |
+
raise ValueError("GraphCast does not support batch size > 1")
|
| 724 |
+
|
| 725 |
+
invar = invar[0].view(self.input_dim_grid_nodes, -1).permute(1, 0)
|
| 726 |
+
|
| 727 |
+
# scatter global features
|
| 728 |
+
invar = self.g2m_graph.get_src_node_features_in_partition(
|
| 729 |
+
invar,
|
| 730 |
+
scatter_features=global_features_on_rank_0,
|
| 731 |
+
)
|
| 732 |
+
|
| 733 |
+
return invar
|
| 734 |
+
|
| 735 |
+
def prepare_output(
|
| 736 |
+
self,
|
| 737 |
+
outvar: Tensor,
|
| 738 |
+
produce_aggregated_output: bool,
|
| 739 |
+
produce_aggregated_output_on_all_ranks: bool = True,
|
| 740 |
+
) -> Tensor:
|
| 741 |
+
"""Prepares the output of the model in the shape [N, C, H, W].
|
| 742 |
+
|
| 743 |
+
Parameters
|
| 744 |
+
----------
|
| 745 |
+
outvar : Tensor
|
| 746 |
+
Output of the final MLP of the model.
|
| 747 |
+
|
| 748 |
+
produce_aggregated_output : bool
|
| 749 |
+
flag indicating whether output is gathered onto each rank
|
| 750 |
+
or kept distributed
|
| 751 |
+
|
| 752 |
+
produce_aggregated_output_on_all_ranks : bool
|
| 753 |
+
flag indicating whether output is gatherered on each rank
|
| 754 |
+
or only gathered at group_rank 0, True by default and
|
| 755 |
+
only valid if produce_aggregated_output is set.
|
| 756 |
+
|
| 757 |
+
Returns
|
| 758 |
+
-------
|
| 759 |
+
Tensor
|
| 760 |
+
The reshaped output of the model.
|
| 761 |
+
"""
|
| 762 |
+
if produce_aggregated_output or not self.is_distributed:
|
| 763 |
+
# default case: output of shape [N, C, H, W]
|
| 764 |
+
if self.is_distributed:
|
| 765 |
+
outvar = self.m2g_graph.get_global_dst_node_features(
|
| 766 |
+
outvar,
|
| 767 |
+
get_on_all_ranks=produce_aggregated_output_on_all_ranks,
|
| 768 |
+
)
|
| 769 |
+
|
| 770 |
+
outvar = outvar.permute(1, 0)
|
| 771 |
+
outvar = outvar.view(self.output_dim_grid_nodes, *self.input_res)
|
| 772 |
+
outvar = torch.unsqueeze(outvar, dim=0)
|
| 773 |
+
|
| 774 |
+
return outvar
|
| 775 |
+
|
| 776 |
+
def to(self, *args: Any, **kwargs: Any) -> Self:
|
| 777 |
+
"""Moves the object to the specified device, dtype, or format.
|
| 778 |
+
This method moves the object and its underlying graph and graph features to
|
| 779 |
+
the specified device, dtype, or format, and returns the updated object.
|
| 780 |
+
|
| 781 |
+
Parameters
|
| 782 |
+
----------
|
| 783 |
+
*args : Any
|
| 784 |
+
Positional arguments to be passed to the `torch._C._nn._parse_to` function.
|
| 785 |
+
**kwargs : Any
|
| 786 |
+
Keyword arguments to be passed to the `torch._C._nn._parse_to` function.
|
| 787 |
+
|
| 788 |
+
Returns
|
| 789 |
+
-------
|
| 790 |
+
GraphCastNet
|
| 791 |
+
The updated object after moving to the specified device, dtype, or format.
|
| 792 |
+
"""
|
| 793 |
+
self = super(GraphCastNet, self).to(*args, **kwargs)
|
| 794 |
+
|
| 795 |
+
self.g2m_edata = self.g2m_edata.to(*args, **kwargs)
|
| 796 |
+
self.m2g_edata = self.m2g_edata.to(*args, **kwargs)
|
| 797 |
+
self.mesh_ndata = self.mesh_ndata.to(*args, **kwargs)
|
| 798 |
+
self.mesh_edata = self.mesh_edata.to(*args, **kwargs)
|
| 799 |
+
|
| 800 |
+
device, _, _, _ = torch._C._nn._parse_to(*args, **kwargs)
|
| 801 |
+
self.g2m_graph = self.g2m_graph.to(device)
|
| 802 |
+
self.mesh_graph = self.mesh_graph.to(device)
|
| 803 |
+
self.m2g_graph = self.m2g_graph.to(device)
|
| 804 |
+
|
| 805 |
+
return self
|
model/graph_cast_processor.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Union
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
# import transformer_engine as te
|
| 6 |
+
from dgl import DGLGraph
|
| 7 |
+
from torch import Tensor
|
| 8 |
+
|
| 9 |
+
from onescience.modules.edge.mesh_edge_block import MeshEdgeBlock
|
| 10 |
+
from onescience.modules.node.mesh_node_block import MeshNodeBlock
|
| 11 |
+
from onescience.modules.utils.gnnlayer_utils import CuGraphCSC, set_checkpoint_fn
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class GraphCastProcessor(nn.Module):
|
| 15 |
+
"""Processor block used in GraphCast operating on a latent space
|
| 16 |
+
represented by hierarchy of icosahedral meshes.
|
| 17 |
+
|
| 18 |
+
Parameters
|
| 19 |
+
----------
|
| 20 |
+
aggregation : str, optional
|
| 21 |
+
message passing aggregation method ("sum", "mean"), by default "sum"
|
| 22 |
+
processor_layers : int, optional
|
| 23 |
+
number of processor layers, by default 16
|
| 24 |
+
input_dim_nodes : int, optional
|
| 25 |
+
input dimensionality of the node features, by default 512
|
| 26 |
+
input_dim_edges : int, optional
|
| 27 |
+
input dimensionality of the edge features, by default 512
|
| 28 |
+
hidden_dim : int, optional
|
| 29 |
+
number of neurons in each hidden layer, by default 512
|
| 30 |
+
hidden_layers : int, optional
|
| 31 |
+
number of hiddel layers, by default 1
|
| 32 |
+
activation_fn : nn.Module, optional
|
| 33 |
+
type of activation function, by default nn.SiLU()
|
| 34 |
+
norm_type : str, optional
|
| 35 |
+
Normalization type ["TELayerNorm", "LayerNorm"].
|
| 36 |
+
Use "TELayerNorm" for optimal performance. By default "LayerNorm".
|
| 37 |
+
do_conat_trick: : bool, default=False
|
| 38 |
+
whether to replace concat+MLP with MLP+idx+sum
|
| 39 |
+
recompute_activation : bool, optional
|
| 40 |
+
Flag for recomputing activation in backward to save memory, by default False.
|
| 41 |
+
Currently, only SiLU is supported.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
def __init__(
|
| 45 |
+
self,
|
| 46 |
+
aggregation: str = "sum",
|
| 47 |
+
processor_layers: int = 16,
|
| 48 |
+
input_dim_nodes: int = 512,
|
| 49 |
+
input_dim_edges: int = 512,
|
| 50 |
+
hidden_dim: int = 512,
|
| 51 |
+
hidden_layers: int = 1,
|
| 52 |
+
activation_fn: nn.Module = nn.SiLU(),
|
| 53 |
+
norm_type: str = "LayerNorm",
|
| 54 |
+
do_concat_trick: bool = False,
|
| 55 |
+
recompute_activation: bool = False,
|
| 56 |
+
):
|
| 57 |
+
super().__init__()
|
| 58 |
+
|
| 59 |
+
edge_block_invars = (
|
| 60 |
+
input_dim_nodes,
|
| 61 |
+
input_dim_edges,
|
| 62 |
+
input_dim_edges,
|
| 63 |
+
hidden_dim,
|
| 64 |
+
hidden_layers,
|
| 65 |
+
activation_fn,
|
| 66 |
+
norm_type,
|
| 67 |
+
do_concat_trick,
|
| 68 |
+
recompute_activation,
|
| 69 |
+
)
|
| 70 |
+
node_block_invars = (
|
| 71 |
+
aggregation,
|
| 72 |
+
input_dim_nodes,
|
| 73 |
+
input_dim_edges,
|
| 74 |
+
input_dim_nodes,
|
| 75 |
+
hidden_dim,
|
| 76 |
+
hidden_layers,
|
| 77 |
+
activation_fn,
|
| 78 |
+
norm_type,
|
| 79 |
+
recompute_activation,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
layers = []
|
| 83 |
+
for _ in range(processor_layers):
|
| 84 |
+
layers.append(MeshEdgeBlock(**dict(zip(
|
| 85 |
+
['input_dim_nodes','input_dim_edges','output_dim','hidden_dim',
|
| 86 |
+
'hidden_layers','activation_fn','norm_type','do_concat_trick',
|
| 87 |
+
'recompute_activation'], edge_block_invars))))
|
| 88 |
+
layers.append(MeshNodeBlock(**dict(zip(
|
| 89 |
+
['aggregation','input_dim_nodes','input_dim_edges','output_dim',
|
| 90 |
+
'hidden_dim','hidden_layers','activation_fn','norm_type',
|
| 91 |
+
'recompute_activation'], node_block_invars))))
|
| 92 |
+
|
| 93 |
+
self.processor_layers = nn.ModuleList(layers)
|
| 94 |
+
self.num_processor_layers = len(self.processor_layers)
|
| 95 |
+
# per default, no checkpointing
|
| 96 |
+
# one segment for compatability
|
| 97 |
+
self.checkpoint_segments = [(0, self.num_processor_layers)]
|
| 98 |
+
self.checkpoint_fn = set_checkpoint_fn(False)
|
| 99 |
+
|
| 100 |
+
def set_checkpoint_segments(self, checkpoint_segments: int):
|
| 101 |
+
"""
|
| 102 |
+
Set the number of checkpoint segments
|
| 103 |
+
|
| 104 |
+
Parameters
|
| 105 |
+
----------
|
| 106 |
+
checkpoint_segments : int
|
| 107 |
+
number of checkpoint segments
|
| 108 |
+
|
| 109 |
+
Raises
|
| 110 |
+
------
|
| 111 |
+
ValueError
|
| 112 |
+
if the number of processor layers is not a multiple of the number of
|
| 113 |
+
checkpoint segments
|
| 114 |
+
"""
|
| 115 |
+
if checkpoint_segments > 0:
|
| 116 |
+
if self.num_processor_layers % checkpoint_segments != 0:
|
| 117 |
+
raise ValueError(
|
| 118 |
+
"Processor layers must be a multiple of checkpoint_segments"
|
| 119 |
+
)
|
| 120 |
+
segment_size = self.num_processor_layers // checkpoint_segments
|
| 121 |
+
self.checkpoint_segments = []
|
| 122 |
+
for i in range(0, self.num_processor_layers, segment_size):
|
| 123 |
+
self.checkpoint_segments.append((i, i + segment_size))
|
| 124 |
+
|
| 125 |
+
self.checkpoint_fn = set_checkpoint_fn(True)
|
| 126 |
+
else:
|
| 127 |
+
self.checkpoint_fn = set_checkpoint_fn(False)
|
| 128 |
+
self.checkpoint_segments = [(0, self.num_processor_layers)]
|
| 129 |
+
|
| 130 |
+
def run_function(self, segment_start: int, segment_end: int):
|
| 131 |
+
"""Custom forward for gradient checkpointing
|
| 132 |
+
|
| 133 |
+
Parameters
|
| 134 |
+
----------
|
| 135 |
+
segment_start : int
|
| 136 |
+
Layer index as start of the segment
|
| 137 |
+
segment_end : int
|
| 138 |
+
Layer index as end of the segment
|
| 139 |
+
|
| 140 |
+
Returns
|
| 141 |
+
-------
|
| 142 |
+
function
|
| 143 |
+
Custom forward function
|
| 144 |
+
"""
|
| 145 |
+
segment = self.processor_layers[segment_start:segment_end]
|
| 146 |
+
|
| 147 |
+
def custom_forward(efeat, nfeat, graph):
|
| 148 |
+
"""Custom forward function"""
|
| 149 |
+
for module in segment:
|
| 150 |
+
efeat, nfeat = module(efeat, nfeat, graph)
|
| 151 |
+
return efeat, nfeat
|
| 152 |
+
|
| 153 |
+
return custom_forward
|
| 154 |
+
|
| 155 |
+
def forward(
|
| 156 |
+
self,
|
| 157 |
+
efeat: Tensor,
|
| 158 |
+
nfeat: Tensor,
|
| 159 |
+
graph: Union[DGLGraph, CuGraphCSC],
|
| 160 |
+
) -> Tensor:
|
| 161 |
+
for segment_start, segment_end in self.checkpoint_segments:
|
| 162 |
+
efeat, nfeat = self.checkpoint_fn(
|
| 163 |
+
self.run_function(segment_start, segment_end),
|
| 164 |
+
efeat,
|
| 165 |
+
nfeat,
|
| 166 |
+
graph,
|
| 167 |
+
use_reentrant=False,
|
| 168 |
+
preserve_rng_state=False,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
return efeat, nfeat
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
class GraphCastProcessorGraphTransformer(nn.Module):
|
| 175 |
+
"""Processor block used in GenCast operating on a latent space
|
| 176 |
+
represented by hierarchy of icosahedral meshes.
|
| 177 |
+
|
| 178 |
+
Parameters
|
| 179 |
+
----------
|
| 180 |
+
attn_mask : torch.Tensor
|
| 181 |
+
Attention mask to be applied within the transformer layers.
|
| 182 |
+
processor_layers : int, optional (default=16)
|
| 183 |
+
Number of processing layers.
|
| 184 |
+
input_dim_nodes : int, optional (default=512)
|
| 185 |
+
Dimension of the input features for each node.
|
| 186 |
+
hidden_dim : int, optional (default=512)
|
| 187 |
+
Dimension of the hidden features within the transformer layers.
|
| 188 |
+
"""
|
| 189 |
+
|
| 190 |
+
def __init__(
|
| 191 |
+
self,
|
| 192 |
+
attention_mask: torch.Tensor,
|
| 193 |
+
num_attention_heads: int = 4,
|
| 194 |
+
processor_layers: int = 16,
|
| 195 |
+
input_dim_nodes: int = 512,
|
| 196 |
+
hidden_dim: int = 512,
|
| 197 |
+
):
|
| 198 |
+
super().__init__()
|
| 199 |
+
self.num_attention_heads = num_attention_heads
|
| 200 |
+
self.hidden_dim = hidden_dim
|
| 201 |
+
self.attention_mask = torch.tensor(attention_mask, dtype=torch.bool)
|
| 202 |
+
self.register_buffer("mask", self.attention_mask, persistent=False)
|
| 203 |
+
|
| 204 |
+
layers = [
|
| 205 |
+
# te.pytorch.TransformerLayer(
|
| 206 |
+
# hidden_size=input_dim_nodes,
|
| 207 |
+
# ffn_hidden_size=hidden_dim,
|
| 208 |
+
# num_attention_heads=num_attention_heads,
|
| 209 |
+
# layer_number=i + 1,
|
| 210 |
+
# fuse_qkv_params=False,
|
| 211 |
+
# )
|
| 212 |
+
# for i in range(processor_layers)
|
| 213 |
+
|
| 214 |
+
nn.TransformerEncoderLayer(
|
| 215 |
+
d_model=input_dim_nodes, # 等同于 hidden_size
|
| 216 |
+
nhead=num_attention_heads, # 等同于 num_attention_heads
|
| 217 |
+
dim_feedforward=hidden_dim, # 等同于 ffn_hidden_size
|
| 218 |
+
activation='gelu', # 激活函数可以选择 gelu 或 relu
|
| 219 |
+
batch_first=True # 使得输入张量维度为 (batch_size, seq_len, d_model)
|
| 220 |
+
)
|
| 221 |
+
for _ in range(processor_layers) # 创建多层 Transformer 编码器层
|
| 222 |
+
]
|
| 223 |
+
self.processor_layers = nn.ModuleList(layers)
|
| 224 |
+
|
| 225 |
+
def forward(
|
| 226 |
+
self,
|
| 227 |
+
nfeat: Tensor,
|
| 228 |
+
) -> Tensor:
|
| 229 |
+
nfeat = nfeat.unsqueeze(1)
|
| 230 |
+
# TODO make sure reshaping the last dim to (h, d) is done automatically in the transformer layer
|
| 231 |
+
for module in self.processor_layers:
|
| 232 |
+
nfeat = module(
|
| 233 |
+
nfeat,
|
| 234 |
+
attention_mask=self.mask,
|
| 235 |
+
self_attn_mask_type="arbitrary",
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
return torch.squeeze(nfeat, 1)
|
scripts/compute_time_diff_std.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import numpy as np
|
| 5 |
+
from tqdm import tqdm
|
| 6 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 7 |
+
from onescience.utils.YParams import YParams
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
# instantiate the training datapipe
|
| 12 |
+
config_file_path = os.path.join(current_path, 'conf/config.yaml')
|
| 13 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 14 |
+
datapipe = ERA5Datapipe(
|
| 15 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 16 |
+
used_variables=cfg_data.dataset.channels,
|
| 17 |
+
used_years=cfg_data.dataset.train_time,
|
| 18 |
+
distributed=False
|
| 19 |
+
)
|
| 20 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 21 |
+
|
| 22 |
+
print(f"Loaded training datapipe of length {len(train_dataloader)}")
|
| 23 |
+
|
| 24 |
+
area = torch.abs(torch.cos(torch.linspace(-90, 90, steps=cfg_data.dataset.img_size[0]) * np.pi / 180))
|
| 25 |
+
area /= torch.mean(area)
|
| 26 |
+
area = area.unsqueeze(1)
|
| 27 |
+
|
| 28 |
+
mean, mean_sqr = 0, 0
|
| 29 |
+
for data in tqdm(train_dataloader):
|
| 30 |
+
invar = data[0] # [b, N, h, w]
|
| 31 |
+
outvar = data[1] # [b, N, h, w]
|
| 32 |
+
diff = outvar - invar
|
| 33 |
+
weighted_diff = area * diff
|
| 34 |
+
weighted_diff_sqr = torch.square(weighted_diff)
|
| 35 |
+
mean += torch.mean(weighted_diff, dim=(2, 3)) / len(train_dataloader)
|
| 36 |
+
mean_sqr += torch.mean(weighted_diff_sqr, dim=(2, 3)) / len(train_dataloader)
|
| 37 |
+
|
| 38 |
+
variance = mean_sqr - mean**2 # [1,num_channel, 1,1]
|
| 39 |
+
std = torch.sqrt(variance)
|
| 40 |
+
|
| 41 |
+
np.save("time_diff_std.npy", std.numpy())
|
| 42 |
+
print(f"saving time_diff_std.npy, shapes are {std.numpy().shape}")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
current_path = os.getcwd()
|
| 47 |
+
sys.path.append(current_path)
|
| 48 |
+
main()
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import h5py
|
| 3 |
+
import numpy as np
|
| 4 |
+
import xarray as xr
|
| 5 |
+
from onescience.utils.YParams import YParams
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# 各数据集固定的空间和时间维度
|
| 9 |
+
DATASET_DIMS = {"T": 10, "H": 721, "W": 1440, "time_step": 6}
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def generate_fake_h5(data_dir, var_names, years, dims):
|
| 13 |
+
"""
|
| 14 |
+
为每个年份生成一个空 h5 文件。
|
| 15 |
+
利用 HDF5 chunked 数据集未写入 chunk 即返回 fill_value=0 的特性,
|
| 16 |
+
文件实际只含元数据,极小,但 shape 与真实数据完全一致。
|
| 17 |
+
均值/标准差也作为数据集内嵌进每年的 h5,与 era5.py 新版读取方式对应。
|
| 18 |
+
"""
|
| 19 |
+
os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
|
| 20 |
+
T, C = dims["T"], len(var_names)
|
| 21 |
+
H, W = dims["H"], dims["W"]
|
| 22 |
+
|
| 23 |
+
means = np.zeros((1, C, 1, 1), dtype=np.float32)
|
| 24 |
+
stds = np.ones((1, C, 1, 1), dtype=np.float32)
|
| 25 |
+
|
| 26 |
+
for year in years:
|
| 27 |
+
path = os.path.join(data_dir, "data", f"{year}.h5")
|
| 28 |
+
with h5py.File(path, "w") as f:
|
| 29 |
+
ds = f.create_dataset(
|
| 30 |
+
"fields",
|
| 31 |
+
shape=(T, C, H, W),
|
| 32 |
+
dtype="float32",
|
| 33 |
+
chunks=(1, C, H, W),
|
| 34 |
+
fillvalue=0.0,
|
| 35 |
+
)
|
| 36 |
+
ds.attrs["variables"] = var_names
|
| 37 |
+
ds.attrs["time_step"] = dims["time_step"]
|
| 38 |
+
f.create_dataset("global_means", data=means)
|
| 39 |
+
f.create_dataset("global_stds", data=stds)
|
| 40 |
+
|
| 41 |
+
size_kb = os.path.getsize(path) / 1024
|
| 42 |
+
print(f" {year}.h5 shape=({T},{C},{H},{W}) "
|
| 43 |
+
f"logical={T*C*H*W*4/1024**3:.1f}GB actual={size_kb:.1f}KB")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def get_static(data_dir, var, name):
|
| 47 |
+
os.makedirs(data_dir, exist_ok=True)
|
| 48 |
+
ds = xr.Dataset(
|
| 49 |
+
data_vars={
|
| 50 |
+
f"{var}": (("valid_time", "latitude", "longitude"),
|
| 51 |
+
np.random.rand(1, 721, 1440).astype(np.float32))
|
| 52 |
+
},
|
| 53 |
+
coords={
|
| 54 |
+
"valid_time": ["2015-12-31"],
|
| 55 |
+
"latitude": np.linspace(90, -90, 721, dtype=np.float64),
|
| 56 |
+
"longitude": np.linspace(0, 359.75, 1440, dtype=np.float64),
|
| 57 |
+
"number": 0,
|
| 58 |
+
"expver": "",
|
| 59 |
+
},
|
| 60 |
+
attrs={
|
| 61 |
+
"GRIB_centre": "ecmf",
|
| 62 |
+
"GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts",
|
| 63 |
+
"GRIB_subCentre": "0",
|
| 64 |
+
"Conventions": "CF-1.7",
|
| 65 |
+
"institution": "European Centre for Medium-Range Weather Forecasts",
|
| 66 |
+
"history": "Generated manually",
|
| 67 |
+
}
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
ds.to_netcdf(f"{data_dir}/{name}.nc")
|
| 71 |
+
arr = np.random.randn(721, 1440).astype(np.float32)
|
| 72 |
+
np.save(f'{data_dir}/land_mask.npy', arr)
|
| 73 |
+
np.save(f'{data_dir}/soil_type.npy', arr)
|
| 74 |
+
np.save(f'{data_dir}/topography.npy', arr)
|
| 75 |
+
print(f"✅ Static data: {arr.shape}, dtype: {arr.dtype}, save to {data_dir}")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
cfg_datapipe = YParams("conf/config.yaml", "datapipe")
|
| 80 |
+
|
| 81 |
+
if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work2/"):
|
| 82 |
+
print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
|
| 83 |
+
exit()
|
| 84 |
+
|
| 85 |
+
years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
|
| 86 |
+
atm_vars = cfg_datapipe.dataset.channels
|
| 87 |
+
|
| 88 |
+
generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, DATASET_DIMS)
|
| 89 |
+
|
| 90 |
+
static_dir = os.path.join(cfg_datapipe.dataset.data_dir, "static")
|
| 91 |
+
get_static(static_dir, 'z', 'geopotential')
|
| 92 |
+
get_static(static_dir, 'lsm', 'land_sea_mask')
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
print("\n✅ Fake datasets generated.")
|
scripts/finetune.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# 获取项目根目录(train.py上级的上级)
|
| 5 |
+
root_path = Path(__file__).parent.parent
|
| 6 |
+
sys.path.append(str(root_path))
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch.distributed as dist
|
| 13 |
+
import logging
|
| 14 |
+
import time
|
| 15 |
+
|
| 16 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 17 |
+
from torch.optim.lr_scheduler import SequentialLR, LinearLR, CosineAnnealingLR, LambdaLR
|
| 18 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 19 |
+
from onescience.utils.YParams import YParams
|
| 20 |
+
from onescience.modules.utils.graphcast.data_utils import StaticData
|
| 21 |
+
from onescience.modules.utils.graphcast.graph_utils import deg2rad
|
| 22 |
+
from model.graph_cast_net import GraphCastNet
|
| 23 |
+
from onescience.modules.utils.graphcast.loss import GraphCastLossFunction
|
| 24 |
+
from apex import optimizers
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def main():
|
| 28 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 29 |
+
logger = logging.getLogger()
|
| 30 |
+
|
| 31 |
+
## Model config init
|
| 32 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 33 |
+
cfg = YParams(config_file_path, "model")
|
| 34 |
+
|
| 35 |
+
## Distributed config init
|
| 36 |
+
cfg.world_size = 1
|
| 37 |
+
if "WORLD_SIZE" in os.environ:
|
| 38 |
+
cfg.world_size = int(os.environ["WORLD_SIZE"])
|
| 39 |
+
world_rank = 0
|
| 40 |
+
local_rank = 0
|
| 41 |
+
if cfg.world_size > 1:
|
| 42 |
+
dist.init_process_group(backend="nccl", init_method="env://")
|
| 43 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 44 |
+
world_rank = dist.get_rank()
|
| 45 |
+
|
| 46 |
+
## DataLoader init
|
| 47 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 48 |
+
|
| 49 |
+
input_dim_grid_nodes = (len(cfg_data.dataset.channels) + cfg.use_cos_zenith + 4 * cfg.use_time_of_year_index) * (cfg.num_history + 1) + cfg.num_channels_static
|
| 50 |
+
|
| 51 |
+
model = GraphCastNet(mesh_level=cfg.mesh_level,
|
| 52 |
+
multimesh=cfg.multimesh,
|
| 53 |
+
input_res=tuple(cfg_data.dataset.img_size),
|
| 54 |
+
input_dim_grid_nodes=input_dim_grid_nodes,
|
| 55 |
+
input_dim_mesh_nodes=3,
|
| 56 |
+
input_dim_edges=4,
|
| 57 |
+
output_dim_grid_nodes=len(cfg_data.dataset.channels),
|
| 58 |
+
processor_type=cfg.processor_type,
|
| 59 |
+
khop_neighbors=cfg.khop_neighbors,
|
| 60 |
+
num_attention_heads=cfg.num_attention_heads,
|
| 61 |
+
processor_layers=cfg.processor_layers,
|
| 62 |
+
hidden_dim=cfg.hidden_dim,
|
| 63 |
+
norm_type=cfg.norm_type,
|
| 64 |
+
do_concat_trick=cfg.concat_trick,
|
| 65 |
+
recompute_activation=cfg.recompute_activation,
|
| 66 |
+
)
|
| 67 |
+
model_dtype = torch.bfloat16 if cfg.full_bf16 else torch.float32
|
| 68 |
+
model.set_checkpoint_encoder(cfg.checkpoint_encoder)
|
| 69 |
+
model.set_checkpoint_decoder(cfg.checkpoint_decoder)
|
| 70 |
+
model = model.to(dtype=model_dtype).to(local_rank)
|
| 71 |
+
if hasattr(model, "module"):
|
| 72 |
+
latitudes = model.module.latitudes
|
| 73 |
+
longitudes = model.module.longitudes
|
| 74 |
+
lat_lon_grid = model.module.lat_lon_grid
|
| 75 |
+
else:
|
| 76 |
+
latitudes = model.latitudes
|
| 77 |
+
longitudes = model.longitudes
|
| 78 |
+
lat_lon_grid = model.lat_lon_grid
|
| 79 |
+
tatic_dir = os.path.join(cfg_data.dataset.data_dir, "static")
|
| 80 |
+
|
| 81 |
+
static_data = StaticData(static_dir, latitudes, longitudes).get().to(device=local_rank)
|
| 82 |
+
channels_list = [i for i in range(len(cfg_data.dataset.channels))]
|
| 83 |
+
area = torch.abs(torch.cos(deg2rad(lat_lon_grid[:, :, 0])))
|
| 84 |
+
area /= torch.mean(area)
|
| 85 |
+
area = area.to(dtype=torch.bfloat16 if cfg.full_bf16 else torch.float32).to(device=local_rank)
|
| 86 |
+
criterion = GraphCastLossFunction(area, channels_list, cfg_data.dataset.dataset_metadata_path, cfg_data.dataset.time_diff_std_path)
|
| 87 |
+
optimizer = optimizers.FusedAdam(model.parameters(),
|
| 88 |
+
lr=cfg.lr, betas=(0.9, 0.95),
|
| 89 |
+
adam_w_mode=True,
|
| 90 |
+
weight_decay=0.1)
|
| 91 |
+
scheduler1 = LinearLR(optimizer, start_factor=1e-3, end_factor=1.0, total_iters=cfg.num_iters_step1, )
|
| 92 |
+
scheduler2 = CosineAnnealingLR(optimizer, T_max=cfg.num_iters_step2, eta_min=0.0)
|
| 93 |
+
scheduler3 = LambdaLR(optimizer, lr_lambda=lambda epoch: (cfg.lr_step3 / cfg.lr))
|
| 94 |
+
scheduler = SequentialLR(optimizer,
|
| 95 |
+
schedulers=[scheduler1, scheduler2, scheduler3],
|
| 96 |
+
milestones=[cfg.num_iters_step1, cfg.num_iters_step1 + cfg.num_iters_step2])
|
| 97 |
+
|
| 98 |
+
## Train process init
|
| 99 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 100 |
+
train_loss_file = f"{cfg.checkpoint_dir}/ft_trloss.npy"
|
| 101 |
+
best_valid_loss = 1.0e6
|
| 102 |
+
best_loss_epoch = 0
|
| 103 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 104 |
+
|
| 105 |
+
## Get model params count
|
| 106 |
+
if cfg.world_size == 1:
|
| 107 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 108 |
+
print("\n\n")
|
| 109 |
+
print("-" * 50)
|
| 110 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 111 |
+
print("-" * 50, "\n")
|
| 112 |
+
|
| 113 |
+
## Load model weight if there exist well-trained model
|
| 114 |
+
if not os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
|
| 115 |
+
print('⚠️ ⚠️ Please train the model first...')
|
| 116 |
+
exit()
|
| 117 |
+
|
| 118 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_finetune_bak.pth"):
|
| 119 |
+
if world_rank == 0:
|
| 120 |
+
print("\n\n")
|
| 121 |
+
print("-" * 50)
|
| 122 |
+
print(f"✅ There has a finetune-model weight, load and continue training...")
|
| 123 |
+
print(f'If you want to finetune a new model, ensure there is no model_finetune_bak.pth file in {cfg.checkpoint_dir}')
|
| 124 |
+
print("-" * 50, "\n")
|
| 125 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 126 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 127 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 128 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 129 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 130 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 131 |
+
train_losses = np.load(train_loss_file)
|
| 132 |
+
else:
|
| 133 |
+
if world_rank == 0:
|
| 134 |
+
print("\n\n")
|
| 135 |
+
print("-" * 50)
|
| 136 |
+
print(f"✅ Load well-trained model and continue to finetune...")
|
| 137 |
+
print(f'If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}')
|
| 138 |
+
print("-" * 50, "\n")
|
| 139 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 140 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 141 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 142 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 143 |
+
|
| 144 |
+
if cfg.world_size > 1:
|
| 145 |
+
model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)
|
| 146 |
+
if cfg.force_single_checkpoint_finetune:
|
| 147 |
+
if hasattr(model, "module"):
|
| 148 |
+
model.module.set_checkpoint_model(True)
|
| 149 |
+
else:
|
| 150 |
+
model.set_checkpoint_model(True)
|
| 151 |
+
if cfg.checkpoint_encoder_finetune:
|
| 152 |
+
if hasattr(model, "module"):
|
| 153 |
+
model.module.set_checkpoint_encoder(True)
|
| 154 |
+
else:
|
| 155 |
+
model.set_checkpoint_encoder(True)
|
| 156 |
+
if cfg.checkpoint_processor_finetune:
|
| 157 |
+
if hasattr(model, "module"):
|
| 158 |
+
model.module.set_checkpoint_processor(cfg.segments)
|
| 159 |
+
else:
|
| 160 |
+
model.set_checkpoint_encoder(True)
|
| 161 |
+
if cfg.checkpoint_decoder_finetune:
|
| 162 |
+
if hasattr(model, "module"):
|
| 163 |
+
model.module.set_checkpoint_decoder(True)
|
| 164 |
+
else:
|
| 165 |
+
model.set_checkpoint_encoder(True)
|
| 166 |
+
|
| 167 |
+
world_rank == 0 and logger.info(f"start finetuning ...")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
num_rollout_steps = 2
|
| 171 |
+
world_rank == 0 and logger.info(f"Switching to {num_rollout_steps}-step rollout!")
|
| 172 |
+
datapipe = ERA5Datapipe(
|
| 173 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 174 |
+
used_variables=cfg_data.dataset.channels,
|
| 175 |
+
used_years=cfg_data.dataset.train_time,
|
| 176 |
+
distributed=dist.is_initialized(),
|
| 177 |
+
output_steps=num_rollout_steps,
|
| 178 |
+
num_workers=0
|
| 179 |
+
)
|
| 180 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 181 |
+
datapipe = ERA5Datapipe(
|
| 182 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 183 |
+
used_variables=cfg_data.dataset.channels,
|
| 184 |
+
used_years=cfg_data.dataset.val_time,
|
| 185 |
+
distributed=dist.is_initialized(),
|
| 186 |
+
output_steps=num_rollout_steps,
|
| 187 |
+
num_workers=0
|
| 188 |
+
)
|
| 189 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 190 |
+
|
| 191 |
+
for epoch in range(cfg.num_iters_step3):
|
| 192 |
+
if epoch % cfg.step_change_freq == 0:
|
| 193 |
+
num_rollout_steps = epoch // cfg.step_change_freq + 2
|
| 194 |
+
world_rank == 0 and logger.info(f"Switching to {num_rollout_steps}-step rollout!")
|
| 195 |
+
datapipe = ERA5Datapipe(
|
| 196 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 197 |
+
used_variables=cfg_data.dataset.channels,
|
| 198 |
+
used_years=cfg_data.dataset.train_time,
|
| 199 |
+
distributed=dist.is_initialized(),
|
| 200 |
+
output_steps=num_rollout_steps,
|
| 201 |
+
num_workers=0
|
| 202 |
+
)
|
| 203 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 204 |
+
|
| 205 |
+
if dist.is_initialized():
|
| 206 |
+
train_sampler.set_epoch(epoch)
|
| 207 |
+
val_sampler.set_epoch(epoch)
|
| 208 |
+
train_loss = 0
|
| 209 |
+
start_time = time.time()
|
| 210 |
+
for j, data in enumerate(train_dataloader):
|
| 211 |
+
invar = data[0].to(device=local_rank)
|
| 212 |
+
|
| 213 |
+
outvar = data[1].to(device=local_rank)
|
| 214 |
+
cos_zenith = data[2].to(device=local_rank)
|
| 215 |
+
in_idx = data[3].item()
|
| 216 |
+
cos_zenith = torch.squeeze(cos_zenith, dim=2)
|
| 217 |
+
cos_zenith = torch.clamp(cos_zenith, min=0.0) - 1.0 / torch.pi
|
| 218 |
+
outvar = outvar.to(dtype=model_dtype)
|
| 219 |
+
for t in range(outvar.shape[1]):
|
| 220 |
+
day_of_year, time_of_day = divmod(in_idx + t * cfg.dt, 24 // cfg.dt)
|
| 221 |
+
normalized_day_of_year = torch.tensor((day_of_year / 365) * (np.pi / 2), dtype=torch.float32, device=local_rank)
|
| 222 |
+
normalized_time_of_day = torch.tensor((time_of_day / (24 - cfg.dt)) * (np.pi / 2), dtype=torch.float32, device=local_rank)
|
| 223 |
+
sin_day_of_year = torch.sin(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 224 |
+
cos_day_of_year = torch.cos(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 225 |
+
sin_time_of_day = torch.sin(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 226 |
+
cos_time_of_day = torch.cos(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 227 |
+
invar = torch.concat((invar, cos_zenith[:, t:t + 1, :, :], static_data, sin_day_of_year, cos_day_of_year, sin_time_of_day, cos_time_of_day), dim=1)
|
| 228 |
+
invar = invar.to(dtype=model_dtype)
|
| 229 |
+
if t < outvar.shape[1] - 1:
|
| 230 |
+
with torch.no_grad():
|
| 231 |
+
outpred = model(invar)
|
| 232 |
+
invar = outpred.detach()
|
| 233 |
+
else:
|
| 234 |
+
outpred = model(invar)
|
| 235 |
+
loss = criterion(outpred, outvar[:, t])
|
| 236 |
+
optimizer.zero_grad()
|
| 237 |
+
loss.backward()
|
| 238 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip_norm)
|
| 239 |
+
torch.cuda.nvtx.range_pop()
|
| 240 |
+
optimizer.step()
|
| 241 |
+
scheduler.step()
|
| 242 |
+
train_loss += loss.item()
|
| 243 |
+
if world_rank == 0:
|
| 244 |
+
logger.info(f'Train: Epoch {epoch} {j+1}/{len(train_dataloader)} '
|
| 245 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 246 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 247 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 248 |
+
|
| 249 |
+
if (j + 1) % cfg.val_freq == 0:
|
| 250 |
+
model.eval()
|
| 251 |
+
valid_loss = 0.0
|
| 252 |
+
with torch.no_grad():
|
| 253 |
+
start_time = time.time()
|
| 254 |
+
for k, data in enumerate(val_dataloader):
|
| 255 |
+
invar = data[0].to(device=local_rank)
|
| 256 |
+
outvar = data[1].to(device=local_rank)
|
| 257 |
+
cos_zenith = data[2].to(device=local_rank)
|
| 258 |
+
in_idx = data[3].item()
|
| 259 |
+
|
| 260 |
+
cos_zenith = torch.squeeze(cos_zenith, dim=2)
|
| 261 |
+
cos_zenith = torch.clamp(cos_zenith, min=0.0) - 1.0 / torch.pi # [b, 2, h, w]
|
| 262 |
+
outvar = outvar.to(dtype=model_dtype)
|
| 263 |
+
loss = 0.0
|
| 264 |
+
for t in range(outvar.shape[1]):
|
| 265 |
+
day_of_year, time_of_day = divmod(in_idx + t * cfg.dt, 24 // cfg.dt)
|
| 266 |
+
normalized_day_of_year = torch.tensor((day_of_year / 365) * (np.pi / 2), dtype=torch.float32, device=local_rank)
|
| 267 |
+
normalized_time_of_day = torch.tensor((time_of_day / (24 - cfg.dt)) * (np.pi / 2), dtype=torch.float32, device=local_rank)
|
| 268 |
+
sin_day_of_year = torch.sin(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 269 |
+
cos_day_of_year = torch.cos(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 270 |
+
sin_time_of_day = torch.sin(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 271 |
+
cos_time_of_day = torch.cos(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 272 |
+
invar = torch.concat((invar, cos_zenith[:, t:t + 1, :, :], static_data, sin_day_of_year, cos_day_of_year, sin_time_of_day, cos_time_of_day), dim=1)
|
| 273 |
+
invar = invar.to(dtype=model_dtype)
|
| 274 |
+
outpred = model(invar)
|
| 275 |
+
invar = outpred
|
| 276 |
+
loss += criterion(outpred, outvar[:, t])
|
| 277 |
+
|
| 278 |
+
loss /= outvar.shape[1]
|
| 279 |
+
if cfg.world_size > 1:
|
| 280 |
+
loss_tensor = loss.detach().to(local_rank) # torch.tensor(loss, device=local_rank)
|
| 281 |
+
dist.all_reduce(loss_tensor)
|
| 282 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 283 |
+
valid_loss += loss
|
| 284 |
+
else:
|
| 285 |
+
valid_loss += loss.item()
|
| 286 |
+
|
| 287 |
+
if world_rank == 0:
|
| 288 |
+
logger.info(f'Valid: Epoch {epoch} {k+1}/{len(val_dataloader)} '
|
| 289 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 290 |
+
f'[{(time.time()-start_time)/(k+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 291 |
+
f'loss:{valid_loss / (k+1): .04f}')
|
| 292 |
+
|
| 293 |
+
valid_loss /= len(val_dataloader)
|
| 294 |
+
is_save_ckp = False
|
| 295 |
+
if valid_loss < best_valid_loss:
|
| 296 |
+
best_valid_loss = valid_loss
|
| 297 |
+
best_loss_epoch = epoch
|
| 298 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir)
|
| 299 |
+
is_save_ckp = True
|
| 300 |
+
train_loss /= (j+1)
|
| 301 |
+
if world_rank == 0:
|
| 302 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 303 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 304 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 305 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 306 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 307 |
+
)
|
| 308 |
+
train_losses = np.append(train_losses, train_loss)
|
| 309 |
+
np.save(train_loss_file, train_losses)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
print(f'Graphcast has been well-trained, next step is fine-tune')
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path):
|
| 316 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 317 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 318 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 319 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 320 |
+
"best_valid_loss": best_valid_loss,
|
| 321 |
+
"best_loss_epoch": best_loss_epoch,
|
| 322 |
+
}
|
| 323 |
+
torch.save(state, f"{model_path}/model_finetune.pth")
|
| 324 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 325 |
+
os.system(f"mv {model_path}/model_finetune.pth {model_path}/model_finetune_bak.pth")
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
if __name__ == "__main__":
|
| 329 |
+
current_path = os.getcwd()
|
| 330 |
+
sys.path.append(current_path)
|
| 331 |
+
main()
|
scripts/get_data_json.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from onescience.utils.YParams import YParams
|
| 3 |
+
import json
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main():
|
| 8 |
+
config_file_path = os.path.join(current_path, 'conf/config.yaml')
|
| 9 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 10 |
+
metadata = {
|
| 11 |
+
"coords": {
|
| 12 |
+
"channel": {str(i): name for i, name in enumerate(cfg_data.dataset.channels)}
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
with open('./data.json', "w") as f:
|
| 16 |
+
json.dump(metadata, f, indent=4)
|
| 17 |
+
print(f"Metadata saved.")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == '__main__':
|
| 22 |
+
current_path = os.getcwd()
|
| 23 |
+
sys.path.append(current_path)
|
| 24 |
+
main()
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# 获取项目根目录(train.py上级的上级)
|
| 5 |
+
root_path = Path(__file__).parent.parent
|
| 6 |
+
sys.path.append(str(root_path))
|
| 7 |
+
import os
|
| 8 |
+
import numpy as np
|
| 9 |
+
import glob
|
| 10 |
+
import torch
|
| 11 |
+
import sys
|
| 12 |
+
import h5py
|
| 13 |
+
from tqdm import tqdm
|
| 14 |
+
|
| 15 |
+
from onescience.utils.YParams import YParams
|
| 16 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 17 |
+
from ruamel.yaml.scalarfloat import ScalarFloat
|
| 18 |
+
from onescience.modules.utils.graphcast.data_utils import StaticData
|
| 19 |
+
from onescience.modules.utils.graphcast.graph_utils import deg2rad
|
| 20 |
+
from model.graph_cast_net import GraphCastNet
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
torch.serialization.add_safe_globals([ScalarFloat])
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def get_stats(data_dir, channels):
|
| 27 |
+
"""从新版 h5 中读取变量列表与归一化参数(均值/标准差)"""
|
| 28 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 29 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 30 |
+
ds = f["fields"]
|
| 31 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 32 |
+
mu = f["global_means"][:] # [1, C, 1, 1]
|
| 33 |
+
std = f["global_stds"][:]
|
| 34 |
+
|
| 35 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 36 |
+
means = mu[:, channel_indices, :, :]
|
| 37 |
+
stds = std[:, channel_indices, :, :]
|
| 38 |
+
return means, stds
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
if __name__ == "__main__":
|
| 42 |
+
current_path = os.getcwd()
|
| 43 |
+
sys.path.append(current_path)
|
| 44 |
+
|
| 45 |
+
## Model config init
|
| 46 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 47 |
+
cfg = YParams(config_file_path, "model")
|
| 48 |
+
## DataLoader init
|
| 49 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 50 |
+
|
| 51 |
+
datapipe = ERA5Datapipe(
|
| 52 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 53 |
+
used_variables=cfg_data.dataset.channels,
|
| 54 |
+
used_years=cfg_data.dataset.test_time,
|
| 55 |
+
distributed=False,
|
| 56 |
+
batch_size=1,
|
| 57 |
+
num_workers=4,
|
| 58 |
+
)
|
| 59 |
+
test_dataloader, _ = datapipe.get_dataloader("test")
|
| 60 |
+
means, stds = get_stats(cfg_data.dataset.data_dir, cfg_data.dataset.channels)
|
| 61 |
+
|
| 62 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_finetune_bak.pth", map_location="cuda:0", weights_only=True)
|
| 63 |
+
model_dtype = torch.bfloat16 if cfg.full_bf16 else torch.float32
|
| 64 |
+
input_dim_grid_nodes = (len(cfg_data.dataset.channels) + cfg.use_cos_zenith + 4 * cfg.use_time_of_year_index) * (cfg.num_history + 1) + cfg.num_channels_static
|
| 65 |
+
model = GraphCastNet(mesh_level=cfg.mesh_level,
|
| 66 |
+
multimesh=cfg.multimesh,
|
| 67 |
+
input_res=tuple(cfg_data.dataset.img_size),
|
| 68 |
+
input_dim_grid_nodes=input_dim_grid_nodes,
|
| 69 |
+
input_dim_mesh_nodes=3,
|
| 70 |
+
input_dim_edges=4,
|
| 71 |
+
output_dim_grid_nodes=len(cfg_data.dataset.channels),
|
| 72 |
+
processor_type=cfg.processor_type,
|
| 73 |
+
khop_neighbors=cfg.khop_neighbors,
|
| 74 |
+
num_attention_heads=cfg.num_attention_heads,
|
| 75 |
+
processor_layers=cfg.processor_layers,
|
| 76 |
+
hidden_dim=cfg.hidden_dim,
|
| 77 |
+
norm_type=cfg.norm_type,
|
| 78 |
+
do_concat_trick=cfg.concat_trick,
|
| 79 |
+
recompute_activation=cfg.recompute_activation,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
model.set_checkpoint_encoder(cfg.checkpoint_encoder)
|
| 83 |
+
model.set_checkpoint_decoder(cfg.checkpoint_decoder)
|
| 84 |
+
model = model.to(dtype=model_dtype).to("cuda:0")
|
| 85 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 86 |
+
|
| 87 |
+
if hasattr(model, "module"):
|
| 88 |
+
latitudes = model.module.latitudes
|
| 89 |
+
longitudes = model.module.longitudes
|
| 90 |
+
else:
|
| 91 |
+
latitudes = model.latitudes
|
| 92 |
+
longitudes = model.longitudes
|
| 93 |
+
static_dir = os.path.join(cfg_data.dataset.data_dir, "static")
|
| 94 |
+
|
| 95 |
+
static_data = StaticData(static_dir, latitudes, longitudes).get().to(device=local_rank)
|
| 96 |
+
model.eval()
|
| 97 |
+
os.makedirs('./result/output/', exist_ok=True)
|
| 98 |
+
print(f"📂 samples will be generated to './result/output/'")
|
| 99 |
+
with torch.no_grad():
|
| 100 |
+
for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
|
| 101 |
+
invar = data[0].to(device="cuda:0")
|
| 102 |
+
cos_zenith = data[2].to(device="cuda:0")
|
| 103 |
+
in_idx = data[3].item()
|
| 104 |
+
filename = data[4][-1][0]
|
| 105 |
+
|
| 106 |
+
cos_zenith = torch.squeeze(cos_zenith, dim=2)
|
| 107 |
+
cos_zenith = torch.clamp(cos_zenith, min=0.0) - 1.0 / torch.pi
|
| 108 |
+
day_of_year, time_of_day = divmod(in_idx * cfg.dt, 24)
|
| 109 |
+
normalized_day_of_year = torch.tensor((day_of_year / 365) * (np.pi / 2), dtype=torch.float32, device="cuda:0")
|
| 110 |
+
normalized_time_of_day = torch.tensor((time_of_day / (24 - cfg.dt)) * (np.pi / 2), dtype=torch.float32, device="cuda:0")
|
| 111 |
+
sin_day_of_year = torch.sin(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 112 |
+
cos_day_of_year = torch.cos(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 113 |
+
sin_time_of_day = torch.sin(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 114 |
+
cos_time_of_day = torch.cos(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 115 |
+
invar = torch.concat((invar, cos_zenith, static_data, sin_day_of_year, cos_day_of_year, sin_time_of_day, cos_time_of_day), dim=1)
|
| 116 |
+
|
| 117 |
+
invar = invar.to(dtype=model_dtype)
|
| 118 |
+
pred_var = model(invar).to(dtype=torch.float32)
|
| 119 |
+
pred_var = pred_var.cpu().numpy()
|
| 120 |
+
pred_var = pred_var * stds + means
|
| 121 |
+
|
| 122 |
+
np.save(f"result/output/{filename}.npy", pred_var)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
import glob
|
| 6 |
+
import h5py
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
+
from onescience.utils.fcn.YParams import YParams
|
| 10 |
+
from matplotlib import rcParams
|
| 11 |
+
|
| 12 |
+
# rcParams['font.family'] = 'serif'
|
| 13 |
+
# rcParams['font.serif'] = ['DejaVu Serif']
|
| 14 |
+
rcParams['mathtext.fontset'] = 'stix'
|
| 15 |
+
rcParams['axes.linewidth'] = 0.9
|
| 16 |
+
rcParams['xtick.major.width'] = 0.9
|
| 17 |
+
rcParams['ytick.major.width'] = 0.9
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_metadata(data_dir, channels):
|
| 21 |
+
"""从新版 h5 attrs 中读取变量列表和 time_step"""
|
| 22 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 23 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 24 |
+
ds = f["fields"]
|
| 25 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 26 |
+
time_step = int(ds.attrs["time_step"])
|
| 27 |
+
|
| 28 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 29 |
+
|
| 30 |
+
total_files = [f for f in os.listdir('./result/output/') if f.endswith('.npy')]
|
| 31 |
+
total_files.sort()
|
| 32 |
+
return total_files, channel_indices, time_step
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def filename_to_index(filename, time_step):
|
| 36 |
+
"""将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
|
| 37 |
+
dt = datetime.strptime(filename, "%Y%m%d%H")
|
| 38 |
+
year_start = datetime(dt.year, 1, 1)
|
| 39 |
+
hours = (dt - year_start).total_seconds() / 3600
|
| 40 |
+
return int(hours / time_step)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def get_result(total_files, channel_indices, time_step, data_dir, clim_mean):
|
| 44 |
+
channel_rmse = np.zeros(len(channel_indices))
|
| 45 |
+
channel_acc = np.zeros(len(channel_indices))
|
| 46 |
+
clim_mean = clim_mean[0, :, :, :]
|
| 47 |
+
if not os.path.exists('result/rmse.npy') or not os.path.exists('result/acc.npy'):
|
| 48 |
+
numerator = np.zeros(len(channel_indices))
|
| 49 |
+
pred_sq_sum = np.zeros(len(channel_indices))
|
| 50 |
+
label_sq_sum = np.zeros(len(channel_indices))
|
| 51 |
+
for file in tqdm(total_files, unit="files"):
|
| 52 |
+
fname = file[:-4] # 去掉 .npy
|
| 53 |
+
year = fname[:4]
|
| 54 |
+
t_idx = filename_to_index(fname, time_step)
|
| 55 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 56 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 57 |
+
label = label[channel_indices]
|
| 58 |
+
pred = np.load(f'result/output/{file}').squeeze()
|
| 59 |
+
|
| 60 |
+
label_anom = label - clim_mean
|
| 61 |
+
pred_anom = pred - clim_mean
|
| 62 |
+
# 累加
|
| 63 |
+
numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
|
| 64 |
+
pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
|
| 65 |
+
label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
|
| 66 |
+
|
| 67 |
+
channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
|
| 68 |
+
channel_rmse /= len(total_files)
|
| 69 |
+
channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
|
| 70 |
+
np.save('./result/acc.npy', channel_acc)
|
| 71 |
+
np.save('result/rmse.npy', channel_rmse)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def show_result():
|
| 75 |
+
channel_rmse = np.load('result/rmse.npy')
|
| 76 |
+
channel_acc = np.load('./result/acc.npy')
|
| 77 |
+
|
| 78 |
+
channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
|
| 79 |
+
w = 24 # 最长 channel 名宽度
|
| 80 |
+
|
| 81 |
+
# 表头
|
| 82 |
+
print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
|
| 83 |
+
print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
|
| 84 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 85 |
+
# 数据行
|
| 86 |
+
for i, ch in enumerate(channels):
|
| 87 |
+
print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
|
| 88 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 89 |
+
print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
|
| 90 |
+
print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def plot(label, pred, var, filename):
|
| 94 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
| 95 |
+
|
| 96 |
+
xtick_labels = ['180°W', '90°W', '0°', '90°E', '180°E']
|
| 97 |
+
ytick_labels = ['90°S', '45°S', '0°', '45°N', '90°N']
|
| 98 |
+
xticks = np.linspace(0, label.shape[-1] - 1, 5)
|
| 99 |
+
yticks = np.linspace(0, label.shape[-2] - 1, 5)
|
| 100 |
+
|
| 101 |
+
vmin = min(label.min(), pred.min())
|
| 102 |
+
vmax = max(label.max(), pred.max())
|
| 103 |
+
|
| 104 |
+
diff = label - pred
|
| 105 |
+
rmse = np.sqrt(np.mean(diff ** 2))
|
| 106 |
+
diff_abs_max = np.abs(diff).max()
|
| 107 |
+
|
| 108 |
+
plot_configs = [
|
| 109 |
+
{'data': label, 'title': 'Truth', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 110 |
+
{'data': pred, 'title': 'Prediction', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 111 |
+
{'data': diff, 'title': f'Difference (RMSE={rmse:.2f})', 'cmap': 'RdBu_r', 'vmin': -diff_abs_max, 'vmax': diff_abs_max},
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
for ax, cfg in zip(axes, plot_configs):
|
| 115 |
+
im = ax.imshow(cfg['data'], cmap=cfg['cmap'], vmin=cfg['vmin'], vmax=cfg['vmax'])
|
| 116 |
+
ax.set_title(cfg['title'], fontsize=12, pad=4)
|
| 117 |
+
ax.set_xlabel('Longitude')
|
| 118 |
+
ax.set_ylabel('Latitude')
|
| 119 |
+
ax.set_xticks(xticks)
|
| 120 |
+
ax.set_xticklabels(xtick_labels)
|
| 121 |
+
ax.set_yticks(yticks)
|
| 122 |
+
ax.set_yticklabels(ytick_labels)
|
| 123 |
+
plt.colorbar(im, ax=ax, orientation='horizontal')
|
| 124 |
+
|
| 125 |
+
fig.suptitle(var, fontsize=14, fontweight='bold', y=0.98)
|
| 126 |
+
plt.savefig(filename, dpi=300, bbox_inches='tight')
|
| 127 |
+
plt.close()
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def plot_loss(train_loss):
|
| 131 |
+
mask = ~(np.isnan(train_loss))
|
| 132 |
+
train_loss = train_loss[mask]
|
| 133 |
+
|
| 134 |
+
fig, ax = plt.subplots(figsize=(5, 3.5))
|
| 135 |
+
colors = {'train': '#2563EB', 'valid': '#EA580C'}
|
| 136 |
+
epochs = np.arange(1, len(train_loss) + 1)
|
| 137 |
+
|
| 138 |
+
ax.plot(epochs, train_loss, color=colors['train'], linewidth=1.5, label='Train')
|
| 139 |
+
min_idx = np.argmin(train_loss)
|
| 140 |
+
ax.scatter(epochs[min_idx], train_loss[min_idx],
|
| 141 |
+
color=colors['valid'], s=40, zorder=5, edgecolors='white')
|
| 142 |
+
ax.annotate(f'Best: {train_loss[min_idx]:.3f}',
|
| 143 |
+
xy=(epochs[min_idx], train_loss[min_idx]),
|
| 144 |
+
xytext=(10, 10), textcoords='offset points', fontsize=8, color=colors['valid'],
|
| 145 |
+
arrowprops=dict(arrowstyle='-', color=colors['valid'], lw=0.5))
|
| 146 |
+
|
| 147 |
+
ax.set(xlabel='Epoch', ylabel='Loss', xlim=(0, len(train_loss) + 1))
|
| 148 |
+
ax.legend(frameon=False, loc='upper right')
|
| 149 |
+
ax.grid(True, linestyle='--', alpha=0.3)
|
| 150 |
+
ax.spines[['top', 'right']].set_visible(False)
|
| 151 |
+
|
| 152 |
+
plt.tight_layout()
|
| 153 |
+
plt.savefig('./result/loss.png', dpi=300, bbox_inches='tight')
|
| 154 |
+
plt.close()
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
if __name__ == "__main__":
|
| 158 |
+
current_path = os.getcwd()
|
| 159 |
+
sys.path.append(current_path)
|
| 160 |
+
config_file_path = os.path.join(current_path, 'conf/config.yaml')
|
| 161 |
+
cfg = YParams(config_file_path, 'model')
|
| 162 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 163 |
+
|
| 164 |
+
train_loss = np.load('./data/checkpoints/trloss.npy')
|
| 165 |
+
plot_loss(train_loss)
|
| 166 |
+
|
| 167 |
+
data_dir = cfg_data.dataset.data_dir
|
| 168 |
+
total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels)
|
| 169 |
+
|
| 170 |
+
# Load data & Compute RMSE/ACC per channel
|
| 171 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 172 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 173 |
+
mu = f["global_means"][:]
|
| 174 |
+
clim_mean = mu[:, channel_indices, :, :]
|
| 175 |
+
get_result(total_files, channel_indices, time_step, data_dir, clim_mean)
|
| 176 |
+
show_result()
|
| 177 |
+
|
| 178 |
+
##### 默认绘制 test_time 第一年的第一个时间步,用户可自行指定日期和变量 #####
|
| 179 |
+
test_year = cfg_data.dataset.test_time[0]
|
| 180 |
+
eg_files = [f'{test_year}010206']
|
| 181 |
+
channel_index = [cfg_data.dataset.channels.index(v) for v in ['2m_temperature', 'geopotential_500', 'temperature_500']]
|
| 182 |
+
|
| 183 |
+
selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
|
| 184 |
+
print(f"seleted date: {eg_files}")
|
| 185 |
+
print(f"selected channels: {selected_var}")
|
| 186 |
+
for file in eg_files:
|
| 187 |
+
year = file[:4]
|
| 188 |
+
t_idx = filename_to_index(file, time_step)
|
| 189 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 190 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 191 |
+
label = label[channel_indices]
|
| 192 |
+
pred = np.load(f'result/output/{file}.npy').squeeze()
|
| 193 |
+
for i in range(len(selected_var)):
|
| 194 |
+
filename = f'./result/{file}_{selected_var[i]}.png'
|
| 195 |
+
plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
|
| 196 |
+
print(f'✅plot {filename}')
|
scripts/train.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# 获取项目根目录(train.py上级的上级)
|
| 5 |
+
root_path = Path(__file__).parent.parent
|
| 6 |
+
sys.path.append(str(root_path))
|
| 7 |
+
import torch
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch.distributed as dist
|
| 12 |
+
import logging
|
| 13 |
+
import time
|
| 14 |
+
|
| 15 |
+
from torch.nn.parallel import DistributedDataParallel
|
| 16 |
+
from torch.optim.lr_scheduler import SequentialLR, LinearLR, CosineAnnealingLR, LambdaLR
|
| 17 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 18 |
+
from onescience.utils.YParams import YParams
|
| 19 |
+
from onescience.modules.utils.graphcast.data_utils import StaticData
|
| 20 |
+
from onescience.modules.utils.graphcast.graph_utils import deg2rad
|
| 21 |
+
from model.graph_cast_net import GraphCastNet
|
| 22 |
+
from onescience.modules.utils.graphcast.loss import GraphCastLossFunction
|
| 23 |
+
from apex import optimizers
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def main():
|
| 27 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 28 |
+
logger = logging.getLogger()
|
| 29 |
+
|
| 30 |
+
## Model config init
|
| 31 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 32 |
+
cfg = YParams(config_file_path, "model")
|
| 33 |
+
|
| 34 |
+
## Distributed config init
|
| 35 |
+
cfg.world_size = 1
|
| 36 |
+
if "WORLD_SIZE" in os.environ:
|
| 37 |
+
cfg.world_size = int(os.environ["WORLD_SIZE"])
|
| 38 |
+
world_rank = 0
|
| 39 |
+
local_rank = 0
|
| 40 |
+
if cfg.world_size > 1:
|
| 41 |
+
dist.init_process_group(backend="nccl", init_method="env://")
|
| 42 |
+
local_rank = int(os.environ["LOCAL_RANK"])
|
| 43 |
+
world_rank = dist.get_rank()
|
| 44 |
+
|
| 45 |
+
## DataLoader init
|
| 46 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 47 |
+
datapipe = ERA5Datapipe(
|
| 48 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 49 |
+
used_variables=cfg_data.dataset.channels,
|
| 50 |
+
used_years=cfg_data.dataset.train_time,
|
| 51 |
+
distributed=dist.is_initialized(),
|
| 52 |
+
num_workers=0
|
| 53 |
+
)
|
| 54 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 55 |
+
datapipe = ERA5Datapipe(
|
| 56 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 57 |
+
used_variables=cfg_data.dataset.channels,
|
| 58 |
+
used_years=cfg_data.dataset.val_time,
|
| 59 |
+
distributed=dist.is_initialized(),
|
| 60 |
+
num_workers=0
|
| 61 |
+
)
|
| 62 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 63 |
+
|
| 64 |
+
input_dim_grid_nodes = (len(cfg_data.dataset.channels) + cfg.use_cos_zenith + 4 * cfg.use_time_of_year_index) * (cfg.num_history + 1) + cfg.num_channels_static
|
| 65 |
+
model = GraphCastNet(mesh_level=cfg.mesh_level,
|
| 66 |
+
multimesh=cfg.multimesh,
|
| 67 |
+
input_res=tuple(cfg_data.dataset.img_size),
|
| 68 |
+
input_dim_grid_nodes=input_dim_grid_nodes,
|
| 69 |
+
input_dim_mesh_nodes=3,
|
| 70 |
+
input_dim_edges=4,
|
| 71 |
+
output_dim_grid_nodes=len(cfg_data.dataset.channels),
|
| 72 |
+
processor_type=cfg.processor_type,
|
| 73 |
+
khop_neighbors=cfg.khop_neighbors,
|
| 74 |
+
num_attention_heads=cfg.num_attention_heads,
|
| 75 |
+
processor_layers=cfg.processor_layers,
|
| 76 |
+
hidden_dim=cfg.hidden_dim,
|
| 77 |
+
norm_type=cfg.norm_type,
|
| 78 |
+
do_concat_trick=cfg.concat_trick,
|
| 79 |
+
recompute_activation=cfg.recompute_activation,
|
| 80 |
+
)
|
| 81 |
+
model_dtype = torch.bfloat16 if cfg.full_bf16 else torch.float32
|
| 82 |
+
model.set_checkpoint_encoder(cfg.checkpoint_encoder)
|
| 83 |
+
model.set_checkpoint_decoder(cfg.checkpoint_decoder)
|
| 84 |
+
model = model.to(dtype=model_dtype).to(local_rank)
|
| 85 |
+
if hasattr(model, "module"):
|
| 86 |
+
latitudes = model.module.latitudes
|
| 87 |
+
longitudes = model.module.longitudes
|
| 88 |
+
lat_lon_grid = model.module.lat_lon_grid
|
| 89 |
+
else:
|
| 90 |
+
latitudes = model.latitudes
|
| 91 |
+
longitudes = model.longitudes
|
| 92 |
+
lat_lon_grid = model.lat_lon_grid
|
| 93 |
+
|
| 94 |
+
static_dir = os.path.join(cfg_data.dataset.data_dir, "static")
|
| 95 |
+
|
| 96 |
+
static_data = StaticData(static_dir, latitudes, longitudes).get().to(device=local_rank)
|
| 97 |
+
channels_list = [i for i in range(len(cfg_data.dataset.channels))]
|
| 98 |
+
area = torch.abs(torch.cos(deg2rad(lat_lon_grid[:, :, 0])))
|
| 99 |
+
area /= torch.mean(area)
|
| 100 |
+
area = area.to(dtype=torch.bfloat16 if cfg.full_bf16 else torch.float32).to(device=local_rank)
|
| 101 |
+
criterion = GraphCastLossFunction(area, channels_list, cfg_data.dataset.dataset_metadata_path, cfg_data.dataset.time_diff_std_path)
|
| 102 |
+
optimizer = optimizers.FusedAdam(model.parameters(),
|
| 103 |
+
lr=cfg.lr, betas=(0.9, 0.95),
|
| 104 |
+
adam_w_mode=True,
|
| 105 |
+
weight_decay=0.1)
|
| 106 |
+
scheduler1 = LinearLR(optimizer, start_factor=1e-3, end_factor=1.0, total_iters=cfg.num_iters_step1, )
|
| 107 |
+
scheduler2 = CosineAnnealingLR(optimizer, T_max=cfg.num_iters_step2, eta_min=0.0)
|
| 108 |
+
scheduler3 = LambdaLR(optimizer, lr_lambda=lambda epoch: (cfg.lr_step3 / cfg.lr))
|
| 109 |
+
scheduler = SequentialLR(optimizer,
|
| 110 |
+
schedulers=[scheduler1, scheduler2, scheduler3],
|
| 111 |
+
milestones=[cfg.num_iters_step1, cfg.num_iters_step1 + cfg.num_iters_step2])
|
| 112 |
+
|
| 113 |
+
## Train process init
|
| 114 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 115 |
+
train_loss_file = f"{cfg.checkpoint_dir}/trloss.npy"
|
| 116 |
+
best_valid_loss = 1.0e6
|
| 117 |
+
best_loss_epoch = 0
|
| 118 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 119 |
+
|
| 120 |
+
## Get model params count
|
| 121 |
+
if cfg.world_size == 1:
|
| 122 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 123 |
+
print("\n\n")
|
| 124 |
+
print("-" * 50)
|
| 125 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 126 |
+
print("-" * 50, "\n")
|
| 127 |
+
|
| 128 |
+
## Load model weight if there exist well-trained model
|
| 129 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
|
| 130 |
+
if world_rank == 0:
|
| 131 |
+
print("\n\n")
|
| 132 |
+
print("-" * 50)
|
| 133 |
+
print(f"✅ There has a model weight, load and continue training...")
|
| 134 |
+
print(f'If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}')
|
| 135 |
+
print("-" * 50, "\n")
|
| 136 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
|
| 137 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 138 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 139 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 140 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 141 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 142 |
+
train_losses = np.load(train_loss_file)
|
| 143 |
+
|
| 144 |
+
## Distributed model
|
| 145 |
+
if cfg.world_size > 1:
|
| 146 |
+
model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)
|
| 147 |
+
|
| 148 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 149 |
+
|
| 150 |
+
for epoch in range(cfg.max_epoch):
|
| 151 |
+
if dist.is_initialized():
|
| 152 |
+
train_sampler.set_epoch(epoch)
|
| 153 |
+
val_sampler.set_epoch(epoch)
|
| 154 |
+
|
| 155 |
+
model.train()
|
| 156 |
+
train_loss = 0
|
| 157 |
+
start_time = time.time()
|
| 158 |
+
for j, data in enumerate(train_dataloader):
|
| 159 |
+
invar = data[0].to(device=local_rank)
|
| 160 |
+
outvar = data[1].to(device=local_rank)
|
| 161 |
+
cos_zenith = data[2].to(device=local_rank)
|
| 162 |
+
in_idx = data[3].item()
|
| 163 |
+
cos_zenith = torch.squeeze(cos_zenith, dim=2)
|
| 164 |
+
cos_zenith = torch.clamp(cos_zenith, min=0.0) - 1.0 / torch.pi
|
| 165 |
+
day_of_year, time_of_day = divmod(in_idx * cfg.dt, 24)
|
| 166 |
+
normalized_day_of_year = torch.tensor((day_of_year / 365) * (np.pi / 2), dtype=torch.float32, device=local_rank)
|
| 167 |
+
normalized_time_of_day = torch.tensor((time_of_day / (24 - cfg.dt)) * (np.pi / 2), dtype=torch.float32, device=local_rank)
|
| 168 |
+
sin_day_of_year = torch.sin(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 169 |
+
cos_day_of_year = torch.cos(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 170 |
+
sin_time_of_day = torch.sin(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 171 |
+
cos_time_of_day = torch.cos(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 172 |
+
invar = torch.concat((invar, cos_zenith, static_data, sin_day_of_year, cos_day_of_year, sin_time_of_day, cos_time_of_day), dim=1)
|
| 173 |
+
invar, outvar = invar.to(dtype=model_dtype), outvar.to(dtype=model_dtype)
|
| 174 |
+
outvar_pred = model(invar)
|
| 175 |
+
loss = criterion(outvar_pred, outvar)
|
| 176 |
+
optimizer.zero_grad()
|
| 177 |
+
loss.backward()
|
| 178 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip_norm)
|
| 179 |
+
torch.cuda.nvtx.range_pop()
|
| 180 |
+
optimizer.step()
|
| 181 |
+
scheduler.step()
|
| 182 |
+
train_loss += loss.item()
|
| 183 |
+
|
| 184 |
+
if world_rank == 0:
|
| 185 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 186 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 187 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 188 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 189 |
+
|
| 190 |
+
if (j + 1) % cfg.val_freq == 0:
|
| 191 |
+
model.eval()
|
| 192 |
+
valid_loss = 0.0
|
| 193 |
+
with torch.no_grad():
|
| 194 |
+
start_time = time.time()
|
| 195 |
+
for k, data in enumerate(val_dataloader):
|
| 196 |
+
invar = data[0].to(device=local_rank)
|
| 197 |
+
outvar = data[1].to(device=local_rank)
|
| 198 |
+
cos_zenith = data[2].to(device=local_rank)
|
| 199 |
+
in_idx = data[3].item()
|
| 200 |
+
|
| 201 |
+
cos_zenith = torch.squeeze(cos_zenith, dim=2)
|
| 202 |
+
cos_zenith = torch.clamp(cos_zenith, min=0.0) - 1.0 / torch.pi # [b, 2, h, w]
|
| 203 |
+
outvar = outvar.to(dtype=model_dtype)
|
| 204 |
+
loss = 0.0
|
| 205 |
+
for t in range(outvar.shape[1]):
|
| 206 |
+
day_of_year, time_of_day = divmod(in_idx + t * cfg.dt, 24 // cfg.dt)
|
| 207 |
+
normalized_day_of_year = torch.tensor((day_of_year / 365) * (np.pi / 2), dtype=torch.float32, device=local_rank)
|
| 208 |
+
normalized_time_of_day = torch.tensor((time_of_day / (24 - cfg.dt)) * (np.pi / 2), dtype=torch.float32, device=local_rank)
|
| 209 |
+
sin_day_of_year = torch.sin(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 210 |
+
cos_day_of_year = torch.cos(normalized_day_of_year).expand(1, 1, 721, 1440)
|
| 211 |
+
sin_time_of_day = torch.sin(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 212 |
+
cos_time_of_day = torch.cos(normalized_time_of_day).expand(1, 1, 721, 1440)
|
| 213 |
+
invar = torch.concat((invar, cos_zenith, static_data, sin_day_of_year, cos_day_of_year, sin_time_of_day, cos_time_of_day), dim=1)
|
| 214 |
+
invar = invar.to(dtype=model_dtype)
|
| 215 |
+
outpred = model(invar)
|
| 216 |
+
invar = outpred
|
| 217 |
+
loss += criterion(outpred, outvar[:, t])
|
| 218 |
+
|
| 219 |
+
loss /= outvar.shape[1]
|
| 220 |
+
if cfg.world_size > 1:
|
| 221 |
+
loss_tensor = loss.detach().to(local_rank) # torch.tensor(loss, device=local_rank)
|
| 222 |
+
dist.all_reduce(loss_tensor)
|
| 223 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 224 |
+
valid_loss += loss
|
| 225 |
+
else:
|
| 226 |
+
valid_loss += loss.item()
|
| 227 |
+
|
| 228 |
+
if world_rank == 0:
|
| 229 |
+
logger.info(f'Valid: Epoch {epoch}-{k+1}/{len(val_dataloader)} '
|
| 230 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 231 |
+
f'[{(time.time()-start_time)/(k+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 232 |
+
f'loss:{valid_loss / (k+1): .04f}')
|
| 233 |
+
|
| 234 |
+
valid_loss /= len(val_dataloader)
|
| 235 |
+
is_save_ckp = False
|
| 236 |
+
if valid_loss < best_valid_loss:
|
| 237 |
+
best_valid_loss = valid_loss
|
| 238 |
+
best_loss_epoch = epoch
|
| 239 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir)
|
| 240 |
+
is_save_ckp = True
|
| 241 |
+
|
| 242 |
+
train_loss /= (j+1)
|
| 243 |
+
if world_rank == 0:
|
| 244 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 245 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 246 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 247 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 248 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 249 |
+
)
|
| 250 |
+
train_losses = np.append(train_losses, train_loss)
|
| 251 |
+
np.save(train_loss_file, train_losses)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 255 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 256 |
+
exit()
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path):
|
| 260 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 261 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 262 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 263 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 264 |
+
"best_valid_loss": best_valid_loss,
|
| 265 |
+
"best_loss_epoch": best_loss_epoch,
|
| 266 |
+
}
|
| 267 |
+
torch.save(state, f"{model_path}/model.pth")
|
| 268 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 269 |
+
os.system(f"mv {model_path}/model.pth {model_path}/model_bak.pth")
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
if __name__ == "__main__":
|
| 273 |
+
current_path = os.getcwd()
|
| 274 |
+
sys.path.append(current_path)
|
| 275 |
+
main()
|
| 276 |
+
|
weight/.gitkeep
ADDED
|
File without changes
|