OneScience commited on
Commit
2862bae
·
verified ·
1 Parent(s): af7e4fd

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;">FuXi</span>
20
+ </strong>
21
+ </p>
22
+
23
+ # Model Introduction
24
+
25
+ FuXi is a global weather forecast foundation model jointly developed by Fudan University and multiple institutions. It is the first end-to-end machine learning framework capable of independently performing data assimilation (DA) and cyclic forecasting.
26
+
27
+ Paper: FuXi: A cascade machine learning forecasting system for 15-day global weather forecast
28
+
29
+ https://arxiv.org/abs/2306.12873
30
+
31
+ # Model Description
32
+
33
+ The FuXi model is trained through a three-stage cascaded approach: short → medium → long. Its training input primarily consists of ERA5 reanalysis data.
34
+
35
+ # Use Cases
36
+
37
+ | Scenario | Description |
38
+ | :---: | :--- |
39
+ | Weather Forecast Training | Train FuXi (short/medium/long three stages) using ERA5 HDF5 data |
40
+ | Local Quick Validation | Use synthetic data to verify data loading, model training & inference, and inference result visualization. |
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/FuXi --local_dir ./FuXi
64
+ cd FuXi
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
+ ### Training
97
+
98
+ FuXi consists of 3 stages and **must be executed in order**. The inference result of each stage serves as the input for the next stage:
99
+
100
+ **short (train) → short (inference) → medium (train) → medium (inference) → long (train) → long (inference)**
101
+
102
+ **1) Train the short model (train from scratch, as the starting entry point)**
103
+
104
+ Single GPU:
105
+
106
+ ```bash
107
+ python scripts/train_short.py
108
+ ```
109
+
110
+ Multi-GPU:
111
+
112
+ ```bash
113
+ torchrun --nproc_per_node=8 --nnodes=1 --rdzv_id=1000 --rdzv_backend=c10d --max_restarts=0 --master_addr="localhost" --master_port=29500 scripts/train_short.py
114
+ ```
115
+
116
+ **2) Short inference (generate input data for medium)**
117
+
118
+ ```bash
119
+ python scripts/inference.py short
120
+ ```
121
+
122
+ **3) Train the medium model (requires short weights + short inference results)**
123
+
124
+ ```bash
125
+ python scripts/train_medium.py
126
+ ```
127
+
128
+ **4) Medium inference (generate input data for long)**
129
+
130
+ ```bash
131
+ python scripts/inference.py medium
132
+ ```
133
+
134
+ **5) Train the long model (requires medium weights + medium inference results)**
135
+
136
+ ```bash
137
+ python scripts/train_long.py
138
+ ```
139
+
140
+ ### Training Weights
141
+
142
+ This repository provides weights trained on 39 years of ERA5 reanalysis data in the `weights/` folder. The weight files will be uploaded soon and are expected to be available in the near future.
143
+
144
+ ### Inference
145
+
146
+ Each stage can perform inference independently:
147
+
148
+ ```bash
149
+ python scripts/inference.py short
150
+ python scripts/inference.py medium
151
+ python scripts/inference.py long
152
+ ```
153
+
154
+ Inference results will be saved to `result/output/<stage>/`.
155
+
156
+ ### Evaluation and Visualization
157
+
158
+ ```bash
159
+ python scripts/result.py short
160
+ python scripts/result.py medium
161
+ python scripts/result.py long
162
+ ```
163
+
164
+ # OneScience Official Information
165
+
166
+ | Platform | OneScience Main Repository | Skills Repository |
167
+ | --- | --- | --- |
168
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
169
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
170
+
171
+ # Citation & License
172
+
173
+ - This repository is a reproduction of the original FuXi paper.
conf/config.yaml ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model:
2
+
3
+ start_epoch: 0
4
+ max_epoch: 1
5
+ train_lr: 2.5E-4
6
+ finetune_lr: 1E-7
7
+ patch_size: [2, 4, 4]
8
+ embed_dim: 1536
9
+ num_groups: 32
10
+ num_heads: 8
11
+ window_size: 7
12
+ short_num_steps: 20
13
+ medium_num_steps: 40
14
+ long_num_steps: 60
15
+ finetune_step: 1
16
+ step_change_freq: 200
17
+ input_steps: 2
18
+ num_blocks: 8
19
+ checkpoint_dir : "./data/checkpoints"
20
+ patience: 50
21
+
22
+ # 整个数据读取流程
23
+ datapipe:
24
+ name: "ERA5"
25
+ task: "weather_forecasting"
26
+
27
+ # dataset设定
28
+ dataset:
29
+ type: "hdf5"
30
+ data_dir: './data/' # "$ONESCIENCE_DATASETS_DIR/ERA5/newh5/"
31
+ train_time: [2000,2001]
32
+ val_time: [2022]
33
+ test_time: [2023]
34
+ img_size: [2, 721, 1440]
35
+ verbose: true
36
+ cache: false
37
+ # 气象变量
38
+
39
+ channels: ['10m_u_component_of_wind', '10m_v_component_of_wind', '2m_temperature', 'mean_sea_level_pressure', 'total_precipitation',
40
+ 'geopotential_50', 'geopotential_100', 'geopotential_150', 'geopotential_200', 'geopotential_250',
41
+ 'geopotential_300', 'geopotential_400', 'geopotential_500', 'geopotential_600', 'geopotential_700',
42
+ 'geopotential_850', 'geopotential_925', 'geopotential_1000',
43
+ 'relative_humidity_50', 'relative_humidity_100', 'relative_humidity_150', 'relative_humidity_200', 'relative_humidity_250',
44
+ 'relative_humidity_300', 'relative_humidity_400', 'relative_humidity_500', 'relative_humidity_600', 'relative_humidity_700',
45
+ 'relative_humidity_850','relative_humidity_925', 'relative_humidity_1000',
46
+ 'u_component_of_wind_50', 'u_component_of_wind_100', 'u_component_of_wind_150', 'u_component_of_wind_200', 'u_component_of_wind_250',
47
+ 'u_component_of_wind_300', 'u_component_of_wind_400', 'u_component_of_wind_500', 'u_component_of_wind_600','u_component_of_wind_700',
48
+ 'u_component_of_wind_850', 'u_component_of_wind_925', 'u_component_of_wind_1000',
49
+ 'v_component_of_wind_50', 'v_component_of_wind_100', 'v_component_of_wind_150', 'v_component_of_wind_200', 'v_component_of_wind_250',
50
+ 'v_component_of_wind_300', 'v_component_of_wind_400', 'v_component_of_wind_500', 'v_component_of_wind_600','v_component_of_wind_700',
51
+ 'v_component_of_wind_850', 'v_component_of_wind_925', 'v_component_of_wind_1000',
52
+ 'temperature_50', 'temperature_100', 'temperature_150', 'temperature_200', 'temperature_250',
53
+ 'temperature_300', 'temperature_400', 'temperature_500', 'temperature_600', 'temperature_700',
54
+ 'temperature_850', 'temperature_925', 'temperature_1000'
55
+ ]
56
+
57
+ variables:
58
+ - "u10" # 10m U wind component
59
+ - "v10" # 10m V wind component
60
+ - "t2m" # 2m temperature
61
+ - "msl" # Mean sea level pressure
62
+ - "z500" # Geopotential at 500 hPa
63
+ - "t850" # Temperature at 850 hPa
64
+
65
+ # 时间配置
66
+ time_range: ["2000-01-01", "2020-12-31"]
67
+ time_steps: 1
68
+ time_res: 6
69
+
70
+ # 空间配置
71
+ spatial_resolution: [0.25, 0.25]
72
+
73
+ # 采样配置
74
+ num_samples: -1 # -1 表示使用全部数据
75
+ shuffle: true
76
+ random_seed: 42
77
+
78
+ # 领域特定配置
79
+ extra:
80
+ levels: [500, 850, 1000]
81
+ lat_range: [-90, 90]
82
+ lon_range: [0, 360]
83
+
84
+ # 数据转换配置
85
+ transforms:
86
+ - type: "Normalize"
87
+ params:
88
+ mean: [0.0, 0.0, 288.0, 101325.0, 50000.0, 270.0]
89
+ std: [5.0, 5.0, 15.0, 1000.0, 5000.0, 10.0]
90
+ keys: ["input", "target"]
91
+
92
+ - type: "ToTensor"
93
+ params:
94
+ keys: null # null表示转换所有numpy数组
95
+
96
+ # 其他配置
97
+
98
+
99
+ # DataLoader配置
100
+ dataloader:
101
+ mask_dtype: "float32"
102
+ batch_size: 1
103
+ num_workers: 1
104
+ pin_memory: true
105
+ drop_last: true
106
+ shuffle: false # 使用sampler时设为false
107
+ prefetch_factor: 2
108
+ persistent_workers: true
109
+
110
+ # 分布式配置
111
+ distributed:
112
+ enabled: true
113
+ sampler: "DistributedSampler"
114
+ rank: 0
115
+ world_size: 4
116
+ shuffle: true
117
+ seed: 42
118
+ drop_last: true
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"other"}
model/fuxi.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+ from torch.nn import functional as F
4
+
5
+ from onescience.modules.embedding.fuxiembedding import FuxiEmbedding
6
+ from onescience.modules.fc.fuxifc import FuxiFC
7
+ from onescience.modules.transformer.fuxitransformer import FuxiTransformer
8
+
9
+
10
+ class Fuxi(nn.Module):
11
+ """
12
+ Fuxi 的主模型实现。
13
+
14
+ 该模型使用以下组件完成输入编码、二维 trunk 特征提取与 patch 级输出恢复:
15
+
16
+ - `OneEmbedding(style="FuxiEmbedding")`
17
+ - 将 `(TimeSteps, Height, Width)` 三维时空块映射为 patch 特征
18
+ - `OneTransformer(style="FuxiTransformer")`
19
+ - 在二维特征图上执行下采样、Swin trunk、上采样
20
+ - `OneFC(style="FuxiFC")`
21
+ - 将每个二维网格位置的 embedding 特征映射为 patch 级输出变量
22
+
23
+ 在当前实现中:
24
+
25
+ - 输入包含多个时间步的二维气象场
26
+ - `patch_size[0]` 默认与 `TimeSteps` 相同,使 embedding 后时间维压缩为 1
27
+ - trunk 只处理二维特征图
28
+ - 最终通过 patch 重排与双线性插值恢复到目标空间分辨率
29
+
30
+ Args:
31
+ img_size (tuple[int, int, int]):
32
+ 输入空间尺寸 `(TimeSteps, Height, Width)`。
33
+ patch_size (tuple[int, int, int]):
34
+ patch 切分尺寸 `(PatchTimeSteps, PatchHeight, PatchWidth)`。
35
+ in_chans (int):
36
+ 输入变量通道数。
37
+ out_chans (int):
38
+ 输出变量通道数。
39
+ embed_dim (int):
40
+ embedding 特征维度。
41
+ num_groups (int):
42
+ trunk 中采样模块的 `GroupNorm` 分组数。
43
+ num_heads (int):
44
+ `SwinTransformerV2Stage` 的注意力头数。
45
+ window_size (int | tuple[int, int]):
46
+ trunk 局部窗口大小。
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ img_size=(2, 721, 1440),
52
+ patch_size=(2, 4, 4),
53
+ in_chans=70,
54
+ out_chans=70,
55
+ embed_dim=1536,
56
+ num_groups=32,
57
+ num_heads=8,
58
+ window_size=7,
59
+ ):
60
+ super().__init__()
61
+
62
+ TimeSteps, Height, Width = img_size
63
+ PatchTimeSteps, PatchHeight, PatchWidth = patch_size
64
+ if TimeSteps != PatchTimeSteps:
65
+ raise ValueError(
66
+ "Current Fuxi model expects patch_size[0] to equal img_size[0] "
67
+ "so the embedding output time dimension is 1 before squeeze"
68
+ )
69
+
70
+ EmbeddedHeight = Height // PatchHeight
71
+ EmbeddedWidth = Width // PatchWidth
72
+ TransformerInputResolution = (
73
+ EmbeddedHeight // 2,
74
+ EmbeddedWidth // 2,
75
+ )
76
+
77
+ self.cube_embedding = FuxiEmbedding(
78
+ img_size=img_size,
79
+ patch_size=patch_size,
80
+ in_chans=in_chans,
81
+ embed_dim=embed_dim,
82
+ )
83
+ self.u_transformer = FuxiTransformer(
84
+ embed_dim=embed_dim,
85
+ num_groups=num_groups,
86
+ input_resolution=TransformerInputResolution,
87
+ num_heads=num_heads,
88
+ window_size=window_size,
89
+ )
90
+ self.fc = FuxiFC(
91
+ in_channels=embed_dim,
92
+ out_channels=out_chans * PatchHeight * PatchWidth,
93
+ )
94
+
95
+ self.patch_size = patch_size
96
+ self.transformer_input_resolution = TransformerInputResolution
97
+ self.embedded_resolution = (EmbeddedHeight, EmbeddedWidth)
98
+ self.out_chans = out_chans
99
+ self.img_size = img_size
100
+
101
+ def forward(self, x):
102
+ """
103
+ Args:
104
+ x (torch.Tensor):
105
+ 输入张量,形状为 `(Batch, in_chans, TimeSteps, Height, Width)`。
106
+
107
+ Returns:
108
+ torch.Tensor:
109
+ 输出张量,形状为 `(Batch, out_chans, Height, Width)`。
110
+ """
111
+ Batch, _, _, _, _ = x.shape
112
+ _, PatchHeight, PatchWidth = self.patch_size
113
+ EmbeddedHeight, EmbeddedWidth = self.embedded_resolution
114
+
115
+ x = self.cube_embedding(x)
116
+ if x.shape[2] != 1:
117
+ raise ValueError(
118
+ f"Expected embedding time dimension 1 before squeeze, but received {x.shape[2]}"
119
+ )
120
+
121
+ x = x.squeeze(2)
122
+ x = self.u_transformer(x)
123
+ x = self.fc(x.permute(0, 2, 3, 1))
124
+ x = x.reshape(
125
+ Batch,
126
+ EmbeddedHeight,
127
+ EmbeddedWidth,
128
+ PatchHeight,
129
+ PatchWidth,
130
+ self.out_chans,
131
+ ).permute(0, 1, 3, 2, 4, 5)
132
+ x = x.reshape(
133
+ Batch,
134
+ EmbeddedHeight * PatchHeight,
135
+ EmbeddedWidth * PatchWidth,
136
+ self.out_chans,
137
+ )
138
+ x = x.permute(0, 3, 1, 2)
139
+
140
+ x = F.interpolate(x, size=self.img_size[1:], mode="bilinear")
141
+
142
+ return x
scripts/data_loader.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import h5py
4
+ import pytz
5
+ import numpy as np
6
+ import torch
7
+
8
+ from datetime import datetime, timedelta
9
+ from torch.utils.data import Dataset, DataLoader
10
+ from torch.utils.data.distributed import DistributedSampler
11
+
12
+ from onescience.datapipes.climate.utils.invariant import latlon_grid
13
+ from onescience.datapipes.climate.utils.zenith_angle import cos_zenith_angle
14
+
15
+
16
+ class ERA5Datapipe:
17
+ def __init__(
18
+ self,
19
+ dataset_dir,
20
+ used_years,
21
+ used_variables,
22
+ pattern='medium',
23
+ distributed=False,
24
+ input_steps=1,
25
+ output_steps=1,
26
+ normalize=True,
27
+ batch_size=1,
28
+ num_workers=4,
29
+ ):
30
+ self.dataset_dir = dataset_dir
31
+ self.used_years = used_years
32
+ self.used_variables = used_variables
33
+ self.pattern = pattern
34
+ self.distributed = distributed
35
+ self.input_steps = input_steps
36
+ self.output_steps = output_steps
37
+ self.normalize = normalize
38
+ self.batch_size = batch_size
39
+ self.num_workers = num_workers
40
+
41
+ def get_dataloader(self, mode):
42
+ dataset = ERA5Dataset(
43
+ dataset_dir=self.dataset_dir,
44
+ used_years=self.used_years,
45
+ used_variables=self.used_variables,
46
+ pattern=self.pattern,
47
+ input_steps=self.input_steps,
48
+ output_steps=self.output_steps,
49
+ normalize=self.normalize,
50
+ )
51
+ is_train = (mode == 'train')
52
+
53
+ sampler = DistributedSampler(dataset, shuffle=is_train) if self.distributed else None
54
+
55
+ return DataLoader(
56
+ dataset,
57
+ batch_size=self.batch_size,
58
+ num_workers=self.num_workers,
59
+ pin_memory=True,
60
+ shuffle=(is_train and not self.distributed),
61
+ sampler=sampler,
62
+ drop_last=self.distributed,
63
+ ), sampler
64
+
65
+
66
+ class ERA5Dataset(Dataset):
67
+ def __init__(
68
+ self,
69
+ dataset_dir,
70
+ used_years,
71
+ used_variables,
72
+ pattern='medium',
73
+ input_steps=1,
74
+ output_steps=1,
75
+ normalize=True,
76
+ ):
77
+ self.dataset_dir = dataset_dir
78
+ self.used_years = used_years
79
+ self.used_variables = used_variables
80
+ self.pattern = pattern
81
+ self.input_steps = input_steps
82
+ self.output_steps = output_steps
83
+ self.normalize = normalize
84
+
85
+ self._init_avail_samples()
86
+ self._init_normalized_files()
87
+ self._init_npy_files()
88
+ self._init_latlon_grid()
89
+
90
+ def _init_avail_samples(self):
91
+ h5_files = sorted(glob.glob(os.path.join(self.dataset_dir, "data", "*.h5")))
92
+ available_years = [int(os.path.basename(f).replace(".h5", "")) for f in h5_files]
93
+
94
+ missing_years = [y for y in self.used_years if y not in available_years]
95
+ if missing_years:
96
+ raise ValueError(f"❌ Years not found in dataset: {missing_years}")
97
+
98
+ # ── 读取变量信息 & 校验 ───────────────────────────────
99
+ with h5py.File(h5_files[0], "r") as f:
100
+ ds = f["fields"]
101
+ self.T, self.C, self.H, self.W = ds.shape
102
+ all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
103
+ self.time_step = int(ds.attrs["time_step"])
104
+
105
+ missing_vars = [v for v in self.used_variables if v not in all_variables]
106
+ if missing_vars:
107
+ raise ValueError(f"❌ Variables not found in dataset: {missing_vars}")
108
+
109
+ # ── 建立索引 ──────────────────────────────────────────
110
+ self.channel_indices = [all_variables.index(v) for v in self.used_variables]
111
+
112
+ def _init_normalized_files(self):
113
+ # ── 从每年 h5 内嵌的 global_means/global_stds 读取归一化统计量 ──
114
+ h5_files = sorted(glob.glob(os.path.join(self.dataset_dir, "data", "*.h5")))
115
+ with h5py.File(h5_files[0], "r") as f:
116
+ mu = f["global_means"][:] # [1, C, 1, 1]
117
+ std = f["global_stds"][:]
118
+ self.mu = torch.as_tensor(mu[:, self.channel_indices, :, :], dtype=torch.float32)
119
+ self.sd = torch.as_tensor(std[:, self.channel_indices, :, :], dtype=torch.float32)
120
+
121
+ def _init_npy_files(self):
122
+ """读取前一阶段模型输出的 npy 文件列表(作为 invar 输入)"""
123
+ self.files = {}
124
+ for year in self.used_years:
125
+ if self.pattern == 'medium':
126
+ path = os.path.join('./result/short/data/', str(year))
127
+ else:
128
+ path = os.path.join('./result/medium/data', str(year))
129
+ files = sorted(glob.glob(os.path.join(path, "*.npy")))
130
+ if not files:
131
+ raise ValueError(f"❌ No npy files found for year {year} under {path}")
132
+ self.files[year] = files
133
+
134
+ n_files = len(files)
135
+ self.samples_per_year = n_files - self.output_steps - (self.input_steps - 1)
136
+ self.total_samples = len(self.used_years) * self.samples_per_year
137
+
138
+ if not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0:
139
+ print('\n')
140
+ print('-' * 50)
141
+ print(f"📂 Pattern: {self.pattern}, used: {self.used_years} years")
142
+ print(f'📂 each year contains {self.samples_per_year} usable samples '
143
+ f'({n_files} files, input {self.input_steps}, output {self.output_steps})')
144
+ print(f'📂 total usable samples: {self.total_samples}')
145
+ print('-' * 50, '\n')
146
+
147
+ def _init_latlon_grid(self):
148
+ latlon = latlon_grid(bounds=((90, -90), (0, 360)), shape=(self.H, self.W))
149
+ self.latlon_torch = torch.tensor(np.stack(latlon, axis=0), dtype=torch.float32)
150
+
151
+ def _filename_to_index(self, filename):
152
+ """将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
153
+ dt = datetime.strptime(filename, "%Y%m%d%H")
154
+ year_start = datetime(dt.year, 1, 1)
155
+ hours = (dt - year_start).total_seconds() / 3600
156
+ return int(hours / self.time_step)
157
+
158
+ def __len__(self):
159
+ return self.total_samples
160
+
161
+ def __getitem__(self, idx):
162
+ year_idx = idx // self.samples_per_year
163
+ step_idx = idx % self.samples_per_year
164
+ year = self.used_years[year_idx]
165
+ files = self.files[year]
166
+
167
+ # ── invar: 从前一阶段模型输出的 npy 文件读取 ──────────
168
+ invar_list = []
169
+ for i in range(step_idx, step_idx + self.input_steps):
170
+ data = np.load(files[i])
171
+ data = np.squeeze(data) # [C, H, W]
172
+ invar_list.append(data)
173
+
174
+ # ── outvar: 从年度 h5 文件读取真实 ERA5 标签 ──────────
175
+ outvar_list = []
176
+ time_index = []
177
+ h5_path = os.path.join(self.dataset_dir, 'data', f'{year}.h5')
178
+ with h5py.File(h5_path, "r") as f:
179
+ for i in range(step_idx + self.input_steps, step_idx + self.input_steps + self.output_steps):
180
+ fname = os.path.basename(files[i])[:-4] # 去掉 .npy,得到 YYYYMMDDHH
181
+ t_idx = self._filename_to_index(fname)
182
+ data = f["fields"][t_idx] # [C, H, W]
183
+ data = data[self.channel_indices]
184
+ outvar_list.append(data)
185
+ time_index.append(fname)
186
+
187
+ invar = np.stack(invar_list, axis=0) # [T, C, H, W]
188
+ outvar = np.stack(outvar_list, axis=0) # [T, C, H, W]
189
+ invar = torch.as_tensor(invar, dtype=torch.float32)
190
+ outvar = torch.as_tensor(outvar, dtype=torch.float32)
191
+
192
+ if self.normalize:
193
+ invar = (invar - self.mu) / self.sd
194
+ outvar = (outvar - self.mu) / self.sd
195
+
196
+ # ── 太阳天顶角 ────────────────────────────────────────
197
+ start_time = datetime(year, 1, 1, tzinfo=pytz.utc)
198
+ timestamps = np.array([
199
+ (start_time + timedelta(hours=(step_idx + t) * self.time_step)).timestamp()
200
+ for t in range(self.output_steps)
201
+ ])
202
+ timestamps = torch.from_numpy(timestamps)
203
+ cos_zenith = cos_zenith_angle(timestamps, latlon=self.latlon_torch).float()
204
+
205
+ return invar.squeeze(0), outvar.squeeze(0), cos_zenith, step_idx, time_index
scripts/fake_data.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 generate_fake_npy(result_dir, n_vars, years, dims):
47
+ """
48
+ 为 short/medium 阶段生成假的模型输出 npy 文件(供 train_medium/train_long 输入)。
49
+ 只创建一个真实的 npy 文件,其余使用 symlink。
50
+ """
51
+ T = dims["T"]
52
+ H, W = dims["H"], dims["W"]
53
+ time_step = dims["time_step"]
54
+
55
+ data_root = os.path.join(result_dir, "data")
56
+ os.makedirs(data_root, exist_ok=True)
57
+
58
+ real_year = years[0]
59
+ real_dir = os.path.join(data_root, str(real_year))
60
+ os.makedirs(real_dir, exist_ok=True)
61
+
62
+ timestamp = f'{real_year}010100'
63
+ real_path = os.path.join(real_dir, f"{timestamp}.npy")
64
+ real_data = np.zeros((n_vars, H, W), dtype=np.float32)
65
+ np.save(real_path, real_data)
66
+
67
+ # 当前年份其余时间步 symlink
68
+ for i in range(T):
69
+ ts = (np.datetime64(f"{real_year}-01-01T00") + np.timedelta64(i * time_step, "h")
70
+ ).astype(str).replace("-", "").replace("T", "")[:10]
71
+ path = os.path.join(real_dir, f"{ts}.npy")
72
+ if path != real_path and not os.path.exists(path):
73
+ os.symlink(f"{timestamp}.npy", path)
74
+
75
+ # 其他年份 symlink
76
+ for y in years[1:]:
77
+ y_dir = os.path.join(data_root, str(y))
78
+ os.makedirs(y_dir, exist_ok=True)
79
+ for i in range(T):
80
+ ts = (np.datetime64(f"{y}-01-01T00") + np.timedelta64(i * time_step, "h")
81
+ ).astype(str).replace("-", "").replace("T", "")[:10]
82
+ path = os.path.join(y_dir, f"{ts}.npy")
83
+ if not os.path.exists(path):
84
+ rel = os.path.relpath(real_path, y_dir)
85
+ os.symlink(rel, path)
86
+
87
+ print(f" ✅ Fake npy data generated → {result_dir}")
88
+
89
+
90
+ def get_static(data_dir, var, name):
91
+ os.makedirs(data_dir, exist_ok=True)
92
+ ds = xr.Dataset(
93
+ data_vars={
94
+ f"{var}": (("valid_time", "latitude", "longitude"),
95
+ np.random.rand(1, 721, 1440).astype(np.float32))
96
+ },
97
+ coords={
98
+ "valid_time": ["2015-12-31"],
99
+ "latitude": np.linspace(90, -90, 721, dtype=np.float64),
100
+ "longitude": np.linspace(0, 359.75, 1440, dtype=np.float64),
101
+ "number": 0,
102
+ "expver": "",
103
+ },
104
+ attrs={
105
+ "GRIB_centre": "ecmf",
106
+ "GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts",
107
+ "GRIB_subCentre": "0",
108
+ "Conventions": "CF-1.7",
109
+ "institution": "European Centre for Medium-Range Weather Forecasts",
110
+ "history": "Generated manually",
111
+ }
112
+ )
113
+
114
+ ds.to_netcdf(f"{data_dir}/{name}.nc")
115
+ arr = np.random.randn(721, 1440).astype(np.float32)
116
+ np.save(f'{data_dir}/land_mask.npy', arr)
117
+ np.save(f'{data_dir}/soil_type.npy', arr)
118
+ np.save(f'{data_dir}/topography.npy', arr)
119
+ print(f"✅ Static data: {arr.shape}, dtype: {arr.dtype}, save to {data_dir}")
120
+
121
+
122
+ if __name__ == "__main__":
123
+ cfg_datapipe = YParams("conf/config.yaml", "datapipe")
124
+
125
+ if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work/"):
126
+ print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
127
+ exit()
128
+
129
+ years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
130
+ atm_vars = cfg_datapipe.dataset.channels
131
+ n_vars = len(atm_vars)
132
+
133
+ # 主 ERA5 数据(供 train_short 直接读取,归一化参数已内嵌进每年 h5)
134
+ generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, DATASET_DIMS)
135
+
136
+ # 前阶段推理输出:result/short 供 train_medium,result/medium 供 train_long
137
+ for stage in ['short', 'medium']:
138
+ generate_fake_npy(f'./result/{stage}', n_vars, years, DATASET_DIMS)
139
+
140
+ static_dir = os.path.join(cfg_datapipe.dataset.data_dir, "static")
141
+ get_static(static_dir, 'z', 'geopotential')
142
+ get_static(static_dir, 'lsm', 'land_sea_mask')
143
+
144
+ print("\n✅ Fake datasets generated.")
scripts/inference.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import os
3
+ import sys
4
+ import glob
5
+ import numpy as np
6
+ import h5py
7
+ from tqdm import tqdm
8
+ from onescience.models.fuxi import Fuxi
9
+ from onescience.utils.YParams import YParams
10
+
11
+
12
+ def get_stats(data_dir, channels):
13
+ """从新版 h5 中读取变量列表与归一化参数(均值/标准差)"""
14
+ h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
15
+ with h5py.File(h5_files[0], "r") as f:
16
+ ds = f["fields"]
17
+ all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
18
+ mu = f["global_means"][:] # [1, C, 1, 1]
19
+ std = f["global_stds"][:]
20
+
21
+ channel_indices = [all_variables.index(v) for v in channels]
22
+ means = mu[:, channel_indices, :, :]
23
+ stds = std[:, channel_indices, :, :]
24
+ return means, stds
25
+
26
+
27
+ if __name__ == "__main__":
28
+ if len(sys.argv) != 2:
29
+ print("Usage: input the mode: : short, medium, or long...")
30
+ sys.exit(1)
31
+
32
+ mode = sys.argv[1]
33
+ if mode not in ['short', 'medium', 'long']:
34
+ print(f'❌ ❌ Please input the mode: short, medium, or long...')
35
+ exit()
36
+
37
+ current_path = os.getcwd()
38
+ sys.path.append(current_path)
39
+
40
+ ## Model config init
41
+ config_file_path = os.path.join(current_path, "conf/config.yaml")
42
+ cfg = YParams(config_file_path, "model")
43
+ ## DataLoader init
44
+ cfg_data = YParams(config_file_path, "datapipe")
45
+ cfg_data.dataloader.batch_size = 1
46
+ means, stds = get_stats(cfg_data.dataset.data_dir, cfg_data.dataset.channels)
47
+
48
+ if mode == 'short':
49
+ from onescience.datapipes.climate import ERA5Datapipe
50
+ train_datapipe = ERA5Datapipe(
51
+ dataset_dir=cfg_data.dataset.data_dir,
52
+ used_variables=cfg_data.dataset.channels,
53
+ used_years=cfg_data.dataset.train_time,
54
+ distributed=False,
55
+ input_steps=2,
56
+ batch_size=1,
57
+ num_workers=4,
58
+ )
59
+ train_dataloader, train_sampler = train_datapipe.get_dataloader("train")
60
+ val_datapipe = ERA5Datapipe(
61
+ dataset_dir=cfg_data.dataset.data_dir,
62
+ used_variables=cfg_data.dataset.channels,
63
+ used_years=cfg_data.dataset.val_time,
64
+ distributed=False,
65
+ input_steps=2,
66
+ batch_size=1,
67
+ num_workers=4,
68
+ )
69
+ val_dataloader, val_sampler = val_datapipe.get_dataloader("valid")
70
+ test_datapipe = ERA5Datapipe(
71
+ dataset_dir=cfg_data.dataset.data_dir,
72
+ used_variables=cfg_data.dataset.channels,
73
+ used_years=cfg_data.dataset.test_time,
74
+ distributed=False,
75
+ input_steps=2,
76
+ batch_size=1,
77
+ num_workers=4,
78
+ )
79
+ test_dataloader, _ = test_datapipe.get_dataloader("test")
80
+ else:
81
+ from data_loader import ERA5Datapipe
82
+ train_datapipe = ERA5Datapipe(
83
+ dataset_dir=cfg_data.dataset.data_dir,
84
+ used_variables=cfg_data.dataset.channels,
85
+ used_years=cfg_data.dataset.train_time,
86
+ pattern=mode,
87
+ distributed=False,
88
+ input_steps=2,
89
+ batch_size=1,
90
+ num_workers=4,
91
+ )
92
+ train_dataloader, train_sampler = train_datapipe.get_dataloader("train")
93
+ val_datapipe = ERA5Datapipe(
94
+ dataset_dir=cfg_data.dataset.data_dir,
95
+ used_variables=cfg_data.dataset.channels,
96
+ used_years=cfg_data.dataset.val_time,
97
+ pattern=mode,
98
+ distributed=False,
99
+ input_steps=2,
100
+ batch_size=1,
101
+ num_workers=4,
102
+ )
103
+ val_dataloader, val_sampler = val_datapipe.get_dataloader("valid")
104
+ test_datapipe = ERA5Datapipe(
105
+ dataset_dir=cfg_data.dataset.data_dir,
106
+ used_variables=cfg_data.dataset.channels,
107
+ used_years=cfg_data.dataset.test_time,
108
+ pattern=mode,
109
+ distributed=False,
110
+ input_steps=2,
111
+ batch_size=1,
112
+ num_workers=4,
113
+ )
114
+ test_dataloader, _ = test_datapipe.get_dataloader("test")
115
+
116
+ ckpt = torch.load(f"{cfg.checkpoint_dir}/model_{mode}_bak.pth", map_location="cuda:0")
117
+ model = Fuxi(img_size=cfg_data.dataset.img_size,
118
+ patch_size=cfg.patch_size,
119
+ in_chans=len(cfg_data.dataset.channels),
120
+ out_chans=len(cfg_data.dataset.channels),
121
+ embed_dim=cfg.embed_dim,
122
+ num_groups=cfg.num_groups,
123
+ num_heads=cfg.num_heads,
124
+ window_size=cfg.window_size
125
+ ).to("cuda:0")
126
+ model.load_state_dict(ckpt["model_state_dict"])
127
+
128
+ model.eval()
129
+ save_path = f'./result/{mode}/data/'
130
+ if mode != 'long':
131
+ with torch.no_grad():
132
+ print(f"📂 infer results will be generated to './result/{mode}/data/'")
133
+ for data in tqdm(train_dataloader, desc="Inferring trainset", unit="batch"):
134
+ invar = data[0].to("cuda:0", dtype=torch.float32) # B, T, C, H, W
135
+ invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
136
+ filename = data[4][-1][0]
137
+ pred_var = model(invar).cpu().numpy()
138
+ pred_var = pred_var * stds + means
139
+ os.makedirs(f'{save_path}/{filename[:4]}', exist_ok=True)
140
+ np.save(f"{save_path}/{filename[:4]}/{filename}.npy", pred_var)
141
+
142
+ with torch.no_grad():
143
+ print(f"📂 infer results will be generated to './result/{mode}/data/'")
144
+ for data in tqdm(val_dataloader, desc="Inferring validset", unit="batch"):
145
+ invar = data[0].to("cuda:0", dtype=torch.float32)
146
+ invar = invar.permute(0, 2, 1, 3, 4)
147
+ filename = data[4][-1][0]
148
+ pred_var = model(invar).cpu().numpy()
149
+ pred_var = pred_var * stds + means
150
+ os.makedirs(f'{save_path}/{filename[:4]}', exist_ok=True)
151
+ np.save(f"{save_path}/{filename[:4]}/{filename}.npy", pred_var)
152
+
153
+ with torch.no_grad():
154
+ print(f"📂 infer results will be generated to './result/{mode}/data/'")
155
+ for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
156
+ invar = data[0].to("cuda:0", dtype=torch.float32)
157
+ invar = invar.permute(0, 2, 1, 3, 4)
158
+ filename = data[4][-1][0]
159
+ pred_var = model(invar).cpu().numpy()
160
+ pred_var = pred_var * stds + means
161
+ os.makedirs(f'{save_path}/{filename[:4]}', exist_ok=True)
162
+ np.save(f"{save_path}/{filename[:4]}/{filename}.npy", pred_var)
scripts/result.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, test_years, mode):
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 = []
31
+ for year in test_years:
32
+ result_dir = f'./result/{mode}/data/{year}/'
33
+ if os.path.exists(result_dir):
34
+ files = [f for f in os.listdir(result_dir) if f.endswith('.npy')]
35
+ total_files.extend(files)
36
+ total_files.sort()
37
+ return total_files, channel_indices, time_step
38
+
39
+
40
+ def filename_to_index(filename, time_step):
41
+ """将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
42
+ dt = datetime.strptime(filename, "%Y%m%d%H")
43
+ year_start = datetime(dt.year, 1, 1)
44
+ hours = (dt - year_start).total_seconds() / 3600
45
+ return int(hours / time_step)
46
+
47
+
48
+ def get_result(total_files, channel_indices, time_step, data_dir, clim_mean, mode):
49
+ channel_rmse = np.zeros(len(channel_indices))
50
+ channel_acc = np.zeros(len(channel_indices))
51
+ clim_mean = clim_mean[0, :, :, :]
52
+ if not os.path.exists(f'./result/{mode}_rmse.npy') or not os.path.exists(f'./result/{mode}_acc.npy'):
53
+ numerator = np.zeros(len(channel_indices))
54
+ pred_sq_sum = np.zeros(len(channel_indices))
55
+ label_sq_sum = np.zeros(len(channel_indices))
56
+ for file in tqdm(total_files, unit="files"):
57
+ fname = file[:-4] # 去掉 .npy
58
+ year = fname[:4]
59
+ t_idx = filename_to_index(fname, time_step)
60
+ with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
61
+ label = f["fields"][t_idx] # [C, H, W]
62
+ label = label[channel_indices]
63
+ pred = np.load(f'result/{mode}/data/{year}/{file}').squeeze()
64
+
65
+ label_anom = label - clim_mean
66
+ pred_anom = pred - clim_mean
67
+ # 累加
68
+ numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
69
+ pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
70
+ label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
71
+ channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
72
+
73
+ channel_rmse /= len(total_files)
74
+ channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
75
+ np.save(f'./result/{mode}_acc.npy', channel_acc)
76
+ np.save(f'./result/{mode}_rmse.npy', channel_rmse)
77
+
78
+
79
+ def show_result(mode):
80
+ channel_rmse = np.load(f'./result/{mode}_rmse.npy')
81
+ channel_acc = np.load(f'./result/{mode}_acc.npy')
82
+
83
+ channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
84
+ w = 24 # 最长 channel 名宽度
85
+
86
+ # 表头
87
+ print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
88
+ print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
89
+ print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
90
+ # 数据行
91
+ for i, ch in enumerate(channels):
92
+ print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
93
+ print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
94
+ print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
95
+ print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
96
+
97
+
98
+ def plot(label, pred, var, filename):
99
+ fig, axes = plt.subplots(1, 3, figsize=(15, 4))
100
+
101
+ xtick_labels = ['180°W', '90°W', '0°', '90°E', '180°E']
102
+ ytick_labels = ['90°S', '45°S', '0°', '45°N', '90°N']
103
+ xticks = np.linspace(0, label.shape[-1] - 1, 5)
104
+ yticks = np.linspace(0, label.shape[-2] - 1, 5)
105
+
106
+ vmin = min(label.min(), pred.min())
107
+ vmax = max(label.max(), pred.max())
108
+
109
+ diff = label - pred
110
+ rmse = np.sqrt(np.mean(diff ** 2))
111
+ diff_abs_max = np.abs(diff).max()
112
+
113
+ plot_configs = [
114
+ {'data': label, 'title': 'Truth', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
115
+ {'data': pred, 'title': 'Prediction', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
116
+ {'data': diff, 'title': f'Difference (RMSE={rmse:.2f})', 'cmap': 'RdBu_r', 'vmin': -diff_abs_max, 'vmax': diff_abs_max},
117
+ ]
118
+
119
+ for ax, cfg in zip(axes, plot_configs):
120
+ im = ax.imshow(cfg['data'], cmap=cfg['cmap'], vmin=cfg['vmin'], vmax=cfg['vmax'])
121
+ ax.set_title(cfg['title'], fontsize=12, pad=4)
122
+ ax.set_xlabel('Longitude')
123
+ ax.set_ylabel('Latitude')
124
+ ax.set_xticks(xticks)
125
+ ax.set_xticklabels(xtick_labels)
126
+ ax.set_yticks(yticks)
127
+ ax.set_yticklabels(ytick_labels)
128
+ plt.colorbar(im, ax=ax, orientation='horizontal')
129
+
130
+ fig.suptitle(var, fontsize=14, fontweight='bold', y=0.98)
131
+ plt.savefig(filename, dpi=300, bbox_inches='tight')
132
+ plt.close()
133
+
134
+
135
+ def plot_loss(train_loss, valid_loss):
136
+ mask = ~(np.isnan(train_loss) | np.isnan(valid_loss))
137
+ train_loss = train_loss[mask]
138
+ valid_loss = valid_loss[mask]
139
+
140
+ fig, ax = plt.subplots(figsize=(5, 3.5))
141
+ colors = {'train': '#2563EB', 'valid': '#EA580C'}
142
+ epochs = np.arange(1, len(train_loss) + 1)
143
+
144
+ ax.plot(epochs, train_loss, color=colors['train'], linewidth=1.5, label='Train')
145
+ ax.plot(epochs, valid_loss, color=colors['valid'], linewidth=1.5, label='Valid', linestyle='--')
146
+ min_idx = np.argmin(valid_loss)
147
+ ax.scatter(epochs[min_idx], valid_loss[min_idx],
148
+ color=colors['valid'], s=40, zorder=5, edgecolors='white')
149
+ ax.annotate(f'Best: {valid_loss[min_idx]:.3f}',
150
+ xy=(epochs[min_idx], valid_loss[min_idx]),
151
+ xytext=(10, 10), textcoords='offset points', fontsize=8, color=colors['valid'],
152
+ arrowprops=dict(arrowstyle='-', color=colors['valid'], lw=0.5))
153
+
154
+ ax.set(xlabel='Epoch', ylabel='Loss', xlim=(0, len(train_loss) + 1))
155
+ ax.legend(frameon=False, loc='upper right')
156
+ ax.grid(True, linestyle='--', alpha=0.3)
157
+ ax.spines[['top', 'right']].set_visible(False)
158
+
159
+ plt.tight_layout()
160
+ plt.savefig('./result/loss.png', dpi=300, bbox_inches='tight')
161
+ plt.close()
162
+
163
+
164
+ if __name__ == "__main__":
165
+ if len(sys.argv) != 2:
166
+ print("Usage: input the mode: : short, medium, or long...")
167
+ sys.exit(1)
168
+
169
+ mode = sys.argv[1]
170
+ if mode not in ['short', 'medium', 'long']:
171
+ print(f'❌ ❌ Please input the mode: short, medium, or long...')
172
+ exit()
173
+
174
+ current_path = os.getcwd()
175
+ sys.path.append(current_path)
176
+ config_file_path = os.path.join(current_path, 'conf/config.yaml')
177
+ cfg = YParams(config_file_path, 'model')
178
+ cfg_data = YParams(config_file_path, "datapipe")
179
+
180
+ train_loss = np.load(f'./data/checkpoints/tr_{mode}_loss.npy')
181
+ valid_loss = np.load(f'./data/checkpoints/va_{mode}_loss.npy')
182
+ plot_loss(train_loss, valid_loss)
183
+
184
+ data_dir = cfg_data.dataset.data_dir
185
+ test_years = cfg_data.dataset.test_time
186
+ total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels, test_years, mode)
187
+
188
+ # Load data & Compute RMSE/ACC per channel
189
+ h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
190
+ with h5py.File(h5_files[0], "r") as f:
191
+ mu = f["global_means"][:]
192
+ clim_mean = mu[:, channel_indices, :, :]
193
+ get_result(total_files, channel_indices, time_step, data_dir, clim_mean, mode)
194
+ show_result(mode)
195
+
196
+ ##### 默认绘制 test_time 第一年的第一个时间步,用户可自行指定日期和变量 #####
197
+ test_year = cfg_data.dataset.test_time[0]
198
+ eg_files = [f'{test_year}010206']
199
+ channel_index = [cfg_data.dataset.channels.index(v) for v in ['2m_temperature', 'geopotential_500', 'temperature_500']]
200
+
201
+ selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
202
+ print(f"seleted date: {eg_files}")
203
+ print(f"selected channels: {selected_var}")
204
+ for file in eg_files:
205
+ year = file[:4]
206
+ t_idx = filename_to_index(file, time_step)
207
+ with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
208
+ label = f["fields"][t_idx] # [C, H, W]
209
+ label = label[channel_indices]
210
+ pred = np.load(f'result/{mode}/data/{year}/{file}.npy').squeeze()
211
+ for i in range(len(selected_var)):
212
+ filename = f'./result/{mode}_{file}_{selected_var[i]}.png'
213
+ plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
214
+ print(f'✅plot {filename}')
scripts/train_long.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 numpy as np
10
+ import torch.distributed as dist
11
+ import logging
12
+ import time
13
+
14
+ from tqdm import tqdm
15
+ from torch.nn.parallel import DistributedDataParallel
16
+ from model.fuxi import Fuxi
17
+ from scripts.data_loader import ERA5Datapipe
18
+ from onescience.utils.YParams import YParams
19
+ from onescience.metrics.climate.loss import LatitudeWeightedLoss
20
+ from onescience.memory.checkpoint import replace_function
21
+
22
+ from apex import optimizers
23
+
24
+
25
+ def main():
26
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
27
+ logger = logging.getLogger()
28
+
29
+ ## Model config init
30
+ config_file_path = os.path.join(current_path, "conf/config.yaml")
31
+ cfg = YParams(config_file_path, "model")
32
+
33
+ ## Distributed config init
34
+ cfg.world_size = 1
35
+ if "WORLD_SIZE" in os.environ:
36
+ cfg.world_size = int(os.environ["WORLD_SIZE"])
37
+ world_rank = 0
38
+ local_rank = 0
39
+ if cfg.world_size > 1:
40
+ dist.init_process_group(backend="nccl", init_method="env://")
41
+ local_rank = int(os.environ["LOCAL_RANK"])
42
+ world_rank = dist.get_rank()
43
+ if not os.path.exists(f"{cfg.checkpoint_dir}/model_medium_bak.pth"):
44
+ if world_rank == 0:
45
+ print(f'❌❌The Fuxi medium model must be trained before this model.')
46
+ exit()
47
+ ## DataLoader init
48
+ cfg_data = YParams(config_file_path, "datapipe")
49
+ datapipe = ERA5Datapipe(
50
+ dataset_dir=cfg_data.dataset.data_dir,
51
+ used_variables=cfg_data.dataset.channels,
52
+ used_years=cfg_data.dataset.train_time,
53
+ pattern='long',
54
+ distributed=dist.is_initialized(),
55
+ output_steps=2,
56
+ input_steps=2,
57
+ batch_size=cfg_data.dataloader.batch_size,
58
+ num_workers=cfg_data.dataloader.num_workers
59
+ )
60
+ train_dataloader, train_sampler = datapipe.get_dataloader("train")
61
+ datapipe = ERA5Datapipe(
62
+ dataset_dir=cfg_data.dataset.data_dir,
63
+ used_variables=cfg_data.dataset.channels,
64
+ used_years=cfg_data.dataset.val_time,
65
+ pattern='long',
66
+ distributed=dist.is_initialized(),
67
+ output_steps=2,
68
+ input_steps=2,
69
+ batch_size=cfg_data.dataloader.batch_size,
70
+ num_workers=cfg_data.dataloader.num_workers
71
+ )
72
+ val_dataloader, val_sampler = datapipe.get_dataloader("valid")
73
+
74
+ ## Model init
75
+ model = Fuxi(img_size=cfg_data.dataset.img_size,
76
+ patch_size=cfg.patch_size,
77
+ in_chans=len(cfg_data.dataset.channels),
78
+ out_chans=len(cfg_data.dataset.channels),
79
+ embed_dim=cfg.embed_dim,
80
+ num_groups=cfg.num_groups,
81
+ num_heads=cfg.num_heads,
82
+ window_size=cfg.window_size
83
+ ).to(local_rank)
84
+ optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.train_lr)
85
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode="min")
86
+ loss_obj = LatitudeWeightedLoss(loss_type="l1", normalize=True).to(local_rank)
87
+
88
+ ## Train process init
89
+ os.makedirs(cfg.checkpoint_dir, exist_ok=True)
90
+ train_loss_file = f"{cfg.checkpoint_dir}/tr_long_loss.npy"
91
+ valid_loss_file = f"{cfg.checkpoint_dir}/va_long_loss.npy"
92
+ best_valid_loss = 1.0e6
93
+ best_loss_epoch = 0
94
+ train_losses = np.empty((0,), dtype=np.float32)
95
+ valid_losses = np.empty((0,), dtype=np.float32)
96
+ current_epoch = 0
97
+
98
+ ## Get model params count
99
+ if cfg.world_size == 1:
100
+ total_params = sum(p.numel() for p in model.parameters())
101
+ print("\n\n")
102
+ print("-" * 50)
103
+ print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
104
+ print("-" * 50, "\n")
105
+
106
+ ## Load model weight if there exist well-trained model
107
+
108
+ if not os.path.exists(f"{cfg.checkpoint_dir}/model_medium_bak.pth"):
109
+ print('⚠️ ⚠️ Please train to get medium model first...')
110
+ exit()
111
+
112
+ if os.path.exists(f"{cfg.checkpoint_dir}/model_long_bak.pth"):
113
+ if world_rank == 0:
114
+ print("\n\n")
115
+ print("-" * 50)
116
+ print(f"✅ There has a long-pattern model weight, load and continue training...")
117
+ print(f'If you want to finetune a new model, ensure there is no model_short_bak.pth file in {cfg.checkpoint_dir}')
118
+ print("-" * 50, "\n")
119
+ ckpt = torch.load(f"{cfg.checkpoint_dir}/model_long_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
120
+ model.load_state_dict(ckpt["model_state_dict"])
121
+ optimizer.load_state_dict(ckpt["optimizer_state_dict"])
122
+ scheduler.load_state_dict(ckpt["scheduler_state_dict"])
123
+ best_valid_loss = ckpt["best_valid_loss"]
124
+ best_loss_epoch = ckpt["best_loss_epoch"]
125
+ current_epoch = ckpt["current_epoch"]
126
+ train_losses = np.load(f"{cfg.checkpoint_dir}/tr_long_loss.npy")
127
+ valid_losses = np.load(f"{cfg.checkpoint_dir}/va_long_loss.npy")
128
+ else:
129
+ if world_rank == 0:
130
+ print("\n\n")
131
+ print("-" * 50)
132
+ print(f"✅ Load medium model and continue to finetune...")
133
+ print("-" * 50, "\n")
134
+ ckpt = torch.load(f"{cfg.checkpoint_dir}/model_medium_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
135
+ model.load_state_dict(ckpt["model_state_dict"])
136
+ optimizer.load_state_dict(ckpt["optimizer_state_dict"])
137
+ scheduler.load_state_dict(ckpt["scheduler_state_dict"])
138
+
139
+ ## Distributed model
140
+ if cfg.world_size > 1:
141
+ model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
142
+
143
+ world_rank == 0 and logger.info(f"start training ...")
144
+
145
+ for epoch in range(current_epoch, cfg.finetune_step):
146
+ if epoch > cfg.step_change_freq:
147
+ num_rollout_steps = epoch // cfg.step_change_freq + 2
148
+ if num_rollout_steps > 12: # Paper: 2~12 curriculum training schedule, then skip to 20.
149
+ num_rollout_steps = cfg.long_num_steps - cfg.medium_num_steps
150
+ if epoch % cfg.step_change_freq == 0 and world_rank == 0:
151
+ logger.info(f"⚠️ ⚠️ Switching to {num_rollout_steps}-step rollout!")
152
+ datapipe = ERA5Datapipe(
153
+ dataset_dir=cfg_data.dataset.data_dir,
154
+ used_variables=cfg_data.dataset.channels,
155
+ used_years=cfg_data.dataset.train_time,
156
+ pattern='long',
157
+ distributed=dist.is_initialized(),
158
+ output_steps=num_rollout_steps,
159
+ input_steps=2,
160
+ batch_size=cfg_data.dataloader.batch_size,
161
+ num_workers=cfg_data.dataloader.num_workers
162
+ )
163
+ train_dataloader, train_sampler = datapipe.get_dataloader("train")
164
+ datapipe = ERA5Datapipe(
165
+ dataset_dir=cfg_data.dataset.data_dir,
166
+ used_variables=cfg_data.dataset.channels,
167
+ used_years=cfg_data.dataset.val_time,
168
+ pattern='long',
169
+ distributed=dist.is_initialized(),
170
+ output_steps=num_rollout_steps,
171
+ input_steps=2,
172
+ batch_size=cfg_data.dataloader.batch_size,
173
+ num_workers=cfg_data.dataloader.num_workers
174
+ )
175
+ val_dataloader, val_sampler = datapipe.get_dataloader("valid")
176
+
177
+ if dist.is_initialized():
178
+ train_sampler.set_epoch(epoch)
179
+ val_sampler.set_epoch(epoch)
180
+
181
+ model.train()
182
+ train_loss = 0
183
+ start_time = time.time()
184
+ for j, data in enumerate(train_dataloader):
185
+ invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
186
+ invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
187
+ outvar = data[1].to(local_rank, dtype=torch.float32)
188
+ for t in range(outvar.shape[1]):
189
+ if t < outvar.shape[1] - 1:
190
+ with torch.no_grad():
191
+ outvar_pred = model(invar)
192
+ # B, 70, 2, 721, 1440
193
+ invar[:, :, 0] = invar[:, :, -1]
194
+ invar[:, :, -1] = outvar_pred.detach()
195
+ else:
196
+ with replace_function(model, ["cube_embedding", "u_transformer"], cfg.world_size > 1):
197
+ outvar_pred = model(invar)
198
+ loss = loss_obj(outvar_pred, outvar[:, t])
199
+ optimizer.zero_grad()
200
+ loss.backward()
201
+ optimizer.step()
202
+ train_loss += loss.item()
203
+ if world_rank == 0:
204
+ logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
205
+ f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
206
+ f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
207
+ f'loss:{train_loss / (j+1): .04f}')
208
+
209
+ train_loss /= len(train_dataloader)
210
+
211
+ model.eval()
212
+ valid_loss = 0
213
+ with torch.no_grad():
214
+ start_time = time.time()
215
+ for j, data in enumerate(val_dataloader):
216
+ invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
217
+ invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
218
+ outvar = data[1].to(local_rank, dtype=torch.float32)
219
+ for t in range(outvar.shape[1]):
220
+ outvar_pred = model(invar)
221
+ # B, 70, 2, 721, 1440
222
+ invar[:, :, 0] = invar[:, :, -1]
223
+ invar[:, :, -1] = outvar_pred.detach()
224
+ loss = loss_obj(outvar_pred, outvar[:, -1])
225
+
226
+ if cfg.world_size > 1:
227
+ loss_tensor = loss.detach().to(local_rank)
228
+ dist.all_reduce(loss_tensor)
229
+ loss = loss_tensor.item() / cfg.world_size
230
+ valid_loss += loss
231
+ else:
232
+ valid_loss += loss.item()
233
+ if world_rank == 0:
234
+ logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
235
+ f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
236
+ f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
237
+ f'loss:{valid_loss / (j+1): .04f}')
238
+
239
+ valid_loss /= len(val_dataloader)
240
+ is_save_ckp = False
241
+ if valid_loss < best_valid_loss:
242
+ best_valid_loss = valid_loss
243
+ best_loss_epoch = epoch
244
+ world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir, epoch)
245
+ is_save_ckp = True
246
+
247
+ scheduler.step(valid_loss)
248
+
249
+ if world_rank == 0:
250
+ logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
251
+ f"Train Loss: {train_loss:.4f}, "
252
+ f"Valid Loss: {valid_loss:.4f}, "
253
+ f"Best loss at Epoch: {best_loss_epoch + 1}"
254
+ + (", saving checkpoint" if is_save_ckp else "")
255
+ )
256
+ train_losses = np.append(train_losses, train_loss)
257
+ valid_losses = np.append(valid_losses, valid_loss)
258
+
259
+ np.save(train_loss_file, train_losses)
260
+ np.save(valid_loss_file, valid_losses)
261
+
262
+ if epoch - best_loss_epoch > cfg.patience:
263
+ print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
264
+ exit()
265
+
266
+
267
+ def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path, epoch):
268
+ model_to_save = model.module if hasattr(model, "module") else model
269
+ state = {"model_state_dict": model_to_save.state_dict(),
270
+ "optimizer_state_dict": optimizer.state_dict(),
271
+ "scheduler_state_dict": scheduler.state_dict(),
272
+ "best_valid_loss": best_valid_loss,
273
+ "best_loss_epoch": best_loss_epoch,
274
+ "current_epoch": epoch
275
+ }
276
+ torch.save(state, f"{model_path}/model_long.pth")
277
+ ### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
278
+ os.system(f"mv {model_path}/model_long.pth {model_path}/model_long_bak.pth")
279
+
280
+
281
+ if __name__ == "__main__":
282
+ current_path = os.getcwd()
283
+ sys.path.append(current_path)
284
+ main()
scripts/train_medium.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 numpy as np
10
+ import torch.distributed as dist
11
+ import logging
12
+ import time
13
+
14
+ from tqdm import tqdm
15
+ from torch.nn.parallel import DistributedDataParallel
16
+ from model.fuxi import Fuxi
17
+ from scripts.data_loader import ERA5Datapipe
18
+ from onescience.utils.YParams import YParams
19
+ from onescience.metrics.climate.loss import LatitudeWeightedLoss
20
+ from onescience.memory.checkpoint import replace_function
21
+
22
+ from apex import optimizers
23
+
24
+
25
+ def main():
26
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
27
+ logger = logging.getLogger()
28
+
29
+ ## Model config init
30
+ config_file_path = os.path.join(current_path, "conf/config.yaml")
31
+ cfg = YParams(config_file_path, "model")
32
+
33
+ ## Distributed config init
34
+ cfg.world_size = 1
35
+ if "WORLD_SIZE" in os.environ:
36
+ cfg.world_size = int(os.environ["WORLD_SIZE"])
37
+ world_rank = 0
38
+ local_rank = 0
39
+ if cfg.world_size > 1:
40
+ dist.init_process_group(backend="nccl", init_method="env://")
41
+ local_rank = int(os.environ["LOCAL_RANK"])
42
+ world_rank = dist.get_rank()
43
+ if not os.path.exists(f"{cfg.checkpoint_dir}/model_short_bak.pth"):
44
+ if world_rank == 0:
45
+ print(f'❌❌The Fuxi short model must be trained before this model.')
46
+ exit()
47
+ ## DataLoader init
48
+ cfg_data = YParams(config_file_path, "datapipe")
49
+ datapipe = ERA5Datapipe(
50
+ dataset_dir=cfg_data.dataset.data_dir,
51
+ used_variables=cfg_data.dataset.channels,
52
+ used_years=cfg_data.dataset.train_time,
53
+ pattern='medium',
54
+ distributed=dist.is_initialized(),
55
+ output_steps=2,
56
+ input_steps=2,
57
+ batch_size=cfg_data.dataloader.batch_size,
58
+ num_workers=cfg_data.dataloader.num_workers
59
+ )
60
+ train_dataloader, train_sampler = datapipe.get_dataloader("train")
61
+ datapipe = ERA5Datapipe(
62
+ dataset_dir=cfg_data.dataset.data_dir,
63
+ used_variables=cfg_data.dataset.channels,
64
+ used_years=cfg_data.dataset.val_time,
65
+ pattern='medium',
66
+ distributed=dist.is_initialized(),
67
+ output_steps=2,
68
+ input_steps=2,
69
+ batch_size=cfg_data.dataloader.batch_size,
70
+ num_workers=cfg_data.dataloader.num_workers
71
+ )
72
+ val_dataloader, val_sampler = datapipe.get_dataloader("valid")
73
+
74
+ ## Model init
75
+ model = Fuxi(img_size=cfg_data.dataset.img_size,
76
+ patch_size=cfg.patch_size,
77
+ in_chans=len(cfg_data.dataset.channels),
78
+ out_chans=len(cfg_data.dataset.channels),
79
+ embed_dim=cfg.embed_dim,
80
+ num_groups=cfg.num_groups,
81
+ num_heads=cfg.num_heads,
82
+ window_size=cfg.window_size
83
+ ).to(local_rank)
84
+ optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.train_lr)
85
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode="min")
86
+ loss_obj = LatitudeWeightedLoss(loss_type="l1", normalize=True).to(local_rank)
87
+
88
+ ## Train process init
89
+ os.makedirs(cfg.checkpoint_dir, exist_ok=True)
90
+ train_loss_file = f"{cfg.checkpoint_dir}/tr_medium_loss.npy"
91
+ valid_loss_file = f"{cfg.checkpoint_dir}/va_medium_loss.npy"
92
+ best_valid_loss = 1.0e6
93
+ best_loss_epoch = 0
94
+ train_losses = np.empty((0,), dtype=np.float32)
95
+ valid_losses = np.empty((0,), dtype=np.float32)
96
+ current_epoch = 0
97
+
98
+ ## Get model params count
99
+ if cfg.world_size == 1:
100
+ total_params = sum(p.numel() for p in model.parameters())
101
+ print("\n\n")
102
+ print("-" * 50)
103
+ print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
104
+ print("-" * 50, "\n")
105
+
106
+ ## Load model weight if there exist well-trained model
107
+
108
+ ## Load model weight if there exist well-trained model
109
+ if not os.path.exists(f"{cfg.checkpoint_dir}/model_short_bak.pth"):
110
+ print('⚠️ ⚠️ Please train to get short model first...')
111
+ exit()
112
+
113
+ if os.path.exists(f"{cfg.checkpoint_dir}/model_medium_bak.pth"):
114
+ if world_rank == 0:
115
+ print("\n\n")
116
+ print("-" * 50)
117
+ print(f"✅ There has a medium-pattern model weight, load and continue training...")
118
+ print(f'If you want to finetune a new model, ensure there is no model_short_bak.pth file in {cfg.checkpoint_dir}')
119
+ print("-" * 50, "\n")
120
+ ckpt = torch.load(f"{cfg.checkpoint_dir}/model_medium_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
121
+ model.load_state_dict(ckpt["model_state_dict"])
122
+ optimizer.load_state_dict(ckpt["optimizer_state_dict"])
123
+ scheduler.load_state_dict(ckpt["scheduler_state_dict"])
124
+ best_valid_loss = ckpt["best_valid_loss"]
125
+ best_loss_epoch = ckpt["best_loss_epoch"]
126
+ current_epoch = ckpt["current_epoch"]
127
+ train_losses = np.load(f"{cfg.checkpoint_dir}/tr_medium_loss.npy")
128
+ valid_losses = np.load(f"{cfg.checkpoint_dir}/va_medium_loss.npy")
129
+ else:
130
+ if world_rank == 0:
131
+ print("\n\n")
132
+ print("-" * 50)
133
+ print(f"✅ Load short model and continue to finetune...")
134
+ print("-" * 50, "\n")
135
+ ckpt = torch.load(f"{cfg.checkpoint_dir}/model_short_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
136
+ model.load_state_dict(ckpt["model_state_dict"])
137
+ optimizer.load_state_dict(ckpt["optimizer_state_dict"])
138
+ scheduler.load_state_dict(ckpt["scheduler_state_dict"])
139
+
140
+
141
+ ## Distributed model
142
+ if cfg.world_size > 1:
143
+ model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
144
+
145
+ world_rank == 0 and logger.info(f"start training ...")
146
+
147
+ for epoch in range(current_epoch, cfg.finetune_step):
148
+ if epoch % cfg.step_change_freq == 0:
149
+ num_rollout_steps = epoch // cfg.step_change_freq + 2
150
+ if num_rollout_steps > 12: # Paper: 2~12 curriculum training schedule, then skip to 20.
151
+ num_rollout_steps = cfg.medium_num_steps - cfg.short_num_steps
152
+ world_rank == 0 and logger.info(f"⚠️ ⚠️ Switching to {num_rollout_steps}-step rollout!")
153
+ datapipe = ERA5Datapipe(
154
+ dataset_dir=cfg_data.dataset.data_dir,
155
+ used_variables=cfg_data.dataset.channels,
156
+ used_years=cfg_data.dataset.train_time,
157
+ pattern='medium',
158
+ distributed=dist.is_initialized(),
159
+ output_steps=num_rollout_steps,
160
+ input_steps=2,
161
+ batch_size=cfg_data.dataloader.batch_size,
162
+ num_workers=cfg_data.dataloader.num_workers
163
+ )
164
+ train_dataloader, train_sampler = datapipe.get_dataloader("train")
165
+ datapipe = ERA5Datapipe(
166
+ dataset_dir=cfg_data.dataset.data_dir,
167
+ used_variables=cfg_data.dataset.channels,
168
+ used_years=cfg_data.dataset.val_time,
169
+ pattern='medium',
170
+ distributed=dist.is_initialized(),
171
+ output_steps=num_rollout_steps,
172
+ input_steps=2,
173
+ batch_size=cfg_data.dataloader.batch_size,
174
+ num_workers=cfg_data.dataloader.num_workers
175
+ )
176
+ val_dataloader, val_sampler = datapipe.get_dataloader("valid")
177
+
178
+ if dist.is_initialized():
179
+ train_sampler.set_epoch(epoch)
180
+ val_sampler.set_epoch(epoch)
181
+
182
+ model.train()
183
+ train_loss = 0
184
+ start_time = time.time()
185
+ for j, data in enumerate(train_dataloader):
186
+ invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
187
+ invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
188
+ outvar = data[1].to(local_rank, dtype=torch.float32)
189
+ for t in range(outvar.shape[1]):
190
+ if t < outvar.shape[1] - 1:
191
+ with torch.no_grad():
192
+ outvar_pred = model(invar)
193
+ # B, 70, 2, 721, 1440
194
+ invar[:, :, 0] = invar[:, :, -1]
195
+ invar[:, :, -1] = outvar_pred.detach()
196
+ else:
197
+ with replace_function(model, ["cube_embedding", "u_transformer"], cfg.world_size > 1):
198
+ outvar_pred = model(invar)
199
+ loss = loss_obj(outvar_pred, outvar[:, t])
200
+ optimizer.zero_grad()
201
+ loss.backward()
202
+ optimizer.step()
203
+ train_loss += loss.item()
204
+ if world_rank == 0:
205
+ logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
206
+ f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
207
+ f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
208
+ f'loss:{train_loss / (j+1): .04f}')
209
+
210
+ train_loss /= len(train_dataloader)
211
+
212
+ model.eval()
213
+ valid_loss = 0
214
+ with torch.no_grad():
215
+ start_time = time.time()
216
+ for j, data in enumerate(val_dataloader):
217
+ invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
218
+ invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
219
+ outvar = data[1].to(local_rank, dtype=torch.float32)
220
+ for t in range(outvar.shape[1]):
221
+ outvar_pred = model(invar)
222
+ # B, 70, 2, 721, 1440
223
+ invar[:, :, 0] = invar[:, :, -1]
224
+ invar[:, :, -1] = outvar_pred.detach()
225
+ loss = loss_obj(outvar_pred, outvar[:, -1])
226
+
227
+ if cfg.world_size > 1:
228
+ loss_tensor = loss.detach().to(local_rank)
229
+ dist.all_reduce(loss_tensor)
230
+ loss = loss_tensor.item() / cfg.world_size
231
+ valid_loss += loss
232
+ else:
233
+ valid_loss += loss.item()
234
+ if world_rank == 0:
235
+ logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
236
+ f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
237
+ f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
238
+ f'loss:{valid_loss / (j+1): .04f}')
239
+
240
+ valid_loss /= len(val_dataloader)
241
+ is_save_ckp = False
242
+ if valid_loss < best_valid_loss:
243
+ best_valid_loss = valid_loss
244
+ best_loss_epoch = epoch
245
+ world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir, epoch)
246
+ is_save_ckp = True
247
+
248
+ scheduler.step(valid_loss)
249
+
250
+ if world_rank == 0:
251
+ logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
252
+ f"Train Loss: {train_loss:.4f}, "
253
+ f"Valid Loss: {valid_loss:.4f}, "
254
+ f"Best loss at Epoch: {best_loss_epoch + 1}"
255
+ + (", saving checkpoint" if is_save_ckp else "")
256
+ )
257
+ train_losses = np.append(train_losses, train_loss)
258
+ valid_losses = np.append(valid_losses, valid_loss)
259
+
260
+ np.save(train_loss_file, train_losses)
261
+ np.save(valid_loss_file, valid_losses)
262
+
263
+ if epoch - best_loss_epoch > cfg.patience:
264
+ print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
265
+ exit()
266
+
267
+
268
+ def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path, epoch):
269
+ model_to_save = model.module if hasattr(model, "module") else model
270
+ state = {"model_state_dict": model_to_save.state_dict(),
271
+ "optimizer_state_dict": optimizer.state_dict(),
272
+ "scheduler_state_dict": scheduler.state_dict(),
273
+ "best_valid_loss": best_valid_loss,
274
+ "best_loss_epoch": best_loss_epoch,
275
+ "current_epoch": epoch
276
+ }
277
+ torch.save(state, f"{model_path}/model_medium.pth")
278
+ ### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
279
+ os.system(f"mv {model_path}/model_medium.pth {model_path}/model_medium_bak.pth")
280
+
281
+
282
+ if __name__ == "__main__":
283
+ current_path = os.getcwd()
284
+ sys.path.append(current_path)
285
+ main()
scripts/train_short.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 numpy as np
10
+ import torch.distributed as dist
11
+ import logging
12
+ import time
13
+
14
+ from tqdm import tqdm
15
+ from torch.nn.parallel import DistributedDataParallel
16
+ from model.fuxi import Fuxi
17
+ from onescience.datapipes.climate import ERA5Datapipe
18
+ from onescience.utils.YParams import YParams
19
+ from onescience.metrics.climate.loss import LatitudeWeightedLoss
20
+ from onescience.memory.checkpoint import replace_function
21
+
22
+ from apex import optimizers
23
+
24
+
25
+ def main():
26
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
27
+ logger = logging.getLogger()
28
+
29
+ ## Model config init
30
+ config_file_path = os.path.join(current_path, "conf/config.yaml")
31
+ cfg = YParams(config_file_path, "model")
32
+
33
+ ## Distributed config init
34
+ cfg.world_size = 1
35
+ if "WORLD_SIZE" in os.environ:
36
+ cfg.world_size = int(os.environ["WORLD_SIZE"])
37
+ world_rank = 0
38
+ local_rank = 0
39
+ if cfg.world_size > 1:
40
+ dist.init_process_group(backend="nccl", init_method="env://")
41
+ local_rank = int(os.environ["LOCAL_RANK"])
42
+ world_rank = dist.get_rank()
43
+
44
+ ## DataLoader init
45
+ cfg_data = YParams(config_file_path, "datapipe")
46
+ datapipe = ERA5Datapipe(
47
+ dataset_dir=cfg_data.dataset.data_dir,
48
+ used_variables=cfg_data.dataset.channels,
49
+ used_years=cfg_data.dataset.train_time,
50
+ distributed=dist.is_initialized(),
51
+ output_steps=2,
52
+ input_steps=2,
53
+ batch_size=cfg_data.dataloader.batch_size,
54
+ num_workers=cfg_data.dataloader.num_workers
55
+ )
56
+ train_dataloader, train_sampler = datapipe.get_dataloader("train")
57
+ datapipe = ERA5Datapipe(
58
+ dataset_dir=cfg_data.dataset.data_dir,
59
+ used_variables=cfg_data.dataset.channels,
60
+ used_years=cfg_data.dataset.val_time,
61
+ distributed=dist.is_initialized(),
62
+ output_steps=2,
63
+ input_steps=2,
64
+ batch_size=cfg_data.dataloader.batch_size,
65
+ num_workers=cfg_data.dataloader.num_workers
66
+ )
67
+ val_dataloader, val_sampler = datapipe.get_dataloader("valid")
68
+
69
+ ## Model init
70
+ model = Fuxi(img_size=cfg_data.dataset.img_size,
71
+ patch_size=cfg.patch_size,
72
+ in_chans=len(cfg_data.dataset.channels),
73
+ out_chans=len(cfg_data.dataset.channels),
74
+ embed_dim=cfg.embed_dim,
75
+ num_groups=cfg.num_groups,
76
+ num_heads=cfg.num_heads,
77
+ window_size=cfg.window_size
78
+ ).to(local_rank)
79
+ optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.train_lr)
80
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode="min")
81
+ loss_obj = LatitudeWeightedLoss(loss_type="l1", normalize=True).to(local_rank)
82
+
83
+ ## Train process init
84
+ os.makedirs(cfg.checkpoint_dir, exist_ok=True)
85
+ train_loss_file = f"{cfg.checkpoint_dir}/tr_short_loss.npy"
86
+ valid_loss_file = f"{cfg.checkpoint_dir}/va_short_loss.npy"
87
+ best_valid_loss = 1.0e6
88
+ best_loss_epoch = 0
89
+ train_losses = np.empty((0,), dtype=np.float32)
90
+ valid_losses = np.empty((0,), dtype=np.float32)
91
+ current_epoch = 0
92
+
93
+ ## Get model params count
94
+ if cfg.world_size == 1:
95
+ total_params = sum(p.numel() for p in model.parameters())
96
+ print("\n\n")
97
+ print("-" * 50)
98
+ print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
99
+ print("-" * 50, "\n")
100
+
101
+ ## Load model weight if there exist well-trained model
102
+ if os.path.exists(f"{cfg.checkpoint_dir}/model_short_bak.pth"):
103
+ if world_rank == 0:
104
+ print("\n\n")
105
+ print("-" * 50)
106
+ print(f"✅ There has a short-pattern model weight, load and continue training...")
107
+ print(f'If you want to finetune a new model, ensure there is no model_short_bak.pth file in {cfg.checkpoint_dir}')
108
+ print("-" * 50, "\n")
109
+ ckpt = torch.load(f"{cfg.checkpoint_dir}/model_short_bak.pth", map_location=f'cuda:{local_rank}', weights_only=False)
110
+ model.load_state_dict(ckpt["model_state_dict"])
111
+ optimizer.load_state_dict(ckpt["optimizer_state_dict"])
112
+ scheduler.load_state_dict(ckpt["scheduler_state_dict"])
113
+ best_valid_loss = ckpt["best_valid_loss"]
114
+ best_loss_epoch = ckpt["best_loss_epoch"]
115
+ current_epoch = ckpt["current_epoch"]
116
+ train_losses = np.load(f"{cfg.checkpoint_dir}/tr_short_loss.npy")
117
+ valid_losses = np.load(f"{cfg.checkpoint_dir}/va_short_loss.npy")
118
+ else:
119
+ if world_rank == 0:
120
+ print("\n\n")
121
+ print("-" * 50)
122
+ print(f"✅ No checkpoint found, training short model from scratch...")
123
+ print("-" * 50, "\n")
124
+
125
+
126
+ ## Distributed model
127
+ if cfg.world_size > 1:
128
+ model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank, find_unused_parameters=True)
129
+
130
+ world_rank == 0 and logger.info(f"start training ...")
131
+
132
+ for epoch in range(current_epoch, cfg.finetune_step):
133
+ if epoch > cfg.step_change_freq:
134
+ num_rollout_steps = epoch // cfg.step_change_freq + 2
135
+ if num_rollout_steps > 12: # Paper: 2~12 curriculum training schedule, then skip to 20.
136
+ num_rollout_steps = cfg.short_num_steps
137
+ if epoch % cfg.step_change_freq == 0 and world_rank == 0:
138
+ logger.info(f"⚠️ ⚠️ Switching to {num_rollout_steps}-step rollout!")
139
+ datapipe = ERA5Datapipe(
140
+ dataset_dir=cfg_data.dataset.data_dir,
141
+ used_variables=cfg_data.dataset.channels,
142
+ used_years=cfg_data.dataset.train_time,
143
+ distributed=dist.is_initialized(),
144
+ output_steps=num_rollout_steps,
145
+ input_steps=2,
146
+ batch_size=cfg_data.dataloader.batch_size,
147
+ num_workers=cfg_data.dataloader.num_workers
148
+ )
149
+ train_dataloader, train_sampler = datapipe.get_dataloader("train")
150
+ datapipe = ERA5Datapipe(
151
+ dataset_dir=cfg_data.dataset.data_dir,
152
+ used_variables=cfg_data.dataset.channels,
153
+ used_years=cfg_data.dataset.val_time,
154
+ distributed=dist.is_initialized(),
155
+ output_steps=num_rollout_steps,
156
+ input_steps=2,
157
+ batch_size=cfg_data.dataloader.batch_size,
158
+ num_workers=cfg_data.dataloader.num_workers
159
+ )
160
+ val_dataloader, val_sampler = datapipe.get_dataloader("valid")
161
+
162
+ if dist.is_initialized():
163
+ train_sampler.set_epoch(epoch)
164
+ val_sampler.set_epoch(epoch)
165
+ model.train()
166
+ train_loss = 0
167
+ start_time = time.time()
168
+ for j, data in enumerate(train_dataloader):
169
+ invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
170
+ invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
171
+ outvar = data[1].to(local_rank, dtype=torch.float32)
172
+ for t in range(outvar.shape[1]):
173
+ if t < outvar.shape[1] - 1:
174
+ with torch.no_grad():
175
+ outvar_pred = model(invar)
176
+ # B, 70, 2, 721, 1440
177
+ invar[:, :, 0] = invar[:, :, -1]
178
+ invar[:, :, -1] = outvar_pred.detach()
179
+ else:
180
+ with replace_function(model, ["cube_embedding", "u_transformer"], cfg.world_size > 1):
181
+ outvar_pred = model(invar)
182
+ loss = loss_obj(outvar_pred, outvar[:, t])
183
+
184
+ optimizer.zero_grad()
185
+ loss.backward()
186
+ optimizer.step()
187
+ train_loss += loss.item()
188
+ if world_rank == 0:
189
+ logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
190
+ f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
191
+ f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
192
+ f'loss:{train_loss / (j+1): .04f}')
193
+ train_loss /= len(train_dataloader)
194
+
195
+ model.eval()
196
+ valid_loss = 0
197
+ with torch.no_grad():
198
+ start_time = time.time()
199
+ for j, data in enumerate(val_dataloader):
200
+ invar = data[0].to(local_rank, dtype=torch.float32) # B, T, C, H, W
201
+ invar = invar.permute(0, 2, 1, 3, 4) # B, C, T, H, W
202
+ outvar = data[1].to(local_rank, dtype=torch.float32)
203
+ for t in range(outvar.shape[1]):
204
+ outvar_pred = model(invar)
205
+ # B, 70, 2, 721, 1440
206
+ invar[:, :, 0] = invar[:, :, -1]
207
+ invar[:, :, -1] = outvar_pred.detach()
208
+ loss = loss_obj(outvar_pred, outvar[:, -1])
209
+
210
+ if cfg.world_size > 1:
211
+ loss_tensor = loss.detach().to(local_rank)
212
+ dist.all_reduce(loss_tensor)
213
+ loss = loss_tensor.item() / cfg.world_size
214
+ valid_loss += loss
215
+ else:
216
+ valid_loss += loss.item()
217
+ if world_rank == 0:
218
+ logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
219
+ f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
220
+ f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
221
+ f'loss:{valid_loss / (j+1): .04f}')
222
+ valid_loss /= len(val_dataloader)
223
+ is_save_ckp = False
224
+ if valid_loss < best_valid_loss:
225
+ best_valid_loss = valid_loss
226
+ best_loss_epoch = epoch
227
+ world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir, epoch)
228
+ is_save_ckp = True
229
+
230
+ scheduler.step(valid_loss)
231
+
232
+ if world_rank == 0:
233
+ logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
234
+ f"Train Loss: {train_loss:.4f}, "
235
+ f"Valid Loss: {valid_loss:.4f}, "
236
+ f"Best loss at Epoch: {best_loss_epoch + 1}"
237
+ + (", saving checkpoint" if is_save_ckp else "")
238
+ )
239
+ train_losses = np.append(train_losses, train_loss)
240
+ valid_losses = np.append(valid_losses, valid_loss)
241
+
242
+ np.save(train_loss_file, train_losses)
243
+ np.save(valid_loss_file, valid_losses)
244
+
245
+ if epoch - best_loss_epoch > cfg.patience:
246
+ print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
247
+ exit()
248
+
249
+
250
+ def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path, epoch):
251
+ model_to_save = model.module if hasattr(model, "module") else model
252
+ state = {"model_state_dict": model_to_save.state_dict(),
253
+ "optimizer_state_dict": optimizer.state_dict(),
254
+ "scheduler_state_dict": scheduler.state_dict(),
255
+ "best_valid_loss": best_valid_loss,
256
+ "best_loss_epoch": best_loss_epoch,
257
+ "current_epoch": epoch
258
+ }
259
+ torch.save(state, f"{model_path}/model_short.pth")
260
+ ### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
261
+ os.system(f"mv {model_path}/model_short.pth {model_path}/model_short_bak.pth")
262
+
263
+
264
+ if __name__ == "__main__":
265
+ current_path = os.getcwd()
266
+ sys.path.append(current_path)
267
+ main()
weight/.gitkeep ADDED
File without changes