OneScience commited on
Commit
aa8cca6
·
verified ·
1 Parent(s): b0fd260

Upload folder using huggingface_hub

Browse files
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