Upload folder using huggingface_hub
Browse files- .DS_Store +0 -0
- .ms_upload_cache +1 -0
- LICENSE +21 -0
- README.md +180 -0
- conf/config.yaml +73 -0
- config.json +116 -0
- configuration.json +1 -0
- model/__pycache__/prithvi_wxc.cpython-311.pyc +0 -0
- model/__pycache__/prithvi_wxc_official.cpython-311.pyc +0 -0
- model/prithvi_wxc.py +144 -0
- model/prithvi_wxc_official.py +1637 -0
- scripts/fake_data.py +118 -0
- scripts/inference.py +104 -0
- scripts/result.py +200 -0
- scripts/train.py +243 -0
- weight/.gitkeep +0 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.ms_upload_cache
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"version": 3, "repo_id": "OneScience/PrithviWxC", "files": {"README.md|1787118190.0|6111": {"hash": "4644dd18dcfd1c681633df078cd60b87a647c9cdf9505c83e0b9f6e03af55faf", "size": 6111, "status": "c"}, "conf/config.yaml|1787189055.0|2484": {"hash": "dc0ae83fd9cb50c61ce7a15fea77481f85c1f587fece5e63a5127a82ba273a67", "size": 2484, "status": "c"}, "model/__pycache__/prithvi_wxc.cpython-311.pyc|1787129097.0|6399": {"hash": "001025caa3762a8c91555f64ca871e687d234518abc23382206b83e0b3881978", "size": 6399, "status": "c"}, "model/__pycache__/prithvi_wxc_official.cpython-311.pyc|1787129243.0|76780": {"hash": "76c3889a828a1ee60630b10ea5a61dce9e25e8ef045fd71e2d150349f77e48ed", "size": 76780, "status": "c"}, "model/prithvi_wxc_official.py|1787129237.0|61202": {"hash": "58a3712e4739889be50dcb43106e7bfa43ed3c10e1ff44422a8192d6f343f992", "size": 61202, "status": "c"}, "configuration.json|1787037409.0|38": {"hash": "97d4072fc4a7a3b71e3184609fc491e77a324a982d9df860d9379c009dff84de", "size": 38, "status": "c"}, "LICENSE|1787037359.0|1106": {"hash": "c681a18eea18d4a07e4f429ca50d3f874c9d7982bb4836fff613218dfe47818e", "size": 1106, "status": "c"}, "model/prithvi_wxc.py|1787037757.0|5712": {"hash": "9f4186ae8f902dfa1b606c4ef53731c4923fda7c212a99941167f65ce37360d3", "size": 5712, "status": "c"}, "scripts/fake_data.py|1787118238.0|4985": {"hash": "b16a395251bffa0ee2948643d66c5d40a89fb63b397c23ec8703cd90f97f2859", "size": 4985, "status": "c"}, "scripts/inference.py|1787118190.0|4127": {"hash": "07920093609f16618001ef9a138ac1b843dd8985c1abfdc8accedf18518bec04", "size": 4127, "status": "c"}, "scripts/result.py|1787129368.0|8391": {"hash": "9a5b66ed2b41bbb766b3ce26a286be904d6d2483d8fde2ada6ea6c6044e6aed0", "size": 8391, "status": "c"}, "scripts/train.py|1787118190.0|10421": {"hash": "af5547fc972472f51bfcc719da9f6d1746492428e03a73345e2f23ac41f635b8", "size": 10421, "status": "c"}, "weight/.gitkeep|1787037589.0|0": {"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 0, "status": "c"}}}
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 Inter Agency Implementation and Advanced Concepts
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
- zh
|
| 6 |
+
tags:
|
| 7 |
+
- OneScience
|
| 8 |
+
- Earth science
|
| 9 |
+
- Weather forecasting
|
| 10 |
+
- Medium- to long-range weather forecasting
|
| 11 |
+
- Foundation models
|
| 12 |
+
- Vision Transformer
|
| 13 |
+
- ERA5
|
| 14 |
+
frameworks: PyTorch
|
| 15 |
+
datasets:
|
| 16 |
+
- OneScience/ERA5
|
| 17 |
+
---
|
| 18 |
+
<p align="center">
|
| 19 |
+
<strong>
|
| 20 |
+
<span style="font-size: 30px;">Prithvi WxC</span>
|
| 21 |
+
</strong>
|
| 22 |
+
</p>
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# Model Introduction
|
| 26 |
+
|
| 27 |
+
Prithvi WxC (Weather and Climate) was proposed by NASA-IMPACT, IBM, and other teams. It is a weather and climate foundation model based on a Vision Transformer (alternating local/global attention with Hiera and MaxViT), supporting forecasting (6-hour-step rollout) and climate simulation (internal error growth).
|
| 28 |
+
|
| 29 |
+
Paper:Prithvi WxC: Foundation Model for Weather and Climate
|
| 30 |
+
|
| 31 |
+
https://arxiv.org/abs/2409.13598
|
| 32 |
+
|
| 33 |
+
# Model Description
|
| 34 |
+
|
| 35 |
+
Prithvi WxC is a deterministic global weather foundation model: it takes atmospheric states at two consecutive 6-hour time steps, optionally with static fields, and outputs the target state. Longer lead times are obtained through autoregressive rollout.This repository is organized from the official `NASA-IMPACT/Prithvi-WxC` implementation and integrated with the OneScience data loading and training workflow.
|
| 36 |
+
|
| 37 |
+
# Use Cases
|
| 38 |
+
|
| 39 |
+
| Scenario | Description |
|
| 40 |
+
| :---: | :--- |
|
| 41 |
+
| Global weather and climate foundation model research | Train or fine-tune a Vision Transformer forecasting model on ERA5 data. |
|
| 42 |
+
| Long-horizon autoregressive rollout | Generate medium- to long-range forecasts autoregressively at 6-hour intervals. |
|
| 43 |
+
| Local quick validation | Use synthetic data to check data loading, training, inference, and result scripts. |
|
| 44 |
+
| ModelScope/OneCode execution | Download the model package, install dependencies, and run the scripts directly. |
|
| 45 |
+
| Multi-card training | Launch multi-process training with `torchrun`. |
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# Usage
|
| 49 |
+
|
| 50 |
+
## 1. OneCode Usage
|
| 51 |
+
|
| 52 |
+
Use the OneCode online environment for intelligent one-click AI4S programming:
|
| 53 |
+
|
| 54 |
+
[Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 55 |
+
|
| 56 |
+
## 2. Manual Installation and Usage
|
| 57 |
+
|
| 58 |
+
**Hardware Requirements**
|
| 59 |
+
|
| 60 |
+
- GPU or DCU is recommended.
|
| 61 |
+
- CPU can be used for imports and small-configuration connectivity validation, but full training and inference are slower.
|
| 62 |
+
- DCU users must install DTK beforehand. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
|
| 63 |
+
- The paper-level configuration (`embed_dim=2560`, 25 encoder blocks, 5 decoder blocks, and approximately 2.3 billion parameters) requires substantial GPU memory.
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
### Download the Model Package
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
hf download OneScience-Group/PrithviWxC --local-dir ./PrithviWxC
|
| 70 |
+
cd PrithviWxC
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Install the Runtime Environment
|
| 74 |
+
|
| 75 |
+
**DCU Environment**
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
# Activate DTK and CONDA first
|
| 79 |
+
conda create -n onescience311 python=3.11 -y
|
| 80 |
+
conda activate onescience311
|
| 81 |
+
# uv installation is supported
|
| 82 |
+
pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
**GPU Environment**
|
| 86 |
+
```bash
|
| 87 |
+
# Activate CONDA first
|
| 88 |
+
conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
|
| 89 |
+
conda activate onescience311
|
| 90 |
+
# uv installation is supported
|
| 91 |
+
pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
### Training Data
|
| 95 |
+
|
| 96 |
+
The OneScience community provides ERA5 data for training (the current repository contains complete data slices subject to data-file size limits). Download it with the command below and confirm that the data path in `conf/config.yaml` is correct:
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
hf download --repo-type dataset OneScience-Group/ERA5 --local-dir ./data
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
For a quick workflow validation, run the synthetic data script first:
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
python scripts/fake_data.py
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
> Note: `scripts/fake_data.py` generates the `[T, C, H, W]` HDF5 data required by the two input time steps and generates `data/static/static.npy` (currently `[4, 32, 64]`) for training and inference.
|
| 109 |
+
|
| 110 |
+
### Training
|
| 111 |
+
|
| 112 |
+
Single card:
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
python scripts/train.py
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
Multiple cards:
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
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
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
Training outputs:
|
| 125 |
+
|
| 126 |
+
```text
|
| 127 |
+
data/checkpoints/model_bak.pth
|
| 128 |
+
data/checkpoints/trloss.npy
|
| 129 |
+
data/checkpoints/valoss.npy
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
### Training Weights
|
| 133 |
+
The `weight/` folder is reserved for model weights. Official weights with approximately 2.3 billion parameters (such as PrithviWxC_160_13b_2t_0p5d_v1.pt) are published on Hugging Face, but their structure differs from this repository's small configuration. Align the channel count and grid size before loading; weights are not provided by default, and users may train the model using the paper configuration.
|
| 134 |
+
|
| 135 |
+
### Inference
|
| 136 |
+
|
| 137 |
+
Inference reads `data/checkpoints/model_bak.pth`:
|
| 138 |
+
|
| 139 |
+
```bash
|
| 140 |
+
python scripts/inference.py
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
Prediction results are written to:
|
| 144 |
+
|
| 145 |
+
```text
|
| 146 |
+
result/output/
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
### Evaluation and Visualization
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
python scripts/result.py
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
Outputs include:
|
| 156 |
+
|
| 157 |
+
- `result/rmse.npy`
|
| 158 |
+
- `result/acc.npy`
|
| 159 |
+
- `result/loss.png`
|
| 160 |
+
- Forecast comparison plots for the specified date and variables
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# Official Source and Reproduction Notes
|
| 164 |
+
|
| 165 |
+
- The model implementation comes from the official `NASA-IMPACT/Prithvi-WxC` (MIT License). The official implementation is embedded unchanged in `model/prithvi_wxc_official.py`; `model/prithvi_wxc.py` is only a YAML-driven thin wrapper (with identity normalization parameters for small-configuration connectivity validation).
|
| 166 |
+
- Commit fetched for the current case directory: `79dabfcd17abe77e2d5c696707c0164a04f2ec01` (2026-02-05).
|
| 167 |
+
- `conf/config.yaml` uses a small configuration (`embed_dim=32`, `n_blocks_encoder=1`, `n_blocks_decoder=1`) for connectivity validation by default; paper-level reproduction requires a 0.5°×0.625° grid, 160 channels, `embed_dim=2560`, and 13+12 encoder blocks/3+2 decoder blocks as specified in the paper.
|
| 168 |
+
- The following details are not disclosed in the paper and are assumptions in this reproduction:data normalization statistics (identity normalization is currently used; real statistics will be supplied with the data), masked-training details and pretraining schedule, and some hyperparameters (such as the relative positional encoding implementation).
|
| 169 |
+
|
| 170 |
+
# Official OneScience Information
|
| 171 |
+
|
| 172 |
+
| Platform | OneScience Main Repository | Skills Repository |
|
| 173 |
+
| --- | --- | --- |
|
| 174 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 175 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 176 |
+
|
| 177 |
+
# Citation and License
|
| 178 |
+
|
| 179 |
+
- This repository is an independent organization and adaptation of Prithvi WxC. The model source is based on the official `NASA-IMPACT/Prithvi-WxC` implementation by Schmude et al. (2024) and follows the MIT License.
|
| 180 |
+
- Please cite:Schmude et al. Prithvi WxC: Foundation Model for Weather and Climate. arXiv:2409.13598, 2024.
|
conf/config.yaml
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Prithvi WxC 训练配置示例
|
| 2 |
+
# 论文配置为 0.5°×0.625°(360/361×576)网格、160 动态变量、embed_dim=2560、
|
| 3 |
+
# 编码器 13 本地+12 全局块、解码器 3 本地+2 全局块(约 23 亿参数,需大规模显存)。
|
| 4 |
+
# 当前为连通性验证小配置(约百万级参数)。
|
| 5 |
+
model:
|
| 6 |
+
start_epoch: 0
|
| 7 |
+
max_epoch: 100
|
| 8 |
+
lr: 1E-3
|
| 9 |
+
patience: 50
|
| 10 |
+
checkpoint_dir: "./data/checkpoints"
|
| 11 |
+
|
| 12 |
+
# Prithvi WxC 结构参数
|
| 13 |
+
in_channels: 6 # 论文为 160(20 单层 + 10 变量 ×14 层)
|
| 14 |
+
input_size_time: 2 # 两个输入时刻
|
| 15 |
+
in_channels_static: 4 # 静态通道数(论文 MEMRA-2 为 4)
|
| 16 |
+
n_lats_px: 32 # 数据纬度方向像元数(论文约 360)
|
| 17 |
+
n_lons_px: 64 # 数据经度方向像元数(论文 576)
|
| 18 |
+
patch_size_px: [2, 2] # token 尺寸(论文 2×2 像素)
|
| 19 |
+
mask_unit_size_px: [8, 8] # 掩码单元尺寸
|
| 20 |
+
mask_ratio_inputs: 0.0 # 预训练为 0.5;预报滚动微调为 0.0
|
| 21 |
+
embed_dim: 32 # 隐藏维度(论文 2560)
|
| 22 |
+
n_blocks_encoder: 1 # 编码器本地-全局对数量(论文 13 对 -> 25 块)
|
| 23 |
+
n_blocks_decoder: 1 # 解码器本地-全局对数量(论文 5 块)
|
| 24 |
+
mlp_multiplier: 4.0
|
| 25 |
+
n_heads: 4 # 注意力头数(论文 16)
|
| 26 |
+
dropout: 0.0
|
| 27 |
+
drop_path: 0.0
|
| 28 |
+
parameter_dropout: 0.0
|
| 29 |
+
residual: "none" # none / temporal / climate
|
| 30 |
+
masking_mode: "global"
|
| 31 |
+
positional_encoding: "absolute"
|
| 32 |
+
encoder_shifting: false
|
| 33 |
+
decoder_shifting: false
|
| 34 |
+
|
| 35 |
+
# 整个数据读取流程
|
| 36 |
+
datapipe:
|
| 37 |
+
name: "ERA5"
|
| 38 |
+
task: "weather_forecasting"
|
| 39 |
+
|
| 40 |
+
dataset:
|
| 41 |
+
type: "hdf5"
|
| 42 |
+
data_dir: './data/'
|
| 43 |
+
train_time: [1951, 1952]
|
| 44 |
+
val_time: [1953]
|
| 45 |
+
test_time: [1954]
|
| 46 |
+
img_size: [32, 64]
|
| 47 |
+
verbose: true
|
| 48 |
+
cache: false
|
| 49 |
+
|
| 50 |
+
# 气象变量(论文 160 通道配置的子集,用于连通性验证)
|
| 51 |
+
channels: ['10m_u_component_of_wind', '10m_v_component_of_wind', '2m_temperature',
|
| 52 |
+
'mean_sea_level_pressure', 'geopotential_500', 'temperature_850']
|
| 53 |
+
|
| 54 |
+
# DataLoader 配置
|
| 55 |
+
dataloader:
|
| 56 |
+
mask_dtype: "float32"
|
| 57 |
+
batch_size: 1
|
| 58 |
+
num_workers: 1
|
| 59 |
+
pin_memory: true
|
| 60 |
+
drop_last: true
|
| 61 |
+
shuffle: false
|
| 62 |
+
prefetch_factor: 2
|
| 63 |
+
persistent_workers: true
|
| 64 |
+
|
| 65 |
+
# 分布式配置
|
| 66 |
+
distributed:
|
| 67 |
+
enabled: true
|
| 68 |
+
sampler: "DistributedSampler"
|
| 69 |
+
rank: 0
|
| 70 |
+
world_size: 2
|
| 71 |
+
shuffle: true
|
| 72 |
+
seed: 42
|
| 73 |
+
drop_last: true
|
config.json
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "Prithvi WxC",
|
| 3 |
+
"model_type": "prithvi_wxc",
|
| 4 |
+
"architectures": [
|
| 5 |
+
"PrithviWxC"
|
| 6 |
+
],
|
| 7 |
+
"framework": "PyTorch",
|
| 8 |
+
"domain": "climate-and-atmosphere",
|
| 9 |
+
"task": "global-weather-forecasting",
|
| 10 |
+
"implementation": {
|
| 11 |
+
"entry_point": "model/prithvi_wxc.py",
|
| 12 |
+
"scope": "YAML-driven wrapper around the vendored official Hiera-MaxViT encoder-decoder, with identity model scalers for the repository's small connectivity configuration"
|
| 13 |
+
},
|
| 14 |
+
"architecture": {
|
| 15 |
+
"family": "Hiera-MaxViT encoder-decoder Vision Transformer",
|
| 16 |
+
"attention_mechanism": "alternating local block attention and global grid attention",
|
| 17 |
+
"input_format": "BTCHW atmospheric history plus BCHW static fields",
|
| 18 |
+
"output_format": "BCHW next atmospheric state",
|
| 19 |
+
"tokenization": "Conv2d patch embedding followed by mask-unit grouping and pixel-shuffle reconstruction",
|
| 20 |
+
"activation": "GELU",
|
| 21 |
+
"normalization": "LayerNorm",
|
| 22 |
+
"repository_default_config": {
|
| 23 |
+
"purpose": "small connectivity validation with synthetic or reduced ERA5 data",
|
| 24 |
+
"in_channels": 6,
|
| 25 |
+
"input_size_time": 2,
|
| 26 |
+
"in_channels_static": 4,
|
| 27 |
+
"grid_size": [
|
| 28 |
+
32,
|
| 29 |
+
64
|
| 30 |
+
],
|
| 31 |
+
"patch_size_px": [
|
| 32 |
+
2,
|
| 33 |
+
2
|
| 34 |
+
],
|
| 35 |
+
"mask_unit_size_px": [
|
| 36 |
+
8,
|
| 37 |
+
8
|
| 38 |
+
],
|
| 39 |
+
"mask_ratio_inputs": 0.0,
|
| 40 |
+
"embed_dim": 32,
|
| 41 |
+
"n_blocks_encoder": 1,
|
| 42 |
+
"n_blocks_decoder": 1,
|
| 43 |
+
"n_heads": 4,
|
| 44 |
+
"mlp_multiplier": 4.0,
|
| 45 |
+
"dropout": 0.0,
|
| 46 |
+
"drop_path": 0.0,
|
| 47 |
+
"parameter_dropout": 0.0,
|
| 48 |
+
"residual": "none",
|
| 49 |
+
"masking_mode": "global",
|
| 50 |
+
"positional_encoding": "absolute",
|
| 51 |
+
"encoder_shifting": false,
|
| 52 |
+
"decoder_shifting": false,
|
| 53 |
+
"normalization_scalers": "identity in the YAML wrapper"
|
| 54 |
+
},
|
| 55 |
+
"paper_reference_config": {
|
| 56 |
+
"grid_resolution_degrees": [
|
| 57 |
+
0.5,
|
| 58 |
+
0.625
|
| 59 |
+
],
|
| 60 |
+
"grid_size": "approximately 360/361 x 576",
|
| 61 |
+
"dynamic_channels": 160,
|
| 62 |
+
"embed_dim": 2560,
|
| 63 |
+
"encoder_blocks": "13 local plus 12 global blocks",
|
| 64 |
+
"decoder_blocks": "3 local plus 2 global blocks",
|
| 65 |
+
"n_heads": 16,
|
| 66 |
+
"pretraining_mask_ratio_inputs": 0.5,
|
| 67 |
+
"forecast_finetuning_mask_ratio_inputs": 0.0,
|
| 68 |
+
"parameter_count": "approximately 2.3 billion",
|
| 69 |
+
"compatibility_note": "official 2.3B checkpoints do not match the repository default small configuration without aligning channels, grid size, and architecture"
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
"data": {
|
| 73 |
+
"dataset": "ERA5",
|
| 74 |
+
"storage": "HDF5 fields with shape TCHW",
|
| 75 |
+
"temporal_interval_hours": 6,
|
| 76 |
+
"input_steps": 2,
|
| 77 |
+
"output_steps": 1,
|
| 78 |
+
"forecast_lead_time_hours": 6,
|
| 79 |
+
"dynamic_channels": [
|
| 80 |
+
"10m_u_component_of_wind",
|
| 81 |
+
"10m_v_component_of_wind",
|
| 82 |
+
"2m_temperature",
|
| 83 |
+
"mean_sea_level_pressure",
|
| 84 |
+
"geopotential_500",
|
| 85 |
+
"temperature_850"
|
| 86 |
+
],
|
| 87 |
+
"static_channels": 4,
|
| 88 |
+
"default_spatial_size": [
|
| 89 |
+
32,
|
| 90 |
+
64
|
| 91 |
+
],
|
| 92 |
+
"default_year_splits": {
|
| 93 |
+
"train": [
|
| 94 |
+
1951,
|
| 95 |
+
1952
|
| 96 |
+
],
|
| 97 |
+
"validation": [
|
| 98 |
+
1953
|
| 99 |
+
],
|
| 100 |
+
"test": [
|
| 101 |
+
1954
|
| 102 |
+
]
|
| 103 |
+
},
|
| 104 |
+
"synthetic_data_generator": "scripts/fake_data.py"
|
| 105 |
+
},
|
| 106 |
+
"configuration_sources": [
|
| 107 |
+
"conf/config.yaml",
|
| 108 |
+
"model/prithvi_wxc.py",
|
| 109 |
+
"model/prithvi_wxc_official.py",
|
| 110 |
+
"scripts/train.py",
|
| 111 |
+
"scripts/inference.py",
|
| 112 |
+
"scripts/fake_data.py",
|
| 113 |
+
"README.md",
|
| 114 |
+
"configuration.json"
|
| 115 |
+
]
|
| 116 |
+
}
|
configuration.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"framework":"Pytorch","task":"other"}
|
model/__pycache__/prithvi_wxc.cpython-311.pyc
ADDED
|
Binary file (6.4 kB). View file
|
|
|
model/__pycache__/prithvi_wxc_official.cpython-311.pyc
ADDED
|
Binary file (76.8 kB). View file
|
|
|
model/prithvi_wxc.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# coding=utf-8
|
| 2 |
+
#
|
| 3 |
+
# SPDX-License-Identifier: MIT
|
| 4 |
+
#
|
| 5 |
+
# Copyright (c) 2024 Inter Agency Implementation and Advanced Concepts
|
| 6 |
+
#
|
| 7 |
+
# This module is a thin configuration wrapper around the official Prithvi WxC
|
| 8 |
+
# model (Schmude et al., arXiv:2409.13598), taken verbatim from the
|
| 9 |
+
# NASA-IMPACT/Prithvi-WxC repository (commit 79dabfcd17abe77e2d5c696707c0164a04f2ec01,
|
| 10 |
+
# MIT License). The official implementation is vendored as
|
| 11 |
+
# ``prithvi_wxc_official.py``; only scaler construction and tensor-level call
|
| 12 |
+
# signatures are added here for YAML-driven usage.
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
|
| 16 |
+
from model.prithvi_wxc_official import PrithviWxC as _PrithviWxC
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _identity_scalers(n, device="cpu"):
|
| 20 |
+
return torch.zeros(n, device=device), torch.ones(n, device=device)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class PrithviWxC(nn.Module):
|
| 24 |
+
"""
|
| 25 |
+
Config-driven Prithvi WxC wrapper.
|
| 26 |
+
|
| 27 |
+
The official model is an encoder-decoder vision Transformer (Hiera + MaxViT,
|
| 28 |
+
alternating local/global attention) that maps an input state history to a
|
| 29 |
+
target state. This wrapper builds the official network with identity
|
| 30 |
+
scalers so that a small YAML config can be exercised; loading the 2.3B
|
| 31 |
+
checkpoints published on Hugging Face is possible by calling
|
| 32 |
+
``load_state_dict`` with the official checkpoint state (the buffer names,
|
| 33 |
+
e.g. ``input_scalers_mu``, are identical).
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
in_channels: number of input/output parameters.
|
| 37 |
+
input_size_time: number of input timestamps (paper uses 2).
|
| 38 |
+
in_channels_static: number of static input channels (>= 1).
|
| 39 |
+
n_lats_px / n_lons_px: grid size in pixels.
|
| 40 |
+
patch_size_px: patch/token size in pixels.
|
| 41 |
+
mask_unit_size_px: mask unit size in pixels.
|
| 42 |
+
mask_ratio_inputs: input masking ratio (0..1, 0 disables).
|
| 43 |
+
embed_dim / n_blocks_encoder / n_blocks_decoder / mlp_multiplier / n_heads:
|
| 44 |
+
transformer architecture hyper-parameters (paper: 2560 / 25 / 5 / 4 / 16).
|
| 45 |
+
dropout / drop_path / parameter_dropout: regularization ratios.
|
| 46 |
+
residual: one of "none" / "temporal" / "climate".
|
| 47 |
+
masking_mode: "global" / "local" / "both".
|
| 48 |
+
positional_encoding: "absolute" or "fourier".
|
| 49 |
+
encoder_shifting / decoder_shifting: Swin-style shifting.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(
|
| 53 |
+
self,
|
| 54 |
+
in_channels=4,
|
| 55 |
+
input_size_time=2,
|
| 56 |
+
in_channels_static=4,
|
| 57 |
+
n_lats_px=32,
|
| 58 |
+
n_lons_px=64,
|
| 59 |
+
patch_size_px=(2, 2),
|
| 60 |
+
mask_unit_size_px=(8, 8),
|
| 61 |
+
mask_ratio_inputs=0.0,
|
| 62 |
+
embed_dim=16,
|
| 63 |
+
n_blocks_encoder=1,
|
| 64 |
+
n_blocks_decoder=1,
|
| 65 |
+
mlp_multiplier=4.0,
|
| 66 |
+
n_heads=4,
|
| 67 |
+
dropout=0.0,
|
| 68 |
+
drop_path=0.0,
|
| 69 |
+
parameter_dropout=0.0,
|
| 70 |
+
residual="none",
|
| 71 |
+
masking_mode="global",
|
| 72 |
+
positional_encoding="absolute",
|
| 73 |
+
encoder_shifting=False,
|
| 74 |
+
decoder_shifting=False,
|
| 75 |
+
):
|
| 76 |
+
super().__init__()
|
| 77 |
+
self.in_channels = int(in_channels)
|
| 78 |
+
self.input_size_time = int(input_size_time)
|
| 79 |
+
self.in_channels_static = int(in_channels_static)
|
| 80 |
+
self.n_lats_px = int(n_lats_px)
|
| 81 |
+
self.n_lons_px = int(n_lons_px)
|
| 82 |
+
self.residual = residual
|
| 83 |
+
self.positional_encoding = positional_encoding
|
| 84 |
+
|
| 85 |
+
mu_in, sigma_in = _identity_scalers(self.in_channels)
|
| 86 |
+
mu_s, sigma_s = _identity_scalers(self.in_channels_static)
|
| 87 |
+
|
| 88 |
+
self.model = _PrithviWxC(
|
| 89 |
+
in_channels=self.in_channels,
|
| 90 |
+
input_size_time=self.input_size_time,
|
| 91 |
+
in_channels_static=self.in_channels_static,
|
| 92 |
+
input_scalers_mu=mu_in,
|
| 93 |
+
input_scalers_sigma=sigma_in,
|
| 94 |
+
input_scalers_epsilon=1e-6,
|
| 95 |
+
static_input_scalers_mu=mu_s,
|
| 96 |
+
static_input_scalers_sigma=sigma_s,
|
| 97 |
+
static_input_scalers_epsilon=1e-6,
|
| 98 |
+
output_scalers=sigma_in,
|
| 99 |
+
n_lats_px=self.n_lats_px,
|
| 100 |
+
n_lons_px=self.n_lons_px,
|
| 101 |
+
patch_size_px=tuple(patch_size_px),
|
| 102 |
+
mask_unit_size_px=tuple(mask_unit_size_px),
|
| 103 |
+
mask_ratio_inputs=float(mask_ratio_inputs),
|
| 104 |
+
mask_ratio_targets=0.0,
|
| 105 |
+
embed_dim=int(embed_dim),
|
| 106 |
+
n_blocks_encoder=int(n_blocks_encoder),
|
| 107 |
+
n_blocks_decoder=int(n_blocks_decoder),
|
| 108 |
+
mlp_multiplier=int(mlp_multiplier),
|
| 109 |
+
n_heads=int(n_heads),
|
| 110 |
+
dropout=float(dropout),
|
| 111 |
+
drop_path=float(drop_path),
|
| 112 |
+
parameter_dropout=float(parameter_dropout),
|
| 113 |
+
residual=residual,
|
| 114 |
+
masking_mode=masking_mode,
|
| 115 |
+
positional_encoding=positional_encoding,
|
| 116 |
+
encoder_shifting=bool(encoder_shifting),
|
| 117 |
+
decoder_shifting=bool(decoder_shifting),
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
def forward(self, x, static, input_time=None, lead_time=None, climate=None):
|
| 121 |
+
"""
|
| 122 |
+
Args:
|
| 123 |
+
x: Tensor of shape [batch, time, parameter, lat, lon].
|
| 124 |
+
static: Tensor of shape [batch, static_channel, lat, lon].
|
| 125 |
+
input_time: Tensor of shape [batch] (optional, default zeros).
|
| 126 |
+
lead_time: Tensor of shape [batch] (optional, default zeros).
|
| 127 |
+
climate: Optional Tensor of shape [batch, parameter, lat, lon].
|
| 128 |
+
Returns:
|
| 129 |
+
Tensor of shape [batch, parameter, lat, lon].
|
| 130 |
+
"""
|
| 131 |
+
if input_time is None:
|
| 132 |
+
input_time = torch.zeros(x.shape[0], device=x.device)
|
| 133 |
+
if lead_time is None:
|
| 134 |
+
lead_time = torch.zeros(x.shape[0], device=x.device)
|
| 135 |
+
batch = {
|
| 136 |
+
"x": x,
|
| 137 |
+
"y": x[:, -1],
|
| 138 |
+
"static": static,
|
| 139 |
+
"input_time": input_time,
|
| 140 |
+
"lead_time": lead_time,
|
| 141 |
+
}
|
| 142 |
+
if climate is not None:
|
| 143 |
+
batch["climate"] = climate
|
| 144 |
+
return self.model(batch)
|
model/prithvi_wxc_official.py
ADDED
|
@@ -0,0 +1,1637 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from importlib.metadata import version
|
| 2 |
+
TORCH_VERSION = version('torch')
|
| 3 |
+
|
| 4 |
+
from functools import cached_property
|
| 5 |
+
from typing import Optional
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
import torch.nn as nn
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
from torch.utils.checkpoint import checkpoint
|
| 13 |
+
if TORCH_VERSION > '2.3.0':
|
| 14 |
+
from torch.nn.attention import SDPBackend, sdpa_kernel
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# DropPath code is straight from timm
|
| 18 |
+
# (https://huggingface.co/spaces/Roll20/pet_score/blame/main/lib/timm/models/layers/drop.py)
|
| 19 |
+
# Primarily since we currently don't have timm in the environment.
|
| 20 |
+
def drop_path(
|
| 21 |
+
x, drop_prob: float = 0.0, training: bool = False, scale_by_keep: bool = True
|
| 22 |
+
):
|
| 23 |
+
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
|
| 24 |
+
|
| 25 |
+
This is the same as the DropConnect impl I created for EfficientNet, etc networks, however,
|
| 26 |
+
the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper...
|
| 27 |
+
See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for
|
| 28 |
+
changing the layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use
|
| 29 |
+
'survival rate' as the argument.
|
| 30 |
+
"""
|
| 31 |
+
if drop_prob == 0.0 or not training:
|
| 32 |
+
return x
|
| 33 |
+
keep_prob = 1 - drop_prob
|
| 34 |
+
shape = (x.shape[0],) + (1,) * (
|
| 35 |
+
x.ndim - 1
|
| 36 |
+
) # work with diff dim tensors, not just 2D ConvNets
|
| 37 |
+
random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
|
| 38 |
+
if keep_prob > 0.0 and scale_by_keep:
|
| 39 |
+
random_tensor.div_(keep_prob)
|
| 40 |
+
return x * random_tensor
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class DropPath(nn.Module):
|
| 44 |
+
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks)."""
|
| 45 |
+
|
| 46 |
+
def __init__(self, drop_prob=None, scale_by_keep=True):
|
| 47 |
+
super(DropPath, self).__init__()
|
| 48 |
+
self.drop_prob = drop_prob
|
| 49 |
+
self.scale_by_keep = scale_by_keep
|
| 50 |
+
|
| 51 |
+
def forward(self, x):
|
| 52 |
+
return drop_path(x, self.drop_prob, self.training, self.scale_by_keep)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class Mlp(nn.Module):
|
| 56 |
+
"""
|
| 57 |
+
Multi layer perceptron.
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def __init__(
|
| 61 |
+
self, features: int, hidden_features: int, dropout: float = 0.0
|
| 62 |
+
) -> None:
|
| 63 |
+
"""
|
| 64 |
+
Args:
|
| 65 |
+
features: Input/output dimension.
|
| 66 |
+
hidden_features: Hidden dimension.
|
| 67 |
+
dropout: Dropout.
|
| 68 |
+
"""
|
| 69 |
+
super().__init__()
|
| 70 |
+
self.net = nn.Sequential(
|
| 71 |
+
nn.Linear(features, hidden_features),
|
| 72 |
+
nn.GELU(),
|
| 73 |
+
nn.Dropout(dropout),
|
| 74 |
+
nn.Linear(hidden_features, features),
|
| 75 |
+
nn.Dropout(dropout),
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 79 |
+
"""
|
| 80 |
+
Args:
|
| 81 |
+
Tensor of shape [..., channel]
|
| 82 |
+
Returns:
|
| 83 |
+
Tensor of same shape as x.
|
| 84 |
+
"""
|
| 85 |
+
return self.net(x)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class LayerNormPassThrough(nn.LayerNorm):
|
| 89 |
+
"""
|
| 90 |
+
Normalising layer that allows the attention mask to be passed through
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
def __init__(self, *args, **kwargs):
|
| 94 |
+
super().__init__(*args, **kwargs)
|
| 95 |
+
|
| 96 |
+
def forward(self, d: tuple[Tensor, Tensor | None]) -> tuple[Tensor, Tensor | None]:
|
| 97 |
+
"""
|
| 98 |
+
Forwards function
|
| 99 |
+
Args:
|
| 100 |
+
d: tuple of the data tensor and the attention mask
|
| 101 |
+
Returns:
|
| 102 |
+
output: normalised output data
|
| 103 |
+
attn_mask: the attention mask that was passed in
|
| 104 |
+
"""
|
| 105 |
+
input, attn_mask = d
|
| 106 |
+
output = F.layer_norm(
|
| 107 |
+
input, self.normalized_shape, self.weight, self.bias, self.eps
|
| 108 |
+
)
|
| 109 |
+
return output, attn_mask
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class MultiheadAttention(nn.Module):
|
| 113 |
+
"""
|
| 114 |
+
Multihead attention layer for inputs of shape [..., sequence, features].
|
| 115 |
+
|
| 116 |
+
Uses `scaled_dot_product_attention` to obtain a memory efficient attention
|
| 117 |
+
computation (https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html).
|
| 118 |
+
This follows:
|
| 119 |
+
- Dao et la. "FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness"
|
| 120 |
+
(https://arxiv.org/abs/2205.14135)
|
| 121 |
+
- Rabe, Staats "Self-attention Does Not Need O(n2) Memory" (https://arxiv.org/abs/2112.05682)
|
| 122 |
+
|
| 123 |
+
Note: Even though the documentation page for `scaled_dot_product_attention`
|
| 124 |
+
states that tensors can have any number of dimensions as long as the shapes
|
| 125 |
+
are along the lines of `(B, ..., S, E)`, the fused and memory efficient
|
| 126 |
+
mechanisms we enforce here require a 4D input. Some experimentatino shows
|
| 127 |
+
that this should be of shape `(B, H, S, E)`, where `H` represents heads.
|
| 128 |
+
However, as of right now this is not confirmed int he documentation.
|
| 129 |
+
"""
|
| 130 |
+
|
| 131 |
+
def __init__(self, features: int, n_heads: int, dropout: float) -> None:
|
| 132 |
+
"""
|
| 133 |
+
Args:
|
| 134 |
+
features: Number of features for inputs to the layer.
|
| 135 |
+
n_heads: Number of attention heads. Should be a factor of features.
|
| 136 |
+
(I.e. the layer uses features // n_heads.)
|
| 137 |
+
dropout: Dropout.
|
| 138 |
+
"""
|
| 139 |
+
super().__init__()
|
| 140 |
+
|
| 141 |
+
if not (features % n_heads) == 0:
|
| 142 |
+
raise ValueError(
|
| 143 |
+
f"Number of features {features} is not divisible by number of heads {n_heads}."
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
self.features = features
|
| 147 |
+
self.n_heads = n_heads
|
| 148 |
+
self.dropout = dropout
|
| 149 |
+
|
| 150 |
+
self.qkv_layer = torch.nn.Linear(features, features * 3, bias=False)
|
| 151 |
+
self.w_layer = torch.nn.Linear(features, features, bias=False)
|
| 152 |
+
|
| 153 |
+
def forward(self, d: tuple[Tensor, Tensor | None]) -> Tensor:
|
| 154 |
+
"""
|
| 155 |
+
Args:
|
| 156 |
+
d: tuple containing Tensor of shape [..., sequence, features] and
|
| 157 |
+
the attention mask
|
| 158 |
+
Returns:
|
| 159 |
+
Tensor of shape [..., sequence, features]
|
| 160 |
+
"""
|
| 161 |
+
x, attn_mask = d
|
| 162 |
+
|
| 163 |
+
if not x.shape[-1] == self.features:
|
| 164 |
+
raise ValueError(
|
| 165 |
+
f"Expecting tensor with last dimension of size {self.features}."
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
passenger_dims = x.shape[:-2]
|
| 169 |
+
B = passenger_dims.numel()
|
| 170 |
+
S = x.shape[-2]
|
| 171 |
+
C = x.shape[-1]
|
| 172 |
+
x = x.reshape(B, S, C)
|
| 173 |
+
|
| 174 |
+
# x [B, S, C]
|
| 175 |
+
# q, k, v [B, H, S, C/H]
|
| 176 |
+
q, k, v = (
|
| 177 |
+
self.qkv_layer(x)
|
| 178 |
+
.view(B, S, self.n_heads, 3 * (C // self.n_heads))
|
| 179 |
+
.transpose(1, 2)
|
| 180 |
+
.chunk(chunks=3, dim=3)
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
# Let us enforce either flash (A100+) or memory efficient attention,
|
| 184 |
+
# falling back to math on CPU / DCU(昇腾) shim where fused kernels are unavailable.
|
| 185 |
+
if TORCH_VERSION > '2.3.0':
|
| 186 |
+
with sdpa_kernel([SDPBackend.FLASH_ATTENTION, SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH]):
|
| 187 |
+
# x [B, H, S, C//H]
|
| 188 |
+
x = F.scaled_dot_product_attention(
|
| 189 |
+
q, k, v, dropout_p=self.dropout
|
| 190 |
+
)
|
| 191 |
+
else:
|
| 192 |
+
with torch.backends.cuda.sdp_kernel(
|
| 193 |
+
enable_flash=True, enable_math=False, enable_mem_efficient=True
|
| 194 |
+
):
|
| 195 |
+
# x [B, H, S, C//H]
|
| 196 |
+
x = F.scaled_dot_product_attention(q, k, v, dropout_p=self.dropout)
|
| 197 |
+
|
| 198 |
+
# x [B, S, C]
|
| 199 |
+
x = x.transpose(1, 2).reshape(B, S, C)
|
| 200 |
+
|
| 201 |
+
# x [B, S, C]
|
| 202 |
+
x = self.w_layer(x)
|
| 203 |
+
|
| 204 |
+
# Back to input shape
|
| 205 |
+
x = x.view(*passenger_dims, S, self.features)
|
| 206 |
+
return x
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class Transformer(nn.Module):
|
| 210 |
+
"""
|
| 211 |
+
Transformer for inputs of shape [..., S, features].
|
| 212 |
+
"""
|
| 213 |
+
|
| 214 |
+
def __init__(
|
| 215 |
+
self,
|
| 216 |
+
features: int,
|
| 217 |
+
mlp_multiplier: int,
|
| 218 |
+
n_heads: int,
|
| 219 |
+
dropout: float,
|
| 220 |
+
drop_path: float,
|
| 221 |
+
) -> None:
|
| 222 |
+
"""
|
| 223 |
+
Args:
|
| 224 |
+
features: Number of features for inputs to the layer.
|
| 225 |
+
mlp_multiplier: Model will use features*mlp_multiplier hidden units.
|
| 226 |
+
n_heads: Number of attention heads. Should be a factor of features.
|
| 227 |
+
(I.e. the layer uses features // n_heads.)
|
| 228 |
+
dropout: Dropout.
|
| 229 |
+
drop_path: DropPath.
|
| 230 |
+
"""
|
| 231 |
+
super().__init__()
|
| 232 |
+
|
| 233 |
+
self.features = features
|
| 234 |
+
self.mlp_multiplier = mlp_multiplier
|
| 235 |
+
self.n_heads = n_heads
|
| 236 |
+
self.dropout = dropout
|
| 237 |
+
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
| 238 |
+
|
| 239 |
+
self.attention = nn.Sequential(
|
| 240 |
+
LayerNormPassThrough(features),
|
| 241 |
+
MultiheadAttention(features, n_heads, dropout),
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
self.ff = nn.Sequential(
|
| 245 |
+
nn.LayerNorm(features),
|
| 246 |
+
Mlp(
|
| 247 |
+
features=features,
|
| 248 |
+
hidden_features=features * mlp_multiplier,
|
| 249 |
+
dropout=dropout,
|
| 250 |
+
),
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
def forward(self, d: tuple[Tensor, Tensor | None]) -> Tensor:
|
| 254 |
+
"""
|
| 255 |
+
Args:
|
| 256 |
+
x: Tensor of shape [..., sequence, features]
|
| 257 |
+
Returns:
|
| 258 |
+
Tensor of shape [..., sequence, features]
|
| 259 |
+
"""
|
| 260 |
+
x, attn_mask = d
|
| 261 |
+
if not x.shape[-1] == self.features:
|
| 262 |
+
raise ValueError(
|
| 263 |
+
f"Expecting tensor with last dimension of size {self.features}."
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
attention_x = self.attention(d)
|
| 267 |
+
|
| 268 |
+
x = x + self.drop_path(attention_x)
|
| 269 |
+
x = x + self.drop_path(self.ff(x))
|
| 270 |
+
|
| 271 |
+
return x
|
| 272 |
+
|
| 273 |
+
class _Shift(nn.Module):
|
| 274 |
+
"""
|
| 275 |
+
Private base class for the shifter. This allows some behaviour to be easily
|
| 276 |
+
handled when the shifter isn't used.
|
| 277 |
+
"""
|
| 278 |
+
|
| 279 |
+
def __init__(self):
|
| 280 |
+
super().__init__()
|
| 281 |
+
|
| 282 |
+
self._shifted = False
|
| 283 |
+
|
| 284 |
+
@torch.no_grad()
|
| 285 |
+
def reset(self) -> None:
|
| 286 |
+
"""
|
| 287 |
+
Resets the bool tracking whether the data is shifted
|
| 288 |
+
"""
|
| 289 |
+
self._shifted: bool = False
|
| 290 |
+
|
| 291 |
+
def forward(self, data: Tensor) -> tuple[Tensor, dict[bool, None]]:
|
| 292 |
+
return data, {True: None, False: None}
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
class SWINShift(_Shift):
|
| 296 |
+
"""
|
| 297 |
+
Handles the shifting of patches similar to how SWIN works. However if we
|
| 298 |
+
shift the latitudes then the poles will wrap and potentially that might be
|
| 299 |
+
problematic. The possition tokens should handle it but masking is safer.
|
| 300 |
+
"""
|
| 301 |
+
|
| 302 |
+
def __init__(
|
| 303 |
+
self,
|
| 304 |
+
mu_shape: tuple[int, int],
|
| 305 |
+
global_shape: tuple[int, int],
|
| 306 |
+
local_shape: tuple[int, int],
|
| 307 |
+
patch_shape: tuple[int, int],
|
| 308 |
+
n_context_tokens: int = 2,
|
| 309 |
+
) -> None:
|
| 310 |
+
"""
|
| 311 |
+
Args:
|
| 312 |
+
mu_shape: the shape to the masking units
|
| 313 |
+
global_shape: number of global patches in lat and lon
|
| 314 |
+
local_shape: size of the local patches
|
| 315 |
+
patch_shape: patch size
|
| 316 |
+
n_context_token: number of additional context tokens at start of _each_ local sequence
|
| 317 |
+
"""
|
| 318 |
+
super().__init__()
|
| 319 |
+
|
| 320 |
+
self._mu_shape = ms = mu_shape
|
| 321 |
+
self._g_shape = gs = global_shape
|
| 322 |
+
self._l_shape = ls = local_shape
|
| 323 |
+
self._p_shape = ps = patch_shape
|
| 324 |
+
self._lat_patch = (gs[0], ls[0], gs[1], ls[1])
|
| 325 |
+
self._n_context_tokens = n_context_tokens
|
| 326 |
+
|
| 327 |
+
self._g_shift_to = tuple(int(0.5 * x / p) for x, p in zip(ms, ps))
|
| 328 |
+
self._g_shift_from = tuple(-int(0.5 * x / p) for x, p in zip(ms, ps))
|
| 329 |
+
|
| 330 |
+
# Define the attention masks for the shifted MaxViT.
|
| 331 |
+
nglobal = global_shape[0] * global_shape[1]
|
| 332 |
+
nlocal = local_shape[0] * local_shape[1] + self._n_context_tokens # "+ 1" for leadtime
|
| 333 |
+
|
| 334 |
+
lm = torch.ones((nglobal, 1, nlocal, nlocal), dtype=bool)
|
| 335 |
+
mwidth = int(0.5 * local_shape[1]) * local_shape[0]
|
| 336 |
+
lm[
|
| 337 |
+
: gs[1],
|
| 338 |
+
:,
|
| 339 |
+
self._n_context_tokens : mwidth + self._n_context_tokens,
|
| 340 |
+
self._n_context_tokens : mwidth + self._n_context_tokens,
|
| 341 |
+
] = False
|
| 342 |
+
self.register_buffer("local_mask", lm)
|
| 343 |
+
|
| 344 |
+
gm = torch.ones((nlocal, 1, nglobal, nglobal), dtype=bool)
|
| 345 |
+
gm[: int(0.5 * ls[1]) * ls[0], :, : gs[1], : gs[1]] = False
|
| 346 |
+
self.register_buffer("global_mask", gm)
|
| 347 |
+
|
| 348 |
+
def _to_grid_global(self, x: Tensor) -> Tensor:
|
| 349 |
+
"""
|
| 350 |
+
Shuffle and reshape the data from the global/local setting back to the
|
| 351 |
+
lat/lon grid setting
|
| 352 |
+
Args:
|
| 353 |
+
x: the data tensor to be shuffled.
|
| 354 |
+
Returns:
|
| 355 |
+
x: data in the global/local setting
|
| 356 |
+
"""
|
| 357 |
+
nbatch, *other = x.shape
|
| 358 |
+
|
| 359 |
+
y1 = x.view(nbatch, *self._g_shape, *self._l_shape, -1)
|
| 360 |
+
y2 = y1.permute(0, 5, 1, 3, 2, 4).contiguous()
|
| 361 |
+
|
| 362 |
+
s = y2.shape
|
| 363 |
+
return y2.view((nbatch, -1, s[2] * s[3], s[4] * s[5]))
|
| 364 |
+
|
| 365 |
+
def _to_grid_local(self, x: Tensor) -> Tensor:
|
| 366 |
+
"""
|
| 367 |
+
Shuffle and reshape the data from the local/global setting to the
|
| 368 |
+
lat/lon grid setting
|
| 369 |
+
Args:
|
| 370 |
+
x: the data tensor to be shuffled.
|
| 371 |
+
Returns:
|
| 372 |
+
x: data in the lat/lon setting.
|
| 373 |
+
"""
|
| 374 |
+
x = x.transpose(2, 1).contiguous()
|
| 375 |
+
return self._to_grid_global(x)
|
| 376 |
+
|
| 377 |
+
def _from_grid_global(self, x: Tensor) -> Tensor:
|
| 378 |
+
"""
|
| 379 |
+
Shuffle and reshape the data from the lat/lon grid to the global/local
|
| 380 |
+
setting
|
| 381 |
+
Args:
|
| 382 |
+
x: the data tensor to be shuffled.
|
| 383 |
+
Returns:
|
| 384 |
+
x: data in the global/local setting
|
| 385 |
+
"""
|
| 386 |
+
nbatch, *other = x.shape
|
| 387 |
+
|
| 388 |
+
z1 = x.view(nbatch, -1, *self._lat_patch)
|
| 389 |
+
z2 = z1.permute(0, 2, 4, 3, 5, 1).contiguous()
|
| 390 |
+
|
| 391 |
+
s = z2.shape
|
| 392 |
+
return z2.view(nbatch, s[1] * s[2], s[3] * s[4], -1)
|
| 393 |
+
|
| 394 |
+
def _from_grid_local(self, x: Tensor) -> Tensor:
|
| 395 |
+
"""
|
| 396 |
+
Shuffle and reshape the data from the lat/lon grid to the local/global
|
| 397 |
+
setting
|
| 398 |
+
Args:
|
| 399 |
+
x: the data tensor to be shuffled.
|
| 400 |
+
Returns:
|
| 401 |
+
x: data in the local/global setting
|
| 402 |
+
"""
|
| 403 |
+
x = self._from_grid_global(x)
|
| 404 |
+
return x.transpose(2, 1).contiguous()
|
| 405 |
+
|
| 406 |
+
def _shift(self, x: Tensor) -> Tensor:
|
| 407 |
+
"""
|
| 408 |
+
Shifts data in the gridded lat/lon setting by half the mask unit shape
|
| 409 |
+
Args:
|
| 410 |
+
x: data to be shifted
|
| 411 |
+
Returns:
|
| 412 |
+
x: either the hsifted or unshifted data
|
| 413 |
+
"""
|
| 414 |
+
shift = self._g_shift_from if self._shifted else self._g_shift_to
|
| 415 |
+
x_shifted = torch.roll(x, shift, (-2, -1))
|
| 416 |
+
|
| 417 |
+
self._shifted = not self._shifted
|
| 418 |
+
return x_shifted
|
| 419 |
+
|
| 420 |
+
def _sep_lt(self, x: Tensor) -> tuple[Tensor, Tensor]:
|
| 421 |
+
"""
|
| 422 |
+
Seperate off the leadtime from the local patches
|
| 423 |
+
Args:
|
| 424 |
+
x: data to have leadtime removed from
|
| 425 |
+
Returns:
|
| 426 |
+
lt: leadtime
|
| 427 |
+
x: data without the lead time in the local patch
|
| 428 |
+
"""
|
| 429 |
+
lt_it = x[:, : self._n_context_tokens, :, :]
|
| 430 |
+
x_stripped = x[:, self._n_context_tokens :, :, :]
|
| 431 |
+
|
| 432 |
+
return lt_it, x_stripped
|
| 433 |
+
|
| 434 |
+
def forward(self, data: Tensor) -> tuple[Tensor, Tensor]:
|
| 435 |
+
"""
|
| 436 |
+
Shift or unshift the the data depending on whether the data is already
|
| 437 |
+
shifted, as defined by self._shifted
|
| 438 |
+
Args:
|
| 439 |
+
data: data to be shifted
|
| 440 |
+
Returns:
|
| 441 |
+
|
| 442 |
+
"""
|
| 443 |
+
lt, x = self._sep_lt(data)
|
| 444 |
+
|
| 445 |
+
x_grid = self._to_grid_local(x)
|
| 446 |
+
x_shifted = self._shift(x_grid)
|
| 447 |
+
x_patched = self._from_grid_local(x_shifted)
|
| 448 |
+
|
| 449 |
+
# Mask has to be repeated based on batch size
|
| 450 |
+
n_batch = x_grid.shape[0]
|
| 451 |
+
local_rep = [n_batch] + [1] * (self.local_mask.ndim - 1)
|
| 452 |
+
global_rep = [n_batch] + [1] * (self.global_mask.ndim - 1)
|
| 453 |
+
|
| 454 |
+
if self._shifted:
|
| 455 |
+
attn_mask = {
|
| 456 |
+
True: self.local_mask.repeat(local_rep),
|
| 457 |
+
False: self.global_mask.repeat(global_rep),
|
| 458 |
+
}
|
| 459 |
+
else:
|
| 460 |
+
attn_mask = {True: None, False: None}
|
| 461 |
+
|
| 462 |
+
return torch.cat((lt, x_patched), axis=1), attn_mask
|
| 463 |
+
|
| 464 |
+
class SWINShiftNoBuffer(_Shift):
|
| 465 |
+
"""
|
| 466 |
+
Handles the shifting of patches similar to how SWIN works. However if we
|
| 467 |
+
shift the latitudes then the poles will wrap and potentially that might be
|
| 468 |
+
problematic. The possition tokens should handle it but masking is safer.
|
| 469 |
+
"""
|
| 470 |
+
|
| 471 |
+
def __init__(
|
| 472 |
+
self,
|
| 473 |
+
mu_shape: tuple[int, int],
|
| 474 |
+
global_shape: tuple[int, int],
|
| 475 |
+
local_shape: tuple[int, int],
|
| 476 |
+
patch_shape: tuple[int, int],
|
| 477 |
+
n_context_tokens: int = 2,
|
| 478 |
+
) -> None:
|
| 479 |
+
"""
|
| 480 |
+
Args:
|
| 481 |
+
mu_shape: the shape to the masking units
|
| 482 |
+
global_shape: number of global patches in lat and lon
|
| 483 |
+
local_shape: size of the local patches
|
| 484 |
+
patch_shape: patch size
|
| 485 |
+
n_context_token: number of additional context tokens at start of _each_ local sequence
|
| 486 |
+
"""
|
| 487 |
+
super().__init__()
|
| 488 |
+
|
| 489 |
+
self._mu_shape = ms = mu_shape
|
| 490 |
+
self._g_shape = gs = global_shape
|
| 491 |
+
self._l_shape = ls = local_shape
|
| 492 |
+
self._p_shape = ps = patch_shape
|
| 493 |
+
self._lat_patch = (gs[0], ls[0], gs[1], ls[1])
|
| 494 |
+
self._n_context_tokens = n_context_tokens
|
| 495 |
+
|
| 496 |
+
self._g_shift_to = tuple(int(0.5 * x / p) for x, p in zip(ms, ps))
|
| 497 |
+
self._g_shift_from = tuple(-int(0.5 * x / p) for x, p in zip(ms, ps))
|
| 498 |
+
|
| 499 |
+
# Define the attention masks for the shifted MaxViT.
|
| 500 |
+
nglobal = global_shape[0] * global_shape[1]
|
| 501 |
+
nlocal = local_shape[0] * local_shape[1] + self._n_context_tokens # "+ 1" for leadtime
|
| 502 |
+
|
| 503 |
+
lm = torch.ones((nglobal, 1, nlocal, nlocal), dtype=bool)
|
| 504 |
+
mwidth = int(0.5 * local_shape[1]) * local_shape[0]
|
| 505 |
+
lm[
|
| 506 |
+
: gs[1],
|
| 507 |
+
:,
|
| 508 |
+
self._n_context_tokens : mwidth + self._n_context_tokens,
|
| 509 |
+
self._n_context_tokens : mwidth + self._n_context_tokens,
|
| 510 |
+
] = False
|
| 511 |
+
self.local_mask = lm
|
| 512 |
+
|
| 513 |
+
gm = torch.ones((nlocal, 1, nglobal, nglobal), dtype=bool)
|
| 514 |
+
gm[: int(0.5 * ls[1]) * ls[0], :, : gs[1], : gs[1]] = False
|
| 515 |
+
self.global_mask = gm
|
| 516 |
+
|
| 517 |
+
def _to_grid_global(self, x: Tensor) -> Tensor:
|
| 518 |
+
"""
|
| 519 |
+
Shuffle and reshape the data from the global/local setting back to the
|
| 520 |
+
lat/lon grid setting
|
| 521 |
+
Args:
|
| 522 |
+
x: the data tensor to be shuffled.
|
| 523 |
+
Returns:
|
| 524 |
+
x: data in the global/local setting
|
| 525 |
+
"""
|
| 526 |
+
nbatch, *other = x.shape
|
| 527 |
+
|
| 528 |
+
y1 = x.view(nbatch, *self._g_shape, *self._l_shape, -1)
|
| 529 |
+
y2 = y1.permute(0, 5, 1, 3, 2, 4).contiguous()
|
| 530 |
+
|
| 531 |
+
s = y2.shape
|
| 532 |
+
return y2.view((nbatch, -1, s[2] * s[3], s[4] * s[5]))
|
| 533 |
+
|
| 534 |
+
def _to_grid_local(self, x: Tensor) -> Tensor:
|
| 535 |
+
"""
|
| 536 |
+
Shuffle and reshape the data from the local/global setting to the
|
| 537 |
+
lat/lon grid setting
|
| 538 |
+
Args:
|
| 539 |
+
x: the data tensor to be shuffled.
|
| 540 |
+
Returns:
|
| 541 |
+
x: data in the lat/lon setting.
|
| 542 |
+
"""
|
| 543 |
+
x = x.transpose(2, 1).contiguous()
|
| 544 |
+
return self._to_grid_global(x)
|
| 545 |
+
|
| 546 |
+
def _from_grid_global(self, x: Tensor) -> Tensor:
|
| 547 |
+
"""
|
| 548 |
+
Shuffle and reshape the data from the lat/lon grid to the global/local
|
| 549 |
+
setting
|
| 550 |
+
Args:
|
| 551 |
+
x: the data tensor to be shuffled.
|
| 552 |
+
Returns:
|
| 553 |
+
x: data in the global/local setting
|
| 554 |
+
"""
|
| 555 |
+
nbatch, *other = x.shape
|
| 556 |
+
|
| 557 |
+
z1 = x.view(nbatch, -1, *self._lat_patch)
|
| 558 |
+
z2 = z1.permute(0, 2, 4, 3, 5, 1).contiguous()
|
| 559 |
+
|
| 560 |
+
s = z2.shape
|
| 561 |
+
return z2.view(nbatch, s[1] * s[2], s[3] * s[4], -1)
|
| 562 |
+
|
| 563 |
+
def _from_grid_local(self, x: Tensor) -> Tensor:
|
| 564 |
+
"""
|
| 565 |
+
Shuffle and reshape the data from the lat/lon grid to the local/global
|
| 566 |
+
setting
|
| 567 |
+
Args:
|
| 568 |
+
x: the data tensor to be shuffled.
|
| 569 |
+
Returns:
|
| 570 |
+
x: data in the local/global setting
|
| 571 |
+
"""
|
| 572 |
+
x = self._from_grid_global(x)
|
| 573 |
+
return x.transpose(2, 1).contiguous()
|
| 574 |
+
|
| 575 |
+
def _shift(self, x: Tensor) -> Tensor:
|
| 576 |
+
"""
|
| 577 |
+
Shifts data in the gridded lat/lon setting by half the mask unit shape
|
| 578 |
+
Args:
|
| 579 |
+
x: data to be shifted
|
| 580 |
+
Returns:
|
| 581 |
+
x: either the hsifted or unshifted data
|
| 582 |
+
"""
|
| 583 |
+
shift = self._g_shift_from if self._shifted else self._g_shift_to
|
| 584 |
+
x_shifted = torch.roll(x, shift, (-2, -1))
|
| 585 |
+
|
| 586 |
+
self._shifted = not self._shifted
|
| 587 |
+
return x_shifted
|
| 588 |
+
|
| 589 |
+
def _sep_lt(self, x: Tensor) -> tuple[Tensor, Tensor]:
|
| 590 |
+
"""
|
| 591 |
+
Seperate off the leadtime from the local patches
|
| 592 |
+
Args:
|
| 593 |
+
x: data to have leadtime removed from
|
| 594 |
+
Returns:
|
| 595 |
+
lt: leadtime
|
| 596 |
+
x: data without the lead time in the local patch
|
| 597 |
+
"""
|
| 598 |
+
lt_it = x[:, : self._n_context_tokens, :, :]
|
| 599 |
+
x_stripped = x[:, self._n_context_tokens :, :, :]
|
| 600 |
+
|
| 601 |
+
return lt_it, x_stripped
|
| 602 |
+
|
| 603 |
+
def forward(self, data: Tensor) -> tuple[Tensor, Tensor]:
|
| 604 |
+
"""
|
| 605 |
+
Shift or unshift the the data depending on whether the data is already
|
| 606 |
+
shifted, as defined by self._shifted
|
| 607 |
+
Args:
|
| 608 |
+
data: data to be shifted
|
| 609 |
+
Returns:
|
| 610 |
+
|
| 611 |
+
"""
|
| 612 |
+
lt, x = self._sep_lt(data)
|
| 613 |
+
|
| 614 |
+
if self.local_mask.device != x.device:
|
| 615 |
+
self.local_mask = self.local_mask.to(device=x.device)
|
| 616 |
+
if self.global_mask.device != x.device:
|
| 617 |
+
self.global_mask = self.global_mask.to(device=x.device)
|
| 618 |
+
|
| 619 |
+
x_grid = self._to_grid_local(x)
|
| 620 |
+
x_shifted = self._shift(x_grid)
|
| 621 |
+
x_patched = self._from_grid_local(x_shifted)
|
| 622 |
+
|
| 623 |
+
# Mask has to be repeated based on batch size
|
| 624 |
+
n_batch = x_grid.shape[0]
|
| 625 |
+
local_rep = [n_batch] + [1] * (self.local_mask.ndim - 1)
|
| 626 |
+
global_rep = [n_batch] + [1] * (self.global_mask.ndim - 1)
|
| 627 |
+
|
| 628 |
+
if self._shifted:
|
| 629 |
+
attn_mask = {
|
| 630 |
+
True: self.local_mask.repeat(local_rep),
|
| 631 |
+
False: self.global_mask.repeat(global_rep),
|
| 632 |
+
}
|
| 633 |
+
else:
|
| 634 |
+
attn_mask = {True: None, False: None}
|
| 635 |
+
|
| 636 |
+
return torch.cat((lt, x_patched), axis=1), attn_mask
|
| 637 |
+
|
| 638 |
+
class LocalGlobalLocalBlock(nn.Module):
|
| 639 |
+
"""
|
| 640 |
+
Applies alternating block and grid attention. Given a parameter n_blocks, the entire
|
| 641 |
+
module contains 2*n_blocks+1 transformer blocks. The first, third, ..., last apply
|
| 642 |
+
local (block) attention. The second, fourth, ... global (grid) attention.
|
| 643 |
+
|
| 644 |
+
This is heavily inspired by Tu et al. "MaxViT: Multi-Axis Vision Transformer"
|
| 645 |
+
(https://arxiv.org/abs/2204.01697).
|
| 646 |
+
"""
|
| 647 |
+
|
| 648 |
+
def __init__(
|
| 649 |
+
self,
|
| 650 |
+
features: int,
|
| 651 |
+
mlp_multiplier: int,
|
| 652 |
+
n_heads: int,
|
| 653 |
+
dropout: float,
|
| 654 |
+
n_blocks: int,
|
| 655 |
+
drop_path: float,
|
| 656 |
+
shifter: nn.Module | None = None,
|
| 657 |
+
checkpoint: list[int]=[],
|
| 658 |
+
) -> None:
|
| 659 |
+
"""
|
| 660 |
+
Args:
|
| 661 |
+
features: Number of features for inputs to the layer.
|
| 662 |
+
mlp_multiplier: Model will use features*mlp_multiplier hidden units.
|
| 663 |
+
n_heads: Number of attention heads. Should be a factor of features.
|
| 664 |
+
(I.e. the layer uses features // n_heads.)
|
| 665 |
+
dropout: Dropout.
|
| 666 |
+
drop_path: DropPath.
|
| 667 |
+
n_blocks: Number of local-global transformer pairs.
|
| 668 |
+
"""
|
| 669 |
+
super().__init__()
|
| 670 |
+
|
| 671 |
+
self.features = features
|
| 672 |
+
self.mlp_multiplier = mlp_multiplier
|
| 673 |
+
self.n_heads = n_heads
|
| 674 |
+
self.dropout = dropout
|
| 675 |
+
self.drop_path = drop_path
|
| 676 |
+
self.n_blocks = n_blocks
|
| 677 |
+
self._checkpoint = checkpoint
|
| 678 |
+
|
| 679 |
+
if len(checkpoint) > 0:
|
| 680 |
+
if min(checkpoint) < 0 or max(checkpoint) >= 2 * n_blocks + 1:
|
| 681 |
+
raise ValueError(f'Checkpoints should satisfy 0 <= i < 2*n_blocks+1. We have {checkpoint}.')
|
| 682 |
+
|
| 683 |
+
self.transformers = nn.ModuleList(
|
| 684 |
+
[
|
| 685 |
+
Transformer(
|
| 686 |
+
features=features,
|
| 687 |
+
mlp_multiplier=mlp_multiplier,
|
| 688 |
+
n_heads=n_heads,
|
| 689 |
+
dropout=dropout,
|
| 690 |
+
drop_path=drop_path,
|
| 691 |
+
)
|
| 692 |
+
for _ in range(2 * n_blocks + 1)
|
| 693 |
+
]
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
self.evaluator = [
|
| 697 |
+
self._checkpoint_wrapper if i in checkpoint else lambda m, x : m(x)
|
| 698 |
+
for i, _ in enumerate(self.transformers)
|
| 699 |
+
]
|
| 700 |
+
|
| 701 |
+
self.shifter = shifter or _Shift()
|
| 702 |
+
|
| 703 |
+
@staticmethod
|
| 704 |
+
def _checkpoint_wrapper(model, data):
|
| 705 |
+
return checkpoint(model, data, use_reentrant=False)
|
| 706 |
+
|
| 707 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 708 |
+
"""
|
| 709 |
+
Args:
|
| 710 |
+
x: Tensor of shape [batch, global_sequence, local_sequence, features]
|
| 711 |
+
Returns:
|
| 712 |
+
Tensor of shape [batch, global_sequence, local_sequence, features]
|
| 713 |
+
"""
|
| 714 |
+
if x.shape[-1] != self.features:
|
| 715 |
+
raise ValueError(
|
| 716 |
+
f"Expecting tensor with last dimension of size {self.features}."
|
| 717 |
+
)
|
| 718 |
+
if x.ndim != 4:
|
| 719 |
+
raise ValueError(
|
| 720 |
+
f"Expecting tensor with exactly four dimensions. Input has shape {x.shape}."
|
| 721 |
+
)
|
| 722 |
+
|
| 723 |
+
self.shifter.reset()
|
| 724 |
+
local: bool = True
|
| 725 |
+
attn_mask = {True: None, False: None}
|
| 726 |
+
|
| 727 |
+
transformer_iter = zip(self.evaluator, self.transformers)
|
| 728 |
+
|
| 729 |
+
# First local block
|
| 730 |
+
evaluator, transformer = next(transformer_iter)
|
| 731 |
+
x = evaluator(transformer, (x, attn_mask[local]))
|
| 732 |
+
|
| 733 |
+
for evaluator, transformer in transformer_iter:
|
| 734 |
+
local = not local
|
| 735 |
+
# We are making exactly 2*n_blocks transposes.
|
| 736 |
+
# So the output has the same shape as input.
|
| 737 |
+
x = x.transpose(1, 2)
|
| 738 |
+
|
| 739 |
+
x = evaluator(transformer, (x, attn_mask[local]))
|
| 740 |
+
|
| 741 |
+
if not local:
|
| 742 |
+
x, attn_mask = self.shifter(x)
|
| 743 |
+
|
| 744 |
+
return x
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
class PatchEmbed(nn.Module):
|
| 748 |
+
"""
|
| 749 |
+
Patch embedding via 2D convolution.
|
| 750 |
+
"""
|
| 751 |
+
|
| 752 |
+
def __init__(
|
| 753 |
+
self, patch_size: int | tuple[int, ...], channels: int, embed_dim: int
|
| 754 |
+
):
|
| 755 |
+
super().__init__()
|
| 756 |
+
|
| 757 |
+
self.patch_size = patch_size
|
| 758 |
+
self.channels = channels
|
| 759 |
+
self.embed_dim = embed_dim
|
| 760 |
+
|
| 761 |
+
self.proj = nn.Conv2d(
|
| 762 |
+
channels, embed_dim, kernel_size=patch_size, stride=patch_size, bias=True
|
| 763 |
+
)
|
| 764 |
+
|
| 765 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 766 |
+
"""
|
| 767 |
+
Args:
|
| 768 |
+
x: Tensor of shape [batch, channels, lat, lon].
|
| 769 |
+
Returns:
|
| 770 |
+
Tensor with shape [batch, embed_dim, lat//patch_size, lon//patch_size]
|
| 771 |
+
"""
|
| 772 |
+
|
| 773 |
+
H, W = x.shape[-2:]
|
| 774 |
+
|
| 775 |
+
if W % self.patch_size[1] != 0:
|
| 776 |
+
raise ValueError(
|
| 777 |
+
f"Cannot do patch embedding for tensor of shape {x.size()}"
|
| 778 |
+
" with patch size {self.patch_size}. (Dimensions are BSCHW.)"
|
| 779 |
+
)
|
| 780 |
+
if H % self.patch_size[0] != 0:
|
| 781 |
+
raise ValueError(
|
| 782 |
+
f"Cannot do patch embedding for tensor of shape {x.size()}"
|
| 783 |
+
f" with patch size {self.patch_size}. (Dimensions are BSCHW.)"
|
| 784 |
+
)
|
| 785 |
+
|
| 786 |
+
x = self.proj(x)
|
| 787 |
+
|
| 788 |
+
return x
|
| 789 |
+
|
| 790 |
+
|
| 791 |
+
class PrithviWxCEncoderDecoder(nn.Module):
|
| 792 |
+
"""
|
| 793 |
+
Hiera-MaxViT encoder/decoder code.
|
| 794 |
+
"""
|
| 795 |
+
|
| 796 |
+
def __init__(
|
| 797 |
+
self,
|
| 798 |
+
embed_dim: int,
|
| 799 |
+
n_blocks: int,
|
| 800 |
+
mlp_multiplier: float,
|
| 801 |
+
n_heads: int,
|
| 802 |
+
dropout: float,
|
| 803 |
+
drop_path: float,
|
| 804 |
+
shifter: nn.Module | None = None,
|
| 805 |
+
transformer_cp: list[int]=[],
|
| 806 |
+
) -> None:
|
| 807 |
+
"""
|
| 808 |
+
Args:
|
| 809 |
+
embed_dim: Embedding dimension
|
| 810 |
+
n_blocks: Number of local-global transformer pairs.
|
| 811 |
+
mlp_multiplier: MLP multiplier for hidden features in feed forward
|
| 812 |
+
networks.
|
| 813 |
+
n_heads: Number of attention heads.
|
| 814 |
+
dropout: Dropout.
|
| 815 |
+
drop_path: DropPath.
|
| 816 |
+
"""
|
| 817 |
+
super().__init__()
|
| 818 |
+
|
| 819 |
+
self.embed_dim = embed_dim
|
| 820 |
+
self.n_blocks = n_blocks
|
| 821 |
+
self.mlp_multiplier = mlp_multiplier
|
| 822 |
+
self.n_heads = n_heads
|
| 823 |
+
self.dropout = dropout
|
| 824 |
+
self._transformer_cp = transformer_cp
|
| 825 |
+
|
| 826 |
+
self.lgl_block = LocalGlobalLocalBlock(
|
| 827 |
+
features=embed_dim,
|
| 828 |
+
mlp_multiplier=mlp_multiplier,
|
| 829 |
+
n_heads=n_heads,
|
| 830 |
+
dropout=dropout,
|
| 831 |
+
drop_path=drop_path,
|
| 832 |
+
n_blocks=n_blocks,
|
| 833 |
+
shifter=shifter,
|
| 834 |
+
checkpoint=transformer_cp,
|
| 835 |
+
)
|
| 836 |
+
|
| 837 |
+
def forward(
|
| 838 |
+
self,
|
| 839 |
+
x: torch.Tensor
|
| 840 |
+
) -> torch.Tensor:
|
| 841 |
+
"""
|
| 842 |
+
Args:
|
| 843 |
+
x: Tensor of shape [batch, global sequence, local sequence, embed_dim]
|
| 844 |
+
Returns:
|
| 845 |
+
Tensor of shape [batch, mask_unit_sequence, local_sequence, embed_dim].
|
| 846 |
+
Identical in shape to the input x.
|
| 847 |
+
"""
|
| 848 |
+
|
| 849 |
+
x = self.lgl_block(x)
|
| 850 |
+
|
| 851 |
+
return x
|
| 852 |
+
|
| 853 |
+
|
| 854 |
+
class PrithviWxC(nn.Module):
|
| 855 |
+
"""
|
| 856 |
+
Encoder-decoder fusing Hiera with MaxViT. See
|
| 857 |
+
- Ryali et al. "Hiera: A Hierarchical Vision Transformer without the
|
| 858 |
+
Bells-and-Whistles" (https://arxiv.org/abs/2306.00989)
|
| 859 |
+
- Tu et al. "MaxViT: Multi-Axis Vision Transformer"
|
| 860 |
+
(https://arxiv.org/abs/2204.01697)
|
| 861 |
+
"""
|
| 862 |
+
|
| 863 |
+
def __init__(
|
| 864 |
+
self,
|
| 865 |
+
in_channels: int,
|
| 866 |
+
input_size_time: int,
|
| 867 |
+
in_channels_static: int,
|
| 868 |
+
input_scalers_mu: Tensor,
|
| 869 |
+
input_scalers_sigma: Tensor,
|
| 870 |
+
input_scalers_epsilon: float,
|
| 871 |
+
static_input_scalers_mu: Tensor,
|
| 872 |
+
static_input_scalers_sigma: Tensor,
|
| 873 |
+
static_input_scalers_epsilon: float,
|
| 874 |
+
output_scalers: Tensor,
|
| 875 |
+
n_lats_px: int,
|
| 876 |
+
n_lons_px: int,
|
| 877 |
+
patch_size_px: tuple[int],
|
| 878 |
+
mask_unit_size_px: tuple[int],
|
| 879 |
+
mask_ratio_inputs: float,
|
| 880 |
+
mask_ratio_targets: float,
|
| 881 |
+
embed_dim: int,
|
| 882 |
+
n_blocks_encoder: int,
|
| 883 |
+
n_blocks_decoder: int,
|
| 884 |
+
mlp_multiplier: float,
|
| 885 |
+
n_heads: int,
|
| 886 |
+
dropout: float,
|
| 887 |
+
drop_path: float,
|
| 888 |
+
parameter_dropout: float,
|
| 889 |
+
residual: str,
|
| 890 |
+
masking_mode: str,
|
| 891 |
+
positional_encoding: str,
|
| 892 |
+
encoder_shifting: bool = False,
|
| 893 |
+
decoder_shifting: bool = False,
|
| 894 |
+
checkpoint_encoder: list[int]=[],
|
| 895 |
+
checkpoint_decoder: list[int]=[],
|
| 896 |
+
) -> None:
|
| 897 |
+
"""
|
| 898 |
+
Args:
|
| 899 |
+
in_channels: Number of input channels.
|
| 900 |
+
input_size_time: Number of timestamps in input.
|
| 901 |
+
in_channels_static: Number of input channels for static data.
|
| 902 |
+
input_scalers_mu: Tensor of size (in_channels,). Used to rescale
|
| 903 |
+
input.
|
| 904 |
+
input_scalers_sigma: Tensor of size (in_channels,). Used to rescale
|
| 905 |
+
input.
|
| 906 |
+
input_scalers_epsilon: Float. Used to rescale input.
|
| 907 |
+
static_input_scalers_mu: Tensor of size (in_channels_static). Used
|
| 908 |
+
to rescale static inputs.
|
| 909 |
+
static_input_scalers_sigma: Tensor of size (in_channels_static).
|
| 910 |
+
Used to rescale static inputs.
|
| 911 |
+
static_input_scalers_epsilon: Float. Used to rescale static inputs.
|
| 912 |
+
output_scalers: Tensor of shape (in_channels,). Used to rescale
|
| 913 |
+
output.
|
| 914 |
+
n_lats_px: Total latitudes in data. In pixels.
|
| 915 |
+
n_lons_px: Total longitudes in data. In pixels.
|
| 916 |
+
patch_size_px: Patch size for tokenization. In pixels lat/lon.
|
| 917 |
+
mask_unit_size_px: Size of each mask unit. In pixels lat/lon.
|
| 918 |
+
mask_ratio_inputs: Masking ratio for inputs. 0 to 1.
|
| 919 |
+
mask_ratio_targets: Masking ratio for targets. 0 to 1.
|
| 920 |
+
embed_dim: Embedding dimension
|
| 921 |
+
n_blocks_encoder: Number of local-global transformer pairs in
|
| 922 |
+
encoder.
|
| 923 |
+
n_blocks_decoder: Number of local-global transformer pairs in
|
| 924 |
+
decoder.
|
| 925 |
+
mlp_multiplier: MLP multiplier for hidden features in feed forward
|
| 926 |
+
networks.
|
| 927 |
+
n_heads: Number of attention heads.
|
| 928 |
+
dropout: Dropout.
|
| 929 |
+
drop_path: DropPath.
|
| 930 |
+
parameter_dropout: Dropout applied to parameters.
|
| 931 |
+
residual: Indicates whether and how model should work as residual
|
| 932 |
+
model. Accepted values are 'climate', 'temporal' and 'none'
|
| 933 |
+
positional_encoding: possible values are ['absolute' (default), 'fourier'].
|
| 934 |
+
'absolute' lat lon encoded in 3 dimensions using sine and cosine
|
| 935 |
+
'fourier' lat/lon to be encoded using various frequencies
|
| 936 |
+
masking_mode: String ['local', 'global', 'both'] that controls the
|
| 937 |
+
type of masking used.
|
| 938 |
+
checkpoint_encoder: List of integers controlling if gradient checkpointing is used on encoder.
|
| 939 |
+
Format: [] for no gradient checkpointing. [3, 7] for checkpointing after 4th and 8th layer etc.
|
| 940 |
+
checkpoint_decoder: List of integers controlling if gradient checkpointing is used on decoder.
|
| 941 |
+
Format: See `checkpoint_encoder`.
|
| 942 |
+
masking_mode: The type of masking to use {'global', 'local', 'both'}
|
| 943 |
+
encoder_shifting: Whether to use swin shifting in the encoder.
|
| 944 |
+
decoder_shifting: Whether to use swin shifting in the decoder.
|
| 945 |
+
"""
|
| 946 |
+
super().__init__()
|
| 947 |
+
|
| 948 |
+
if mask_ratio_targets > 0.0:
|
| 949 |
+
raise NotImplementedError("Target masking is not implemented.")
|
| 950 |
+
|
| 951 |
+
self.in_channels = in_channels
|
| 952 |
+
self.input_size_time = input_size_time
|
| 953 |
+
self.in_channels_static = in_channels_static
|
| 954 |
+
self.n_lats_px = n_lats_px
|
| 955 |
+
self.n_lons_px = n_lons_px
|
| 956 |
+
self.patch_size_px = patch_size_px
|
| 957 |
+
self.mask_unit_size_px = mask_unit_size_px
|
| 958 |
+
self.mask_ratio_inputs = mask_ratio_inputs
|
| 959 |
+
self.mask_ratio_targets = mask_ratio_targets
|
| 960 |
+
self.embed_dim = embed_dim
|
| 961 |
+
self.n_blocks_encoder = n_blocks_encoder
|
| 962 |
+
self.n_blocks_decoder = n_blocks_decoder
|
| 963 |
+
self.mlp_multiplier = mlp_multiplier
|
| 964 |
+
self.n_heads = n_heads
|
| 965 |
+
self.dropout = dropout
|
| 966 |
+
self.drop_path = drop_path
|
| 967 |
+
self.residual = residual
|
| 968 |
+
self._encoder_shift = encoder_shifting
|
| 969 |
+
self._decoder_shift = decoder_shifting
|
| 970 |
+
self.positional_encoding = positional_encoding
|
| 971 |
+
self._checkpoint_encoder = checkpoint_encoder
|
| 972 |
+
self._checkpoint_decoder = checkpoint_decoder
|
| 973 |
+
|
| 974 |
+
assert self.n_lats_px % self.mask_unit_size_px[0] == 0
|
| 975 |
+
assert self.n_lons_px % self.mask_unit_size_px[1] == 0
|
| 976 |
+
assert self.mask_unit_size_px[0] % self.patch_size_px[0] == 0
|
| 977 |
+
assert self.mask_unit_size_px[1] % self.patch_size_px[1] == 0
|
| 978 |
+
|
| 979 |
+
if self.patch_size_px[0] != self.patch_size_px[1]:
|
| 980 |
+
raise NotImplementedError(
|
| 981 |
+
"Current pixel shuffle implementation assumes same patch size along both dimensions."
|
| 982 |
+
)
|
| 983 |
+
|
| 984 |
+
self.local_shape_mu = (
|
| 985 |
+
self.mask_unit_size_px[0] // self.patch_size_px[0],
|
| 986 |
+
self.mask_unit_size_px[1] // self.patch_size_px[1],
|
| 987 |
+
)
|
| 988 |
+
self.global_shape_mu = (
|
| 989 |
+
self.n_lats_px // self.mask_unit_size_px[0],
|
| 990 |
+
self.n_lons_px // self.mask_unit_size_px[1],
|
| 991 |
+
)
|
| 992 |
+
|
| 993 |
+
assert input_scalers_mu.shape == (in_channels,)
|
| 994 |
+
assert input_scalers_sigma.shape == (in_channels,)
|
| 995 |
+
assert output_scalers.shape == (in_channels,)
|
| 996 |
+
|
| 997 |
+
if self.positional_encoding != 'fourier':
|
| 998 |
+
assert static_input_scalers_mu.shape == (in_channels_static,)
|
| 999 |
+
assert static_input_scalers_sigma.shape == (in_channels_static,)
|
| 1000 |
+
|
| 1001 |
+
# Input shape [batch, time, parameter, lat, lon]
|
| 1002 |
+
self.input_scalers_epsilon = input_scalers_epsilon
|
| 1003 |
+
self.register_buffer('input_scalers_mu', input_scalers_mu.reshape(1, 1, -1, 1, 1))
|
| 1004 |
+
self.register_buffer('input_scalers_sigma', input_scalers_sigma.reshape(1, 1, -1, 1, 1))
|
| 1005 |
+
|
| 1006 |
+
# Static inputs shape [batch, parameter, lat, lon]
|
| 1007 |
+
self.static_input_scalers_epsilon = static_input_scalers_epsilon
|
| 1008 |
+
self.register_buffer('static_input_scalers_mu', static_input_scalers_mu.reshape(1, -1, 1, 1))
|
| 1009 |
+
self.register_buffer('static_input_scalers_sigma', static_input_scalers_sigma.reshape(1, -1, 1, 1))
|
| 1010 |
+
|
| 1011 |
+
# Output shape [batch, parameter, lat, lon]
|
| 1012 |
+
self.register_buffer('output_scalers', output_scalers.reshape(1, -1, 1, 1))
|
| 1013 |
+
|
| 1014 |
+
self.parameter_dropout = nn.Dropout2d(p=parameter_dropout)
|
| 1015 |
+
|
| 1016 |
+
self.patch_embedding = PatchEmbed(
|
| 1017 |
+
patch_size=patch_size_px,
|
| 1018 |
+
channels=in_channels * input_size_time,
|
| 1019 |
+
embed_dim=embed_dim,
|
| 1020 |
+
)
|
| 1021 |
+
|
| 1022 |
+
if self.residual == "climate":
|
| 1023 |
+
self.patch_embedding_static = PatchEmbed(
|
| 1024 |
+
patch_size=patch_size_px,
|
| 1025 |
+
channels=in_channels + in_channels_static,
|
| 1026 |
+
embed_dim=embed_dim,
|
| 1027 |
+
)
|
| 1028 |
+
else:
|
| 1029 |
+
self.patch_embedding_static = PatchEmbed(
|
| 1030 |
+
patch_size=patch_size_px,
|
| 1031 |
+
channels=in_channels_static,
|
| 1032 |
+
embed_dim=embed_dim,
|
| 1033 |
+
)
|
| 1034 |
+
|
| 1035 |
+
self.input_time_embedding = nn.Linear(1, embed_dim//4, bias=True)
|
| 1036 |
+
self.lead_time_embedding = nn.Linear(1, embed_dim//4, bias=True)
|
| 1037 |
+
|
| 1038 |
+
self.mask_token = nn.Parameter(torch.randn(1, 1, 1, self.embed_dim))
|
| 1039 |
+
self._nglobal_mu = np.prod(self.global_shape_mu)
|
| 1040 |
+
self._global_idx = torch.arange(self._nglobal_mu)
|
| 1041 |
+
|
| 1042 |
+
self._nlocal_mu = np.prod(self.local_shape_mu)
|
| 1043 |
+
self._local_idx = torch.arange(self._nlocal_mu)
|
| 1044 |
+
|
| 1045 |
+
if self._encoder_shift:
|
| 1046 |
+
self.encoder_shifter = e_shifter = SWINShiftNoBuffer(
|
| 1047 |
+
self.mask_unit_size_px,
|
| 1048 |
+
self.global_shape_mu,
|
| 1049 |
+
self.local_shape_mu,
|
| 1050 |
+
self.patch_size_px,
|
| 1051 |
+
n_context_tokens=0,
|
| 1052 |
+
)
|
| 1053 |
+
else:
|
| 1054 |
+
self.encoder_shifter = e_shifter = None
|
| 1055 |
+
self.encoder = PrithviWxCEncoderDecoder(
|
| 1056 |
+
embed_dim=embed_dim,
|
| 1057 |
+
n_blocks=n_blocks_encoder,
|
| 1058 |
+
mlp_multiplier=mlp_multiplier,
|
| 1059 |
+
n_heads=n_heads,
|
| 1060 |
+
dropout=dropout,
|
| 1061 |
+
drop_path=drop_path,
|
| 1062 |
+
shifter=e_shifter,
|
| 1063 |
+
transformer_cp=checkpoint_encoder,
|
| 1064 |
+
)
|
| 1065 |
+
|
| 1066 |
+
if n_blocks_decoder != 0:
|
| 1067 |
+
if self._decoder_shift:
|
| 1068 |
+
self.decoder_shifter = d_shifter = SWINShift(
|
| 1069 |
+
self.mask_unit_size_px,
|
| 1070 |
+
self.global_shape_mu,
|
| 1071 |
+
self.local_shape_mu,
|
| 1072 |
+
self.patch_size_px,
|
| 1073 |
+
n_context_tokens=0,
|
| 1074 |
+
)
|
| 1075 |
+
else:
|
| 1076 |
+
self.decoder_shifter = d_shifter = None
|
| 1077 |
+
|
| 1078 |
+
self.decoder = PrithviWxCEncoderDecoder(
|
| 1079 |
+
embed_dim=embed_dim,
|
| 1080 |
+
n_blocks=n_blocks_decoder,
|
| 1081 |
+
mlp_multiplier=mlp_multiplier,
|
| 1082 |
+
n_heads=n_heads,
|
| 1083 |
+
dropout=dropout,
|
| 1084 |
+
drop_path=0.,
|
| 1085 |
+
shifter=d_shifter,
|
| 1086 |
+
transformer_cp=checkpoint_decoder,
|
| 1087 |
+
)
|
| 1088 |
+
|
| 1089 |
+
self.unembed = nn.Linear(
|
| 1090 |
+
self.embed_dim,
|
| 1091 |
+
self.in_channels * self.patch_size_px[0] * self.patch_size_px[1],
|
| 1092 |
+
bias=True,
|
| 1093 |
+
)
|
| 1094 |
+
|
| 1095 |
+
self.masking_mode = masking_mode.lower()
|
| 1096 |
+
match self.masking_mode:
|
| 1097 |
+
case "local":
|
| 1098 |
+
self.generate_mask = self._gen_mask_local
|
| 1099 |
+
case "global":
|
| 1100 |
+
self.generate_mask = self._gen_mask_global
|
| 1101 |
+
case "both":
|
| 1102 |
+
self._mask_both_local: bool = True
|
| 1103 |
+
self.generate_mask = self._gen_mask_both
|
| 1104 |
+
case _:
|
| 1105 |
+
raise ValueError(f"Masking mode '{masking_mode}' not supported")
|
| 1106 |
+
|
| 1107 |
+
def swap_masking(self) -> None:
|
| 1108 |
+
if hasattr(self, '_mask_both_local'):
|
| 1109 |
+
self._mask_both_local = not self._mask_both_local
|
| 1110 |
+
|
| 1111 |
+
@cached_property
|
| 1112 |
+
def n_masked_global(self):
|
| 1113 |
+
return int(self.mask_ratio_inputs * np.prod(self.global_shape_mu))
|
| 1114 |
+
|
| 1115 |
+
@cached_property
|
| 1116 |
+
def n_masked_local(self):
|
| 1117 |
+
return int(self.mask_ratio_inputs * np.prod(self.local_shape_mu))
|
| 1118 |
+
|
| 1119 |
+
@staticmethod
|
| 1120 |
+
def _shuffle_along_axis(a, axis):
|
| 1121 |
+
# https://stackoverflow.com/questions/5040797/shuffling-numpy-array-along-a-given-axis
|
| 1122 |
+
idx = torch.argsort(input=torch.rand(*a.shape), dim=axis)
|
| 1123 |
+
return torch.gather(a, dim=axis, index=idx)
|
| 1124 |
+
|
| 1125 |
+
def _gen_mask_local(self, sizes: tuple[int]) -> tuple[Tensor]:
|
| 1126 |
+
"""
|
| 1127 |
+
Args:
|
| 1128 |
+
batch_size: Number of elements in batch
|
| 1129 |
+
Returns:
|
| 1130 |
+
Tuple of torch tensors. [indices masked, indices unmasked].
|
| 1131 |
+
Each of these is a tensor of shape (batch, global sequene)
|
| 1132 |
+
"""
|
| 1133 |
+
# We identifies which indices (values) should be masked
|
| 1134 |
+
|
| 1135 |
+
maskable_indices = self._local_idx.view(1, -1).expand(*sizes[:2], -1)
|
| 1136 |
+
|
| 1137 |
+
maskable_indices = self._shuffle_along_axis(maskable_indices, 2)
|
| 1138 |
+
|
| 1139 |
+
# `...` cannot be jit'd :-(
|
| 1140 |
+
indices_masked = maskable_indices[:, :, : self.n_masked_local]
|
| 1141 |
+
indices_unmasked = maskable_indices[:, :, self.n_masked_local :]
|
| 1142 |
+
|
| 1143 |
+
return indices_masked, indices_unmasked
|
| 1144 |
+
|
| 1145 |
+
def _gen_mask_global(self, sizes: tuple[int]) -> tuple[Tensor]:
|
| 1146 |
+
"""
|
| 1147 |
+
Args:
|
| 1148 |
+
batch_size: Number of elements in batch
|
| 1149 |
+
Returns:
|
| 1150 |
+
Tuple of torch tensors. [indices masked, indices unmasked].
|
| 1151 |
+
Each of these is a tensor of shape (batch, global sequene)
|
| 1152 |
+
"""
|
| 1153 |
+
# We identifies which indices (values) should be masked
|
| 1154 |
+
|
| 1155 |
+
maskable_indices = self._global_idx.view(1, -1).expand(*sizes[:1], -1)
|
| 1156 |
+
|
| 1157 |
+
maskable_indices = self._shuffle_along_axis(maskable_indices, 1)
|
| 1158 |
+
|
| 1159 |
+
indices_masked = maskable_indices[:, : self.n_masked_global]
|
| 1160 |
+
indices_unmasked = maskable_indices[:, self.n_masked_global :]
|
| 1161 |
+
|
| 1162 |
+
return indices_masked, indices_unmasked
|
| 1163 |
+
|
| 1164 |
+
def _gen_mask_both(self, sizes: tuple[int]) -> tuple[Tensor]:
|
| 1165 |
+
if self._mask_both_local:
|
| 1166 |
+
return self._gen_mask_local(sizes)
|
| 1167 |
+
else:
|
| 1168 |
+
return self._gen_mask_global(sizes)
|
| 1169 |
+
|
| 1170 |
+
@staticmethod
|
| 1171 |
+
def reconstruct_batch(
|
| 1172 |
+
idx_masked: Tensor,
|
| 1173 |
+
idx_unmasked: Tensor,
|
| 1174 |
+
data_masked: Tensor,
|
| 1175 |
+
data_unmasked: Tensor,
|
| 1176 |
+
) -> Tensor:
|
| 1177 |
+
"""
|
| 1178 |
+
Reconstructs a tensor along the mask unit dimension. Batched version.
|
| 1179 |
+
|
| 1180 |
+
Args:
|
| 1181 |
+
idx_masked: Tensor of shape `batch, mask unit sequence`.
|
| 1182 |
+
idx_unmasked: Tensor of shape `batch, mask unit sequence`.
|
| 1183 |
+
data_masked: Tensor of shape `batch, mask unit sequence, ...`.
|
| 1184 |
+
Should have same size along mask unit sequence dimension as
|
| 1185 |
+
idx_masked. Dimensions beyond the first two, marked here as ...
|
| 1186 |
+
will typically be `local_sequence, channel` or `channel, lat, lon`.
|
| 1187 |
+
These dimensions should agree with data_unmasked.
|
| 1188 |
+
data_unmasked: Tensor of shape `batch, mask unit sequence, ...`.
|
| 1189 |
+
Should have same size along mask unit sequence dimension as
|
| 1190 |
+
idx_unmasked. Dimensions beyond the first two, marked here as
|
| 1191 |
+
... will typically be `local_sequence, channel` or `channel,
|
| 1192 |
+
lat, lon`. These dimensions should agree with data_masked.
|
| 1193 |
+
Returns:
|
| 1194 |
+
Tensor of same shape as inputs data_masked and data_unmasked. I.e.
|
| 1195 |
+
`batch, mask unit sequence, ...`. Index for the total data composed
|
| 1196 |
+
of the masked and the unmasked part
|
| 1197 |
+
"""
|
| 1198 |
+
dim: int = idx_masked.ndim
|
| 1199 |
+
|
| 1200 |
+
idx_total = torch.argsort(torch.cat([idx_masked, idx_unmasked], dim=-1), dim=-1)
|
| 1201 |
+
idx_total = idx_total.view(*idx_total.shape, *[1] * (data_unmasked.ndim - dim))
|
| 1202 |
+
idx_total = idx_total.expand(*idx_total.shape[:dim], *data_unmasked.shape[dim:])
|
| 1203 |
+
|
| 1204 |
+
data = torch.cat([data_masked, data_unmasked], dim=dim - 1)
|
| 1205 |
+
data = torch.gather(data, dim=dim - 1, index=idx_total)
|
| 1206 |
+
|
| 1207 |
+
return data, idx_total
|
| 1208 |
+
|
| 1209 |
+
def fourier_pos_encoding(self, x_static):
|
| 1210 |
+
"""
|
| 1211 |
+
Args
|
| 1212 |
+
x_static: B x C x H x W. first two channels are lat, and lon respectively
|
| 1213 |
+
Returns
|
| 1214 |
+
Tensor of shape B x E x H x W where E is the embedding dimension.
|
| 1215 |
+
"""
|
| 1216 |
+
|
| 1217 |
+
# B x C x H x W -> B x 1 x H/P x W/P
|
| 1218 |
+
latitudes_patch = F.avg_pool2d(x_static[:, [0]], kernel_size=self.patch_size_px, stride=self.patch_size_px)
|
| 1219 |
+
longitudes_patch = F.avg_pool2d(x_static[:, [1]], kernel_size=self.patch_size_px, stride=self.patch_size_px)
|
| 1220 |
+
|
| 1221 |
+
modes = torch.arange(self.embed_dim//4, device=x_static.device).view(1, -1, 1, 1) + 1.
|
| 1222 |
+
pos_encoding = torch.cat(
|
| 1223 |
+
(
|
| 1224 |
+
torch.sin(latitudes_patch*modes),
|
| 1225 |
+
torch.sin(longitudes_patch*modes),
|
| 1226 |
+
torch.cos(latitudes_patch*modes),
|
| 1227 |
+
torch.cos(longitudes_patch*modes),
|
| 1228 |
+
),
|
| 1229 |
+
axis=1
|
| 1230 |
+
)
|
| 1231 |
+
|
| 1232 |
+
return pos_encoding # B x E x H/P x W/P
|
| 1233 |
+
|
| 1234 |
+
def time_encoding(self, input_time, lead_time):
|
| 1235 |
+
'''
|
| 1236 |
+
Args:
|
| 1237 |
+
input_time: Tensor of shape [batch].
|
| 1238 |
+
lead_time: Tensor of shape [batch].
|
| 1239 |
+
Returns:
|
| 1240 |
+
Tensor of shape [batch, embed_dim, 1, 1]
|
| 1241 |
+
'''
|
| 1242 |
+
input_time = self.input_time_embedding(input_time.view(-1, 1, 1, 1))
|
| 1243 |
+
lead_time = self.lead_time_embedding(lead_time.view(-1, 1, 1, 1))
|
| 1244 |
+
|
| 1245 |
+
time_encoding = torch.cat(
|
| 1246 |
+
(
|
| 1247 |
+
torch.cos(input_time),
|
| 1248 |
+
torch.cos(lead_time),
|
| 1249 |
+
torch.sin(input_time),
|
| 1250 |
+
torch.sin(lead_time),
|
| 1251 |
+
),
|
| 1252 |
+
axis=3
|
| 1253 |
+
)
|
| 1254 |
+
return time_encoding
|
| 1255 |
+
|
| 1256 |
+
def to_patching(self, x: Tensor) -> Tensor:
|
| 1257 |
+
"""Transform data from lat/lon space to two axis patching
|
| 1258 |
+
|
| 1259 |
+
Args: ->
|
| 1260 |
+
x: Tesnor in lat/lon space (N, C, Nlat//P_0, Nlon//P_1)
|
| 1261 |
+
|
| 1262 |
+
Returns:
|
| 1263 |
+
Tensor in patch space (N, G, L, C)
|
| 1264 |
+
"""
|
| 1265 |
+
n_batch = x.shape[0]
|
| 1266 |
+
|
| 1267 |
+
x = x.view(
|
| 1268 |
+
n_batch,
|
| 1269 |
+
self.embed_dim,
|
| 1270 |
+
self.global_shape_mu[0],
|
| 1271 |
+
self.local_shape_mu[0],
|
| 1272 |
+
self.global_shape_mu[1],
|
| 1273 |
+
self.local_shape_mu[1],
|
| 1274 |
+
)
|
| 1275 |
+
x = x.permute(0, 2, 4, 3, 5, 1).contiguous()
|
| 1276 |
+
|
| 1277 |
+
s = x.shape
|
| 1278 |
+
return x.view(n_batch, s[1] * s[2], s[3] * s[4], -1)
|
| 1279 |
+
|
| 1280 |
+
def from_patching(self, x: Tensor) -> Tensor:
|
| 1281 |
+
"""Transform data from two axis patching to lat/lon space
|
| 1282 |
+
|
| 1283 |
+
Args:
|
| 1284 |
+
x: Tensor in patch space with shape (N, G, L, C*P_0*P_1)
|
| 1285 |
+
|
| 1286 |
+
Returns:
|
| 1287 |
+
Tensor in lat/lon space (N, C*P_0*P_1, Nlat//P_0, Nlon // P_1)
|
| 1288 |
+
"""
|
| 1289 |
+
n_batch = x.shape[0]
|
| 1290 |
+
|
| 1291 |
+
x = x.view(
|
| 1292 |
+
n_batch,
|
| 1293 |
+
self.global_shape_mu[0],
|
| 1294 |
+
self.global_shape_mu[1],
|
| 1295 |
+
self.local_shape_mu[0],
|
| 1296 |
+
self.local_shape_mu[1],
|
| 1297 |
+
-1,
|
| 1298 |
+
)
|
| 1299 |
+
x = x.permute(0, 5, 1, 3, 2, 4).contiguous()
|
| 1300 |
+
|
| 1301 |
+
s = x.shape
|
| 1302 |
+
return x.view(n_batch, -1, s[2]*s[3], s[4]*s[5])
|
| 1303 |
+
|
| 1304 |
+
def forward(self, batch: dict[str, torch.Tensor]) -> torch.Tensor:
|
| 1305 |
+
"""
|
| 1306 |
+
Args:
|
| 1307 |
+
batch: Dictionary containing the keys 'x', 'y', 'input_time',
|
| 1308 |
+
'lead_time' and 'static'. The associated torch tensors have the
|
| 1309 |
+
following shapes:
|
| 1310 |
+
x: Tensor of shape [batch, time, parameter, lat, lon]
|
| 1311 |
+
y: Tensor of shape [batch, parameter, lat, lon]
|
| 1312 |
+
static: Tensor of shape [batch, channel_static, lat, lon]
|
| 1313 |
+
climate: Optional tensor of shape [batch, parameter, lat, lon]
|
| 1314 |
+
input_time: Tensor of shape [batch]. Or none.
|
| 1315 |
+
lead_time: Tensor of shape [batch]. Or none.
|
| 1316 |
+
Returns:
|
| 1317 |
+
Tensor of shape [batch, parameter, lat, lon].
|
| 1318 |
+
"""
|
| 1319 |
+
assert batch["x"].shape[2] == self.in_channels
|
| 1320 |
+
assert batch["x"].shape[3] == self.n_lats_px
|
| 1321 |
+
assert batch["x"].shape[4] == self.n_lons_px
|
| 1322 |
+
assert batch["y"].shape[1] == self.in_channels
|
| 1323 |
+
assert batch["y"].shape[2] == self.n_lats_px
|
| 1324 |
+
assert batch["y"].shape[3] == self.n_lons_px
|
| 1325 |
+
if self.positional_encoding == 'fourier':
|
| 1326 |
+
# the first two features (lat, lon) are encoded separately
|
| 1327 |
+
assert batch['static'].shape[1] - 2 == self.in_channels_static, "When setting self.positional_encoding to fourier, the number of static params change in the dataset. So, in the config, reduce num_static_channels (e.g., 4 instead of 7)."
|
| 1328 |
+
else:
|
| 1329 |
+
assert batch['static'].shape[1] == self.in_channels_static
|
| 1330 |
+
assert batch["static"].shape[2] == self.n_lats_px
|
| 1331 |
+
assert batch["static"].shape[3] == self.n_lons_px
|
| 1332 |
+
|
| 1333 |
+
x_rescaled = (batch["x"] - self.input_scalers_mu) / (
|
| 1334 |
+
self.input_scalers_sigma + self.input_scalers_epsilon
|
| 1335 |
+
)
|
| 1336 |
+
batch_size = x_rescaled.shape[0]
|
| 1337 |
+
|
| 1338 |
+
if self.positional_encoding == 'fourier':
|
| 1339 |
+
x_static_pos = self.fourier_pos_encoding(batch['static']) # B, embed_dim, lat / patch_size, lon / patch_size
|
| 1340 |
+
x_static = (batch['static'][:, 2:] - self.static_input_scalers_mu[:, 3:]) / ( # The first two channels in batch['static'] are used in positional encoding
|
| 1341 |
+
self.static_input_scalers_sigma[:, 3:] + self.static_input_scalers_epsilon # This translates to the first three channels in 'static_input_scalers_mu'
|
| 1342 |
+
)
|
| 1343 |
+
else:
|
| 1344 |
+
x_static = (batch["static"] - self.static_input_scalers_mu) / (
|
| 1345 |
+
self.static_input_scalers_sigma + self.static_input_scalers_epsilon
|
| 1346 |
+
)
|
| 1347 |
+
|
| 1348 |
+
if self.residual == "temporal":
|
| 1349 |
+
# We create a residual of same shape as y
|
| 1350 |
+
index = torch.where(batch["lead_time"] > 0, batch["x"].shape[1] - 1, 0)
|
| 1351 |
+
index = index.view(-1, 1, 1, 1, 1)
|
| 1352 |
+
index = index.expand(batch_size, 1, *batch["x"].shape[2:])
|
| 1353 |
+
x_hat = torch.gather(batch["x"], dim=1, index=index)
|
| 1354 |
+
x_hat = x_hat.squeeze(1)
|
| 1355 |
+
assert (
|
| 1356 |
+
batch["y"].shape == x_hat.shape
|
| 1357 |
+
), f'Shapes {batch["y"].shape} and {x_hat.shape} do not agree.'
|
| 1358 |
+
elif self.residual == "climate":
|
| 1359 |
+
climate_scaled = (
|
| 1360 |
+
batch["climate"] - self.input_scalers_mu.view(1, -1, 1, 1)
|
| 1361 |
+
) / (
|
| 1362 |
+
self.input_scalers_sigma.view(1, -1, 1, 1) + self.input_scalers_epsilon
|
| 1363 |
+
)
|
| 1364 |
+
|
| 1365 |
+
# [batch, time, parameter, lat, lon] -> [batch, time x parameter, lat, lon]
|
| 1366 |
+
x_rescaled = x_rescaled.flatten(1, 2)
|
| 1367 |
+
# Parameter dropout
|
| 1368 |
+
x_rescaled = self.parameter_dropout(x_rescaled)
|
| 1369 |
+
|
| 1370 |
+
x_embedded = self.patch_embedding(x_rescaled)
|
| 1371 |
+
assert x_embedded.shape[1] == self.embed_dim
|
| 1372 |
+
|
| 1373 |
+
if self.residual == "climate":
|
| 1374 |
+
static_embedded = self.patch_embedding_static(
|
| 1375 |
+
torch.cat((x_static, climate_scaled), dim=1)
|
| 1376 |
+
)
|
| 1377 |
+
else:
|
| 1378 |
+
static_embedded = self.patch_embedding_static(x_static)
|
| 1379 |
+
assert static_embedded.shape[1] == self.embed_dim
|
| 1380 |
+
|
| 1381 |
+
if self.positional_encoding == 'fourier':
|
| 1382 |
+
static_embedded += x_static_pos
|
| 1383 |
+
|
| 1384 |
+
x_embedded = self.to_patching(x_embedded)
|
| 1385 |
+
static_embedded = self.to_patching(static_embedded)
|
| 1386 |
+
|
| 1387 |
+
time_encoding = self.time_encoding(batch['input_time'], batch['lead_time'])
|
| 1388 |
+
|
| 1389 |
+
tokens = x_embedded + static_embedded + time_encoding
|
| 1390 |
+
|
| 1391 |
+
# Now we generate masks based on masking_mode
|
| 1392 |
+
indices_masked, indices_unmasked = self.generate_mask(
|
| 1393 |
+
(batch_size, self._nglobal_mu)
|
| 1394 |
+
)
|
| 1395 |
+
indices_masked = indices_masked.to(device=tokens.device)
|
| 1396 |
+
indices_unmasked = indices_unmasked.to(device=tokens.device)
|
| 1397 |
+
maskdim: int = indices_masked.ndim
|
| 1398 |
+
|
| 1399 |
+
# Unmasking
|
| 1400 |
+
unmask_view = (*indices_unmasked.shape, *[1] * (tokens.ndim - maskdim))
|
| 1401 |
+
unmasked = torch.gather(
|
| 1402 |
+
tokens,
|
| 1403 |
+
dim=maskdim - 1,
|
| 1404 |
+
index=indices_unmasked.view(*unmask_view).expand(
|
| 1405 |
+
*indices_unmasked.shape, *tokens.shape[maskdim:]
|
| 1406 |
+
),
|
| 1407 |
+
)
|
| 1408 |
+
|
| 1409 |
+
# Encoder
|
| 1410 |
+
x_encoded = self.encoder(unmasked)
|
| 1411 |
+
|
| 1412 |
+
# Generate and position encode the mask tokens
|
| 1413 |
+
# (1, 1, 1, embed_dim) -> (batch, global_seq_masked, local seq, embed_dim)
|
| 1414 |
+
mask_view = (*indices_masked.shape, *[1] * (tokens.ndim - maskdim))
|
| 1415 |
+
masking = self.mask_token.repeat(*static_embedded.shape[:3], 1)
|
| 1416 |
+
masked = masking + static_embedded
|
| 1417 |
+
masked = torch.gather(
|
| 1418 |
+
masked,
|
| 1419 |
+
dim=maskdim - 1,
|
| 1420 |
+
index=indices_masked.view(*mask_view).expand(
|
| 1421 |
+
*indices_masked.shape, *tokens.shape[maskdim:]
|
| 1422 |
+
),
|
| 1423 |
+
)
|
| 1424 |
+
|
| 1425 |
+
recon, _ = self.reconstruct_batch(
|
| 1426 |
+
indices_masked, indices_unmasked, masked, x_encoded
|
| 1427 |
+
)
|
| 1428 |
+
|
| 1429 |
+
x_decoded = self.decoder(recon)
|
| 1430 |
+
|
| 1431 |
+
# Output: (batch, global sequence, local sequence, in_channels * patch_size[0] * patch_size[1])
|
| 1432 |
+
x_unembed = self.unembed(x_decoded)
|
| 1433 |
+
|
| 1434 |
+
# Reshape to (batch, global_lat, global_lon, local_lat, local_lon, in_channels * patch_size[0] * patch_size[1])
|
| 1435 |
+
assert x_unembed.shape[0] == batch_size
|
| 1436 |
+
assert x_unembed.shape[1] == self.global_shape_mu[0] * self.global_shape_mu[1]
|
| 1437 |
+
assert x_unembed.shape[2] == self.local_shape_mu[0] * self.local_shape_mu[1]
|
| 1438 |
+
assert (
|
| 1439 |
+
x_unembed.shape[3]
|
| 1440 |
+
== self.in_channels * self.patch_size_px[0] * self.patch_size_px[1]
|
| 1441 |
+
)
|
| 1442 |
+
|
| 1443 |
+
x_out = self.from_patching(x_unembed)
|
| 1444 |
+
|
| 1445 |
+
# Pixel shuffle to (batch, in_channels, lat, lon)
|
| 1446 |
+
x_out = F.pixel_shuffle(x_out, self.patch_size_px[0])
|
| 1447 |
+
|
| 1448 |
+
if self.residual == "temporal":
|
| 1449 |
+
x_out = self.output_scalers * x_out + x_hat
|
| 1450 |
+
elif self.residual == "climate":
|
| 1451 |
+
x_out = self.output_scalers * x_out + batch["climate"]
|
| 1452 |
+
elif self.residual == "none":
|
| 1453 |
+
x_out = self.output_scalers * x_out + self.input_scalers_mu.reshape(
|
| 1454 |
+
1, -1, 1, 1
|
| 1455 |
+
)
|
| 1456 |
+
|
| 1457 |
+
return x_out
|
| 1458 |
+
|
| 1459 |
+
|
| 1460 |
+
def masked_inference(
|
| 1461 |
+
model,
|
| 1462 |
+
batch: dict[str, torch.Tensor],
|
| 1463 |
+
mask_global: Optional[torch.Tensor] = None,
|
| 1464 |
+
mask_local: Optional[torch.Tensor] = None,
|
| 1465 |
+
) -> torch.Tensor:
|
| 1466 |
+
"""
|
| 1467 |
+
Perform masked inference with Prithvi-WxC model
|
| 1468 |
+
|
| 1469 |
+
Args:
|
| 1470 |
+
model: The PrithviWxC model to perform the inference with.
|
| 1471 |
+
batch: Dictionary containing the keys 'x', 'y', 'input_time',
|
| 1472 |
+
'lead_time' and 'static'. The associated torch tensors have the
|
| 1473 |
+
following shapes:
|
| 1474 |
+
x: Tensor of shape [batch, time, parameter, lat, lon]
|
| 1475 |
+
y: Tensor of shape [batch, parameter, lat, lon]
|
| 1476 |
+
static: Tensor of shape [batch, channel_static, lat, lon]
|
| 1477 |
+
climate: Optional tensor of shape [batch, parameter, lat, lon]
|
| 1478 |
+
input_time: Tensor of shape [batch]. Or none.
|
| 1479 |
+
lead_time: Tensor of shape [batch]. Or none.
|
| 1480 |
+
mask_global: A 2D tensor of shape [n_lats_px / mask_unit_size_px[0], n_lons_px / patch_size_px[1]]
|
| 1481 |
+
identifying the global patches to mask.
|
| 1482 |
+
mask_local: A 2D tensor of shape [mask_unit_size_px[0] / patch_size_px[0], mask_unit_size_px[1] / patch_size_px[1]]
|
| 1483 |
+
identifying the global patches to mask.
|
| 1484 |
+
Returns:
|
| 1485 |
+
Tensor of shape [batch, parameter, lat, lon].
|
| 1486 |
+
"""
|
| 1487 |
+
assert batch["x"].shape[2] == model.in_channels
|
| 1488 |
+
assert batch["x"].shape[3] == model.n_lats_px
|
| 1489 |
+
assert batch["x"].shape[4] == model.n_lons_px
|
| 1490 |
+
assert batch["y"].shape[1] == model.in_channels
|
| 1491 |
+
assert batch["y"].shape[2] == model.n_lats_px
|
| 1492 |
+
assert batch["y"].shape[3] == model.n_lons_px
|
| 1493 |
+
if model.positional_encoding == 'fourier':
|
| 1494 |
+
# the first two features (lat, lon) are encoded separately
|
| 1495 |
+
assert batch['static'].shape[1] - 2 == model.in_channels_static, "When setting model.positional_encoding to fourier, the number of static params change in the dataset. So, in the config, reduce num_static_channels (e.g., 4 instead of 7)."
|
| 1496 |
+
else:
|
| 1497 |
+
assert batch['static'].shape[1] == model.in_channels_static
|
| 1498 |
+
assert batch["static"].shape[2] == model.n_lats_px
|
| 1499 |
+
assert batch["static"].shape[3] == model.n_lons_px
|
| 1500 |
+
|
| 1501 |
+
x_rescaled = (batch["x"] - model.input_scalers_mu) / (
|
| 1502 |
+
model.input_scalers_sigma + model.input_scalers_epsilon
|
| 1503 |
+
)
|
| 1504 |
+
batch_size = x_rescaled.shape[0]
|
| 1505 |
+
|
| 1506 |
+
if model.positional_encoding == 'fourier':
|
| 1507 |
+
x_static_pos = model.fourier_pos_encoding(batch['static']) # B, embed_dim, lat / patch_size, lon / patch_size
|
| 1508 |
+
x_static = (batch['static'][:, 2:] - model.static_input_scalers_mu[:, 3:]) / ( # The first two channels in batch['static'] are used in positional encoding
|
| 1509 |
+
model.static_input_scalers_sigma[:, 3:] + model.static_input_scalers_epsilon # This translates to the first three channels in 'static_input_scalers_mu'
|
| 1510 |
+
)
|
| 1511 |
+
else:
|
| 1512 |
+
x_static = (batch["static"] - model.static_input_scalers_mu) / (
|
| 1513 |
+
model.static_input_scalers_sigma + model.static_input_scalers_epsilon
|
| 1514 |
+
)
|
| 1515 |
+
|
| 1516 |
+
if model.residual == "temporal":
|
| 1517 |
+
# We create a residual of same shape as y
|
| 1518 |
+
index = torch.where(batch["lead_time"] > 0, batch["x"].shape[1] - 1, 0)
|
| 1519 |
+
index = index.view(-1, 1, 1, 1, 1)
|
| 1520 |
+
index = index.expand(batch_size, 1, *batch["x"].shape[2:])
|
| 1521 |
+
x_hat = torch.gather(batch["x"], dim=1, index=index)
|
| 1522 |
+
x_hat = x_hat.squeeze(1)
|
| 1523 |
+
assert (
|
| 1524 |
+
batch["y"].shape == x_hat.shape
|
| 1525 |
+
), f'Shapes {batch["y"].shape} and {x_hat.shape} do not agree.'
|
| 1526 |
+
elif model.residual == "climate":
|
| 1527 |
+
climate_scaled = (
|
| 1528 |
+
batch["climate"] - model.input_scalers_mu.view(1, -1, 1, 1)
|
| 1529 |
+
) / (
|
| 1530 |
+
model.input_scalers_sigma.view(1, -1, 1, 1) + model.input_scalers_epsilon
|
| 1531 |
+
)
|
| 1532 |
+
|
| 1533 |
+
# [batch, time, parameter, lat, lon] -> [batch, time x parameter, lat, lon]
|
| 1534 |
+
x_rescaled = x_rescaled.flatten(1, 2)
|
| 1535 |
+
# Parameter dropout
|
| 1536 |
+
x_rescaled = model.parameter_dropout(x_rescaled)
|
| 1537 |
+
|
| 1538 |
+
x_embedded = model.patch_embedding(x_rescaled)
|
| 1539 |
+
assert x_embedded.shape[1] == model.embed_dim
|
| 1540 |
+
|
| 1541 |
+
if model.residual == "climate":
|
| 1542 |
+
static_embedded = model.patch_embedding_static(
|
| 1543 |
+
torch.cat((x_static, climate_scaled), dim=1)
|
| 1544 |
+
)
|
| 1545 |
+
else:
|
| 1546 |
+
static_embedded = model.patch_embedding_static(x_static)
|
| 1547 |
+
assert static_embedded.shape[1] == model.embed_dim
|
| 1548 |
+
|
| 1549 |
+
if model.positional_encoding == 'fourier':
|
| 1550 |
+
static_embedded += x_static_pos
|
| 1551 |
+
|
| 1552 |
+
x_embedded = model.to_patching(x_embedded)
|
| 1553 |
+
static_embedded = model.to_patching(static_embedded)
|
| 1554 |
+
|
| 1555 |
+
time_encoding = model.time_encoding(batch['input_time'], batch['lead_time'])
|
| 1556 |
+
|
| 1557 |
+
tokens = x_embedded + static_embedded + time_encoding
|
| 1558 |
+
|
| 1559 |
+
if mask_global is not None:
|
| 1560 |
+
indices_masked = torch.where(mask_global.flatten())[0]
|
| 1561 |
+
indices_masked = torch.repeat_interleave(indices_masked[None], tokens.shape[0], dim=0)
|
| 1562 |
+
indices_unmasked = torch.where(~mask_global.flatten())[0]
|
| 1563 |
+
indices_unmasked = torch.repeat_interleave(indices_unmasked[None], tokens.shape[0], dim=0)
|
| 1564 |
+
elif mask_local is not None:
|
| 1565 |
+
indices_masked = torch.where(mask_local.flatten())[0]
|
| 1566 |
+
indices_masked = indices_masked[None, None].expand(tokens.shape[0], tokens.shape[1], -1)
|
| 1567 |
+
indices_unmasked = torch.where(~mask_local.flatten())[0]
|
| 1568 |
+
indices_unmasked = indices_unmasked[None, None].expand(tokens.shape[0], tokens.shape[1], -1)
|
| 1569 |
+
print(indices_unmasked.shape)
|
| 1570 |
+
else:
|
| 1571 |
+
mask_global = torch.zeros(model.global_shape_mu, dtype=torch.bool)
|
| 1572 |
+
indices_masked = torch.where(mask_global.flatten())[0]
|
| 1573 |
+
indices_masked = torch.repeat_interleave(indices_masked[None], tokens.shape[0], dim=0)
|
| 1574 |
+
indices_unmasked = torch.where(~mask_global.flatten())[0]
|
| 1575 |
+
indices_unmasked = torch.repeat_interleave(indices_unmasked[None], tokens.shape[0], dim=0)
|
| 1576 |
+
|
| 1577 |
+
maskdim: int = indices_masked.ndim
|
| 1578 |
+
|
| 1579 |
+
# Unmasking
|
| 1580 |
+
unmask_view = (*indices_unmasked.shape, *[1] * (tokens.ndim - maskdim))
|
| 1581 |
+
unmasked = torch.gather(
|
| 1582 |
+
tokens,
|
| 1583 |
+
dim=maskdim - 1,
|
| 1584 |
+
index=indices_unmasked.view(*unmask_view).expand(
|
| 1585 |
+
*indices_unmasked.shape, *tokens.shape[maskdim:]
|
| 1586 |
+
),
|
| 1587 |
+
)
|
| 1588 |
+
|
| 1589 |
+
# Encoder
|
| 1590 |
+
x_encoded = model.encoder(unmasked)
|
| 1591 |
+
|
| 1592 |
+
# Generate and position encode the mask tokens
|
| 1593 |
+
# (1, 1, 1, embed_dim) -> (batch, global_seq_masked, local seq, embed_dim)
|
| 1594 |
+
mask_view = (*indices_masked.shape, *[1] * (tokens.ndim - maskdim))
|
| 1595 |
+
masking = model.mask_token.repeat(*static_embedded.shape[:3], 1)
|
| 1596 |
+
masked = masking + static_embedded
|
| 1597 |
+
masked = torch.gather(
|
| 1598 |
+
masked,
|
| 1599 |
+
dim=maskdim - 1,
|
| 1600 |
+
index=indices_masked.view(*mask_view).expand(
|
| 1601 |
+
*indices_masked.shape, *tokens.shape[maskdim:]
|
| 1602 |
+
),
|
| 1603 |
+
)
|
| 1604 |
+
|
| 1605 |
+
recon, _ = model.reconstruct_batch(
|
| 1606 |
+
indices_masked, indices_unmasked, masked, x_encoded
|
| 1607 |
+
)
|
| 1608 |
+
|
| 1609 |
+
x_decoded = model.decoder(recon)
|
| 1610 |
+
|
| 1611 |
+
# Output: (batch, global sequence, local sequence, in_channels * patch_size[0] * patch_size[1])
|
| 1612 |
+
x_unembed = model.unembed(x_decoded)
|
| 1613 |
+
|
| 1614 |
+
# Reshape to (batch, global_lat, global_lon, local_lat, local_lon, in_channels * patch_size[0] * patch_size[1])
|
| 1615 |
+
assert x_unembed.shape[0] == batch_size
|
| 1616 |
+
assert x_unembed.shape[1] == model.global_shape_mu[0] * model.global_shape_mu[1]
|
| 1617 |
+
assert x_unembed.shape[2] == model.local_shape_mu[0] * model.local_shape_mu[1]
|
| 1618 |
+
assert (
|
| 1619 |
+
x_unembed.shape[3]
|
| 1620 |
+
== model.in_channels * model.patch_size_px[0] * model.patch_size_px[1]
|
| 1621 |
+
)
|
| 1622 |
+
|
| 1623 |
+
x_out = model.from_patching(x_unembed)
|
| 1624 |
+
|
| 1625 |
+
# Pixel shuffle to (batch, in_channels, lat, lon)
|
| 1626 |
+
x_out = F.pixel_shuffle(x_out, model.patch_size_px[0])
|
| 1627 |
+
|
| 1628 |
+
if model.residual == "temporal":
|
| 1629 |
+
x_out = model.output_scalers * x_out + x_hat
|
| 1630 |
+
elif model.residual == "climate":
|
| 1631 |
+
x_out = model.output_scalers * x_out + batch["climate"]
|
| 1632 |
+
elif model.residual == "none":
|
| 1633 |
+
x_out = model.output_scalers * x_out + model.input_scalers_mu.reshape(
|
| 1634 |
+
1, -1, 1, 1
|
| 1635 |
+
)
|
| 1636 |
+
|
| 1637 |
+
return x_out
|
scripts/fake_data.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Prithvi WxC 输入两个时刻、预测一个时刻,并额外接收 4 通道静态场。
|
| 9 |
+
def get_dims(cfg_model, cfg_data):
|
| 10 |
+
H, W = int(cfg_model.n_lats_px), int(cfg_model.n_lons_px)
|
| 11 |
+
if tuple(map(int, cfg_data.dataset.img_size)) != (H, W):
|
| 12 |
+
raise ValueError("model grid and datapipe.dataset.img_size must match")
|
| 13 |
+
input_steps, output_steps = int(cfg_model.input_size_time), 1
|
| 14 |
+
samples = max(int(cfg_data.dataloader.batch_size), 2)
|
| 15 |
+
T = input_steps + output_steps + samples - 1
|
| 16 |
+
return {
|
| 17 |
+
"T": T, "H": H, "W": W, "time_step": 6,
|
| 18 |
+
"input_steps": input_steps, "output_steps": output_steps,
|
| 19 |
+
"static_channels": int(cfg_model.in_channels_static),
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def generate_fake_h5(data_dir, var_names, years, dims):
|
| 24 |
+
"""
|
| 25 |
+
为每个年份生成一个空 h5 文件。
|
| 26 |
+
利用 HDF5 chunked 数据集未写入 chunk 即返回 fill_value=0 的特性,
|
| 27 |
+
文件实际只含元数据,极小,但 shape 与真实数据完全一致。
|
| 28 |
+
均值/标准差也作为数据集内嵌进每年的 h5,与 era5.py 新版读取方式对应。
|
| 29 |
+
"""
|
| 30 |
+
os.makedirs(os.path.join(data_dir, "data"), exist_ok=True)
|
| 31 |
+
T, C = dims["T"], len(var_names)
|
| 32 |
+
H, W = dims["H"], dims["W"]
|
| 33 |
+
|
| 34 |
+
means = np.zeros((1, C, 1, 1), dtype=np.float32)
|
| 35 |
+
stds = np.ones((1, C, 1, 1), dtype=np.float32)
|
| 36 |
+
|
| 37 |
+
for year in years:
|
| 38 |
+
path = os.path.join(data_dir, "data", f"{year}.h5")
|
| 39 |
+
with h5py.File(path, "w") as f:
|
| 40 |
+
ds = f.create_dataset(
|
| 41 |
+
"fields",
|
| 42 |
+
shape=(T, C, H, W),
|
| 43 |
+
dtype="float32",
|
| 44 |
+
chunks=(1, C, H, W),
|
| 45 |
+
fillvalue=0.0,
|
| 46 |
+
)
|
| 47 |
+
ds.attrs["variables"] = var_names
|
| 48 |
+
ds.attrs["time_step"] = dims["time_step"]
|
| 49 |
+
f.create_dataset("global_means", data=means)
|
| 50 |
+
f.create_dataset("global_stds", data=stds)
|
| 51 |
+
|
| 52 |
+
size_kb = os.path.getsize(path) / 1024
|
| 53 |
+
print(f" {year}.h5 shape=({T},{C},{H},{W}) "
|
| 54 |
+
f"logical={T*C*H*W*4/1024**3:.1f}GB actual={size_kb:.1f}KB")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def get_static(data_dir, H, W, channels):
|
| 58 |
+
os.makedirs(data_dir, exist_ok=True)
|
| 59 |
+
lat = np.linspace(90, -90, H, dtype=np.float32)
|
| 60 |
+
lon = np.linspace(0, 360 - 360 / W, W, dtype=np.float32)
|
| 61 |
+
lat_grid = np.broadcast_to(lat[:, None], (H, W)) / 90.0
|
| 62 |
+
lon_grid = np.broadcast_to(lon[None, :], (H, W)) / 180.0 - 1.0
|
| 63 |
+
land_mask = (np.sin(np.deg2rad(lat_grid * 90)) > 0).astype(np.float32)
|
| 64 |
+
topography = np.cos(np.deg2rad(lat_grid * 90)).astype(np.float32)
|
| 65 |
+
base = [lat_grid, lon_grid, land_mask, topography]
|
| 66 |
+
static = np.stack((base * ((channels + 3) // 4))[:channels]).astype(np.float32)
|
| 67 |
+
|
| 68 |
+
ds = xr.Dataset(
|
| 69 |
+
data_vars={
|
| 70 |
+
"z": (("valid_time", "latitude", "longitude"), static[-1:]),
|
| 71 |
+
"lsm": (("valid_time", "latitude", "longitude"), static[min(2, channels - 1):min(2, channels - 1) + 1]),
|
| 72 |
+
},
|
| 73 |
+
coords={
|
| 74 |
+
"valid_time": ["2015-12-31"],
|
| 75 |
+
"latitude": lat.astype(np.float64),
|
| 76 |
+
"longitude": lon.astype(np.float64),
|
| 77 |
+
"number": 0,
|
| 78 |
+
"expver": "",
|
| 79 |
+
},
|
| 80 |
+
attrs={
|
| 81 |
+
"GRIB_centre": "ecmf",
|
| 82 |
+
"GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts",
|
| 83 |
+
"GRIB_subCentre": "0",
|
| 84 |
+
"Conventions": "CF-1.7",
|
| 85 |
+
"institution": "European Centre for Medium-Range Weather Forecasts",
|
| 86 |
+
"history": "Generated manually",
|
| 87 |
+
}
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
ds[["z"]].to_netcdf(f"{data_dir}/geopotential.nc")
|
| 91 |
+
ds[["lsm"]].to_netcdf(f"{data_dir}/land_sea_mask.nc")
|
| 92 |
+
np.save(f'{data_dir}/static.npy', static)
|
| 93 |
+
np.save(f'{data_dir}/land_mask.npy', land_mask)
|
| 94 |
+
np.save(f'{data_dir}/soil_type.npy', np.zeros((H, W), dtype=np.float32))
|
| 95 |
+
np.save(f'{data_dir}/topography.npy', topography)
|
| 96 |
+
print(f"✅ Static data: {static.shape}, dtype: {static.dtype}, save to {data_dir}")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
if __name__ == "__main__":
|
| 100 |
+
cfg_model = YParams("conf/config.yaml", "model")
|
| 101 |
+
cfg_datapipe = YParams("conf/config.yaml", "datapipe")
|
| 102 |
+
|
| 103 |
+
if cfg_datapipe.dataset.data_dir.startswith("/public/") or cfg_datapipe.dataset.data_dir.startswith("/work2/"):
|
| 104 |
+
print("请检查 config,确保各 *_dir 指向本地测试路径而非生产路径。")
|
| 105 |
+
exit()
|
| 106 |
+
|
| 107 |
+
years = cfg_datapipe.dataset.train_time + cfg_datapipe.dataset.val_time + cfg_datapipe.dataset.test_time
|
| 108 |
+
atm_vars = cfg_datapipe.dataset.channels
|
| 109 |
+
if len(atm_vars) != int(cfg_model.in_channels):
|
| 110 |
+
raise ValueError("channel count must match model.in_channels")
|
| 111 |
+
|
| 112 |
+
dims = get_dims(cfg_model, cfg_datapipe)
|
| 113 |
+
generate_fake_h5(cfg_datapipe.dataset.data_dir, atm_vars, years, dims)
|
| 114 |
+
|
| 115 |
+
static_dir = os.path.join(cfg_datapipe.dataset.data_dir, "static")
|
| 116 |
+
get_static(static_dir, dims["H"], dims["W"], dims["static_channels"])
|
| 117 |
+
|
| 118 |
+
print("\n✅ Fake datasets generated.")
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 glob
|
| 10 |
+
import numpy as np
|
| 11 |
+
import h5py
|
| 12 |
+
from tqdm import tqdm
|
| 13 |
+
from model.prithvi_wxc import PrithviWxC
|
| 14 |
+
from onescience.utils.YParams import YParams
|
| 15 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_stats(data_dir, channels):
|
| 19 |
+
"""从新版 h5 中读取变量列表与归一化参数(均值/标准差)"""
|
| 20 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 21 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 22 |
+
ds = f["fields"]
|
| 23 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 24 |
+
mu = f["global_means"][:] # [1, C, 1, 1]
|
| 25 |
+
std = f["global_stds"][:]
|
| 26 |
+
|
| 27 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 28 |
+
means = mu[:, channel_indices, :, :]
|
| 29 |
+
stds = std[:, channel_indices, :, :]
|
| 30 |
+
return means, stds
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
current_path = os.getcwd()
|
| 35 |
+
sys.path.append(current_path)
|
| 36 |
+
|
| 37 |
+
## Model config init
|
| 38 |
+
config_file_path = os.path.join(current_path, "conf/config.yaml")
|
| 39 |
+
cfg = YParams(config_file_path, "model")
|
| 40 |
+
|
| 41 |
+
## DataLoader init
|
| 42 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 43 |
+
means, stds = get_stats(cfg_data.dataset.data_dir, cfg_data.dataset.channels)
|
| 44 |
+
|
| 45 |
+
cfg['N_in_channels'] = len(cfg_data.dataset.channels)
|
| 46 |
+
cfg['N_out_channels'] = len(cfg_data.dataset.channels)
|
| 47 |
+
|
| 48 |
+
datapipe = ERA5Datapipe(
|
| 49 |
+
dataset_dir=cfg_data.dataset.data_dir,
|
| 50 |
+
used_variables=cfg_data.dataset.channels,
|
| 51 |
+
used_years=cfg_data.dataset.test_time,
|
| 52 |
+
distributed=False,
|
| 53 |
+
input_steps=2,
|
| 54 |
+
batch_size=1,
|
| 55 |
+
num_workers=4,
|
| 56 |
+
)
|
| 57 |
+
test_dataloader, _ = datapipe.get_dataloader("test")
|
| 58 |
+
|
| 59 |
+
device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
| 60 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
|
| 61 |
+
model = PrithviWxC(
|
| 62 |
+
in_channels=cfg['N_in_channels'],
|
| 63 |
+
input_size_time=cfg.input_size_time,
|
| 64 |
+
in_channels_static=cfg.in_channels_static,
|
| 65 |
+
n_lats_px=cfg.n_lats_px,
|
| 66 |
+
n_lons_px=cfg.n_lons_px,
|
| 67 |
+
patch_size_px=cfg.patch_size_px,
|
| 68 |
+
mask_unit_size_px=cfg.mask_unit_size_px,
|
| 69 |
+
mask_ratio_inputs=0.0,
|
| 70 |
+
embed_dim=cfg.embed_dim,
|
| 71 |
+
n_blocks_encoder=cfg.n_blocks_encoder,
|
| 72 |
+
n_blocks_decoder=cfg.n_blocks_decoder,
|
| 73 |
+
mlp_multiplier=cfg.mlp_multiplier,
|
| 74 |
+
n_heads=cfg.n_heads,
|
| 75 |
+
dropout=cfg.dropout,
|
| 76 |
+
drop_path=cfg.drop_path,
|
| 77 |
+
parameter_dropout=cfg.parameter_dropout,
|
| 78 |
+
residual=cfg.residual,
|
| 79 |
+
masking_mode=cfg.masking_mode,
|
| 80 |
+
positional_encoding=cfg.positional_encoding,
|
| 81 |
+
encoder_shifting=cfg.encoder_shifting,
|
| 82 |
+
decoder_shifting=cfg.decoder_shifting,
|
| 83 |
+
).to(device)
|
| 84 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 85 |
+
|
| 86 |
+
model.eval()
|
| 87 |
+
os.makedirs('result/output/', exist_ok=True)
|
| 88 |
+
print(f"📂 infer results will be generated to './result/output/'")
|
| 89 |
+
H, W = int(cfg.n_lats_px), int(cfg.n_lons_px)
|
| 90 |
+
static_path = os.path.join(cfg_data.dataset.data_dir, "static", "static.npy")
|
| 91 |
+
static_base = torch.from_numpy(np.load(static_path)).to(device=device, dtype=torch.float32).unsqueeze(0)
|
| 92 |
+
expected_static = (1, int(cfg.in_channels_static), H, W)
|
| 93 |
+
if tuple(static_base.shape) != expected_static:
|
| 94 |
+
raise ValueError(f"static data shape {tuple(static_base.shape)} != expected {expected_static}")
|
| 95 |
+
with torch.no_grad():
|
| 96 |
+
for data in tqdm(test_dataloader, desc="Inferring testset", unit="batch"):
|
| 97 |
+
invar = data[0].to(device, dtype=torch.float32) # [1, 2, C, H, W]
|
| 98 |
+
filename = data[4][-1][0]
|
| 99 |
+
B = invar.shape[0]
|
| 100 |
+
static = static_base.expand(B, -1, -1, -1)
|
| 101 |
+
lead_time = torch.full((B,), 6.0, device=device)
|
| 102 |
+
pred_var = model(invar, static, lead_time=lead_time).cpu().numpy()
|
| 103 |
+
pred_var = pred_var * stds + means
|
| 104 |
+
np.save(f"result/output/{filename}.npy", pred_var)
|
scripts/result.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib
|
| 3 |
+
matplotlib.use('Agg')
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
import glob
|
| 8 |
+
import h5py
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from tqdm import tqdm
|
| 11 |
+
from onescience.utils.YParams import YParams
|
| 12 |
+
from matplotlib import rcParams
|
| 13 |
+
|
| 14 |
+
# rcParams['font.family'] = 'serif'
|
| 15 |
+
# rcParams['font.serif'] = ['DejaVu Serif']
|
| 16 |
+
rcParams['mathtext.fontset'] = 'stix'
|
| 17 |
+
rcParams['axes.linewidth'] = 0.9
|
| 18 |
+
rcParams['xtick.major.width'] = 0.9
|
| 19 |
+
rcParams['ytick.major.width'] = 0.9
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_metadata(data_dir, channels):
|
| 23 |
+
"""从新版 h5 attrs 中读取变量列表和 time_step"""
|
| 24 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 25 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 26 |
+
ds = f["fields"]
|
| 27 |
+
all_variables = [v.decode() if isinstance(v, bytes) else v for v in ds.attrs["variables"]]
|
| 28 |
+
time_step = int(ds.attrs["time_step"])
|
| 29 |
+
|
| 30 |
+
channel_indices = [all_variables.index(v) for v in channels]
|
| 31 |
+
|
| 32 |
+
total_files = [f for f in os.listdir('./result/output/') if f.endswith('.npy')]
|
| 33 |
+
total_files.sort()
|
| 34 |
+
return total_files, channel_indices, time_step
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def filename_to_index(filename, time_step):
|
| 38 |
+
"""将 YYYYMMDDHH 格式的文件名转换为年度 h5 文件中的时间步索引"""
|
| 39 |
+
dt = datetime.strptime(filename, "%Y%m%d%H")
|
| 40 |
+
year_start = datetime(dt.year, 1, 1)
|
| 41 |
+
hours = (dt - year_start).total_seconds() / 3600
|
| 42 |
+
return int(hours / time_step)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_result(total_files, channel_indices, time_step, data_dir, clim_mean):
|
| 46 |
+
channel_rmse = np.zeros(len(channel_indices))
|
| 47 |
+
channel_acc = np.zeros(len(channel_indices))
|
| 48 |
+
clim_mean = clim_mean[0, :, :, :]
|
| 49 |
+
if not os.path.exists('./result/rmse.npy') or not os.path.exists('result/acc.npy'):
|
| 50 |
+
numerator = np.zeros(len(channel_indices))
|
| 51 |
+
pred_sq_sum = np.zeros(len(channel_indices))
|
| 52 |
+
label_sq_sum = np.zeros(len(channel_indices))
|
| 53 |
+
for file in tqdm(total_files, unit="files"):
|
| 54 |
+
fname = file[:-4] # 去掉 .npy
|
| 55 |
+
year = fname[:4]
|
| 56 |
+
t_idx = filename_to_index(fname, time_step)
|
| 57 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 58 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 59 |
+
label = label[channel_indices]
|
| 60 |
+
pred = np.load(f'result/output/{file}').squeeze()
|
| 61 |
+
|
| 62 |
+
label_anom = label - clim_mean
|
| 63 |
+
pred_anom = pred - clim_mean
|
| 64 |
+
# 累加
|
| 65 |
+
numerator += np.sum(pred_anom * label_anom, axis=(1, 2))
|
| 66 |
+
pred_sq_sum += np.sum(pred_anom ** 2, axis=(1, 2))
|
| 67 |
+
label_sq_sum += np.sum(label_anom ** 2, axis=(1, 2))
|
| 68 |
+
|
| 69 |
+
channel_rmse += np.sqrt(np.mean((label - pred) ** 2, axis=(1, 2)))
|
| 70 |
+
channel_rmse /= len(total_files)
|
| 71 |
+
channel_acc = numerator / (np.sqrt(pred_sq_sum * label_sq_sum) + 1e-8)
|
| 72 |
+
np.save('./result/acc.npy', channel_acc)
|
| 73 |
+
np.save('./result/rmse.npy', channel_rmse)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def show_result():
|
| 77 |
+
channel_rmse = np.load('./result/rmse.npy')
|
| 78 |
+
channel_acc = np.load('./result/acc.npy')
|
| 79 |
+
|
| 80 |
+
channels = [cfg_data.dataset.channels[i] for i in range(len(channel_indices))]
|
| 81 |
+
w = 24 # 最长 channel 名宽度
|
| 82 |
+
|
| 83 |
+
# 表头
|
| 84 |
+
print(f"┌{'─' * (w + 2)}┬{'─' * 14}┬{'─' * 14}┐")
|
| 85 |
+
print(f"│ {'Channel':<{w}} │ {'RMSE':>12} │ {'ACC':>12} │")
|
| 86 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 87 |
+
# 数据行
|
| 88 |
+
for i, ch in enumerate(channels):
|
| 89 |
+
print(f"│ {ch:<{w}} │ {channel_rmse[i]:>12.4f} | {channel_acc[i]:>12.4f} |")
|
| 90 |
+
print(f"├{'─' * (w + 2)}┼{'─' * 14}┼{'─' * 14}┤")
|
| 91 |
+
print(f"│ {'Average':<{w}} │ {np.mean(channel_rmse):>12.4f} │ {np.mean(channel_acc):>12.4f} │")
|
| 92 |
+
print(f"└{'─' * (w + 2)}┴{'─' * 14}┴{'─' * 14}┘")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def plot(label, pred, var, filename):
|
| 96 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
| 97 |
+
|
| 98 |
+
xtick_labels = ['180°W', '90°W', '0°', '90°E', '180°E']
|
| 99 |
+
ytick_labels = ['90°S', '45°S', '0°', '45°N', '90°N']
|
| 100 |
+
xticks = np.linspace(0, label.shape[-1] - 1, 5)
|
| 101 |
+
yticks = np.linspace(0, label.shape[-2] - 1, 5)
|
| 102 |
+
|
| 103 |
+
vmin = min(label.min(), pred.min())
|
| 104 |
+
vmax = max(label.max(), pred.max())
|
| 105 |
+
|
| 106 |
+
diff = label - pred
|
| 107 |
+
rmse = np.sqrt(np.mean(diff ** 2))
|
| 108 |
+
diff_abs_max = np.abs(diff).max()
|
| 109 |
+
|
| 110 |
+
plot_configs = [
|
| 111 |
+
{'data': label, 'title': 'Truth', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 112 |
+
{'data': pred, 'title': 'Prediction', 'cmap': 'viridis', 'vmin': vmin, 'vmax': vmax},
|
| 113 |
+
{'data': diff, 'title': f'Difference (RMSE={rmse:.2f})', 'cmap': 'RdBu_r', 'vmin': -diff_abs_max, 'vmax': diff_abs_max},
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
for ax, cfg in zip(axes, plot_configs):
|
| 117 |
+
im = ax.imshow(cfg['data'], cmap=cfg['cmap'], vmin=cfg['vmin'], vmax=cfg['vmax'])
|
| 118 |
+
ax.set_title(cfg['title'], fontsize=12, pad=4)
|
| 119 |
+
ax.set_xlabel('Longitude')
|
| 120 |
+
ax.set_ylabel('Latitude')
|
| 121 |
+
ax.set_xticks(xticks)
|
| 122 |
+
ax.set_xticklabels(xtick_labels)
|
| 123 |
+
ax.set_yticks(yticks)
|
| 124 |
+
ax.set_yticklabels(ytick_labels)
|
| 125 |
+
plt.colorbar(im, ax=ax, orientation='horizontal')
|
| 126 |
+
|
| 127 |
+
fig.suptitle(var, fontsize=14, fontweight='bold', y=0.98)
|
| 128 |
+
plt.savefig(filename, dpi=300, bbox_inches='tight')
|
| 129 |
+
plt.close()
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def plot_loss(train_loss, valid_loss):
|
| 133 |
+
mask = ~(np.isnan(train_loss) | np.isnan(valid_loss))
|
| 134 |
+
train_loss = train_loss[mask]
|
| 135 |
+
valid_loss = valid_loss[mask]
|
| 136 |
+
|
| 137 |
+
fig, ax = plt.subplots(figsize=(5, 3.5))
|
| 138 |
+
colors = {'train': '#2563EB', 'valid': '#EA580C'}
|
| 139 |
+
epochs = np.arange(1, len(train_loss) + 1)
|
| 140 |
+
|
| 141 |
+
ax.plot(epochs, train_loss, color=colors['train'], linewidth=1.5, label='Train')
|
| 142 |
+
ax.plot(epochs, valid_loss, color=colors['valid'], linewidth=1.5, label='Valid', linestyle='--')
|
| 143 |
+
min_idx = np.argmin(valid_loss)
|
| 144 |
+
ax.scatter(epochs[min_idx], valid_loss[min_idx],
|
| 145 |
+
color=colors['valid'], s=40, zorder=5, edgecolors='white')
|
| 146 |
+
ax.annotate(f'Best: {valid_loss[min_idx]:.3f}',
|
| 147 |
+
xy=(epochs[min_idx], valid_loss[min_idx]),
|
| 148 |
+
xytext=(10, 10), textcoords='offset points', fontsize=8, color=colors['valid'],
|
| 149 |
+
arrowprops=dict(arrowstyle='-', color=colors['valid'], lw=0.5))
|
| 150 |
+
|
| 151 |
+
ax.set(xlabel='Epoch', ylabel='Loss', xlim=(0, len(train_loss) + 1))
|
| 152 |
+
ax.legend(frameon=False, loc='upper right')
|
| 153 |
+
ax.grid(True, linestyle='--', alpha=0.3)
|
| 154 |
+
ax.spines[['top', 'right']].set_visible(False)
|
| 155 |
+
|
| 156 |
+
plt.tight_layout()
|
| 157 |
+
plt.savefig('./result/loss.png', dpi=300, bbox_inches='tight')
|
| 158 |
+
plt.close()
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
current_path = os.getcwd()
|
| 163 |
+
sys.path.append(current_path)
|
| 164 |
+
config_file_path = os.path.join(current_path, 'conf/config.yaml')
|
| 165 |
+
cfg = YParams(config_file_path, 'model')
|
| 166 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 167 |
+
|
| 168 |
+
train_loss = np.load('./data/checkpoints/trloss.npy')
|
| 169 |
+
valid_loss = np.load('./data/checkpoints/valoss.npy')
|
| 170 |
+
plot_loss(train_loss, valid_loss)
|
| 171 |
+
|
| 172 |
+
data_dir = cfg_data.dataset.data_dir
|
| 173 |
+
total_files, channel_indices, time_step = get_metadata(data_dir, cfg_data.dataset.channels)
|
| 174 |
+
|
| 175 |
+
# Load data & Compute RMSE/ACC per channel
|
| 176 |
+
h5_files = sorted(glob.glob(os.path.join(data_dir, "data", "*.h5")))
|
| 177 |
+
with h5py.File(h5_files[0], "r") as f:
|
| 178 |
+
mu = f["global_means"][:]
|
| 179 |
+
clim_mean = mu[:, channel_indices, :, :]
|
| 180 |
+
get_result(total_files, channel_indices, time_step, data_dir, clim_mean)
|
| 181 |
+
show_result()
|
| 182 |
+
|
| 183 |
+
##### 默认绘制 result/output 中第一个预测结果,用户可自行指定日期和变量 #####
|
| 184 |
+
eg_files = [total_files[0][:-4]]
|
| 185 |
+
channel_index = [cfg_data.dataset.channels.index(v) for v in ['2m_temperature', 'geopotential_500', 'temperature_850']]
|
| 186 |
+
|
| 187 |
+
selected_var = [cfg_data.dataset.channels[int(i)] for i in channel_index]
|
| 188 |
+
print(f"seleted date: {eg_files}")
|
| 189 |
+
print(f"selected channels: {selected_var}")
|
| 190 |
+
for file in eg_files:
|
| 191 |
+
year = file[:4]
|
| 192 |
+
t_idx = filename_to_index(file, time_step)
|
| 193 |
+
with h5py.File(os.path.join(data_dir, 'data', f'{year}.h5'), "r") as f:
|
| 194 |
+
label = f["fields"][t_idx] # [C, H, W]
|
| 195 |
+
label = label[channel_indices]
|
| 196 |
+
pred = np.load(f'result/output/{file}.npy').squeeze()
|
| 197 |
+
for i in range(len(selected_var)):
|
| 198 |
+
filename = f'./result/{file}_{selected_var[i]}.png'
|
| 199 |
+
plot(label[channel_index[i]], pred[channel_index[i]], selected_var[i], filename)
|
| 200 |
+
print(f'✅plot {filename}')
|
scripts/train.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 model.prithvi_wxc import PrithviWxC
|
| 15 |
+
from onescience.datapipes.climate import ERA5Datapipe
|
| 16 |
+
from onescience.utils.YParams import YParams
|
| 17 |
+
from onescience.utils.fcn.darcy_loss import LpLoss
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from apex import optimizers
|
| 21 |
+
_FUSED_ADAM = True
|
| 22 |
+
except Exception:
|
| 23 |
+
_FUSED_ADAM = False
|
| 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 and torch.cuda.is_available():
|
| 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 |
+
device = f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu"
|
| 45 |
+
|
| 46 |
+
## DataLoader init
|
| 47 |
+
cfg_data = YParams(config_file_path, "datapipe")
|
| 48 |
+
cfg['N_in_channels'] = len(cfg_data.dataset.channels)
|
| 49 |
+
cfg['N_out_channels'] = len(cfg_data.dataset.channels)
|
| 50 |
+
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=dist.is_initialized(),
|
| 55 |
+
input_steps=2,
|
| 56 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 57 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 58 |
+
)
|
| 59 |
+
train_dataloader, train_sampler = datapipe.get_dataloader("train")
|
| 60 |
+
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=dist.is_initialized(),
|
| 65 |
+
input_steps=2,
|
| 66 |
+
batch_size=cfg_data.dataloader.batch_size,
|
| 67 |
+
num_workers=cfg_data.dataloader.num_workers,
|
| 68 |
+
)
|
| 69 |
+
val_dataloader, val_sampler = datapipe.get_dataloader("valid")
|
| 70 |
+
|
| 71 |
+
# Model init
|
| 72 |
+
model = PrithviWxC(
|
| 73 |
+
in_channels=cfg['N_in_channels'],
|
| 74 |
+
input_size_time=cfg.input_size_time,
|
| 75 |
+
in_channels_static=cfg.in_channels_static,
|
| 76 |
+
n_lats_px=cfg.n_lats_px,
|
| 77 |
+
n_lons_px=cfg.n_lons_px,
|
| 78 |
+
patch_size_px=cfg.patch_size_px,
|
| 79 |
+
mask_unit_size_px=cfg.mask_unit_size_px,
|
| 80 |
+
mask_ratio_inputs=cfg.mask_ratio_inputs,
|
| 81 |
+
embed_dim=cfg.embed_dim,
|
| 82 |
+
n_blocks_encoder=cfg.n_blocks_encoder,
|
| 83 |
+
n_blocks_decoder=cfg.n_blocks_decoder,
|
| 84 |
+
mlp_multiplier=cfg.mlp_multiplier,
|
| 85 |
+
n_heads=cfg.n_heads,
|
| 86 |
+
dropout=cfg.dropout,
|
| 87 |
+
drop_path=cfg.drop_path,
|
| 88 |
+
parameter_dropout=cfg.parameter_dropout,
|
| 89 |
+
residual=cfg.residual,
|
| 90 |
+
masking_mode=cfg.masking_mode,
|
| 91 |
+
positional_encoding=cfg.positional_encoding,
|
| 92 |
+
encoder_shifting=cfg.encoder_shifting,
|
| 93 |
+
decoder_shifting=cfg.decoder_shifting,
|
| 94 |
+
).to(device)
|
| 95 |
+
|
| 96 |
+
if _FUSED_ADAM:
|
| 97 |
+
optimizer = optimizers.FusedAdam(model.parameters(), lr=cfg.lr)
|
| 98 |
+
else:
|
| 99 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=cfg.lr)
|
| 100 |
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2, patience=5, mode='min')
|
| 101 |
+
loss_obj = LpLoss()
|
| 102 |
+
|
| 103 |
+
## Train process init
|
| 104 |
+
os.makedirs(cfg.checkpoint_dir, exist_ok=True)
|
| 105 |
+
train_loss_file = f"{cfg.checkpoint_dir}/trloss.npy"
|
| 106 |
+
valid_loss_file = f"{cfg.checkpoint_dir}/valoss.npy"
|
| 107 |
+
best_valid_loss = 1.0e6
|
| 108 |
+
best_loss_epoch = 0
|
| 109 |
+
train_losses = np.empty((0,), dtype=np.float32)
|
| 110 |
+
valid_losses = np.empty((0,), dtype=np.float32)
|
| 111 |
+
|
| 112 |
+
## Get model params count
|
| 113 |
+
if cfg.world_size == 1:
|
| 114 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 115 |
+
print("\n\n")
|
| 116 |
+
print("-" * 50)
|
| 117 |
+
print(f"📂 now params is {total_params}, {total_params / 1e6:.2f}M, {total_params / 1e9:.2f}B")
|
| 118 |
+
print("-" * 50, "\n")
|
| 119 |
+
|
| 120 |
+
## Load model weight if there exist well-trained model
|
| 121 |
+
if os.path.exists(f"{cfg.checkpoint_dir}/model_bak.pth"):
|
| 122 |
+
if world_rank == 0:
|
| 123 |
+
print("\n\n")
|
| 124 |
+
print("-" * 50)
|
| 125 |
+
print(f"✅ There has a model weight, load and continue training...")
|
| 126 |
+
print(f'If you want to train a new model, ensure there is no *.pth file in {cfg.checkpoint_dir}')
|
| 127 |
+
print("-" * 50, "\n")
|
| 128 |
+
ckpt = torch.load(f"{cfg.checkpoint_dir}/model_bak.pth", map_location=device, weights_only=False)
|
| 129 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 130 |
+
optimizer.load_state_dict(ckpt["optimizer_state_dict"])
|
| 131 |
+
scheduler.load_state_dict(ckpt["scheduler_state_dict"])
|
| 132 |
+
best_valid_loss = ckpt["best_valid_loss"]
|
| 133 |
+
best_loss_epoch = ckpt["best_loss_epoch"]
|
| 134 |
+
train_losses = np.load(train_loss_file)
|
| 135 |
+
valid_losses = np.load(valid_loss_file)
|
| 136 |
+
|
| 137 |
+
## Distributed model
|
| 138 |
+
if dist.is_initialized():
|
| 139 |
+
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)
|
| 140 |
+
world_rank == 0 and logger.info(f"start training ...")
|
| 141 |
+
|
| 142 |
+
H, W = int(cfg.n_lats_px), int(cfg.n_lons_px)
|
| 143 |
+
static_path = os.path.join(cfg_data.dataset.data_dir, "static", "static.npy")
|
| 144 |
+
static_base = torch.from_numpy(np.load(static_path)).to(device=device, dtype=torch.float32).unsqueeze(0)
|
| 145 |
+
expected_static = (1, int(cfg.in_channels_static), H, W)
|
| 146 |
+
if tuple(static_base.shape) != expected_static:
|
| 147 |
+
raise ValueError(f"static data shape {tuple(static_base.shape)} != expected {expected_static}")
|
| 148 |
+
for epoch in range(cfg.max_epoch):
|
| 149 |
+
if dist.is_initialized():
|
| 150 |
+
train_sampler.set_epoch(epoch)
|
| 151 |
+
val_sampler.set_epoch(epoch)
|
| 152 |
+
model.train()
|
| 153 |
+
train_loss = 0
|
| 154 |
+
start_time = time.time()
|
| 155 |
+
for j, data in enumerate(train_dataloader):
|
| 156 |
+
invar = data[0].to(device, dtype=torch.float32) # [B, 2, C, H, W]
|
| 157 |
+
outvar = data[1].to(device, dtype=torch.float32) # [B, C, H, W]
|
| 158 |
+
B = invar.shape[0]
|
| 159 |
+
static = static_base.expand(B, -1, -1, -1)
|
| 160 |
+
lead_time = torch.full((B,), 6.0, device=device)
|
| 161 |
+
outvar_pred = model(invar, static, lead_time=lead_time)
|
| 162 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 163 |
+
optimizer.zero_grad()
|
| 164 |
+
loss.backward()
|
| 165 |
+
optimizer.step()
|
| 166 |
+
train_loss += loss.item()
|
| 167 |
+
if world_rank == 0:
|
| 168 |
+
logger.info(f'Train: Epoch {epoch}-{j+1}/{len(train_dataloader)} '
|
| 169 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 170 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 171 |
+
f'loss:{train_loss / (j+1): .04f}')
|
| 172 |
+
|
| 173 |
+
train_loss /= len(train_dataloader)
|
| 174 |
+
|
| 175 |
+
model.eval()
|
| 176 |
+
valid_loss = 0
|
| 177 |
+
with torch.no_grad():
|
| 178 |
+
start_time = time.time()
|
| 179 |
+
for j, data in enumerate(val_dataloader):
|
| 180 |
+
invar = data[0].to(device, dtype=torch.float32)
|
| 181 |
+
outvar = data[1].to(device, dtype=torch.float32)
|
| 182 |
+
B = invar.shape[0]
|
| 183 |
+
static = static_base.expand(B, -1, -1, -1)
|
| 184 |
+
lead_time = torch.full((B,), 6.0, device=device)
|
| 185 |
+
outvar_pred = model(invar, static, lead_time=lead_time)
|
| 186 |
+
loss = loss_obj(outvar, outvar_pred)
|
| 187 |
+
|
| 188 |
+
if dist.is_initialized():
|
| 189 |
+
loss_tensor = loss.detach().to(device)
|
| 190 |
+
dist.all_reduce(loss_tensor)
|
| 191 |
+
loss = loss_tensor.item() / cfg.world_size
|
| 192 |
+
valid_loss += loss
|
| 193 |
+
else:
|
| 194 |
+
valid_loss += loss.item()
|
| 195 |
+
if world_rank == 0:
|
| 196 |
+
logger.info(f'Valid: Epoch {epoch}-{j+1}/{len(val_dataloader)} '
|
| 197 |
+
f'[cost {int((time.time()-start_time) // 60):02}:{int((time.time()-start_time) % 60):02}] '
|
| 198 |
+
f'[{(time.time()-start_time)/(j+1): .02f}s/{cfg_data.dataloader.batch_size}batch] '
|
| 199 |
+
f'loss:{valid_loss / (j+1): .04f}')
|
| 200 |
+
|
| 201 |
+
valid_loss /= len(val_dataloader)
|
| 202 |
+
is_save_ckp = False
|
| 203 |
+
if valid_loss < best_valid_loss:
|
| 204 |
+
best_valid_loss = valid_loss
|
| 205 |
+
best_loss_epoch = epoch
|
| 206 |
+
world_rank == 0 and save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, cfg.checkpoint_dir)
|
| 207 |
+
is_save_ckp = True
|
| 208 |
+
scheduler.step(valid_loss)
|
| 209 |
+
|
| 210 |
+
if world_rank == 0:
|
| 211 |
+
logger.info(f"Epoch [{epoch + 1}/{cfg.max_epoch}], "
|
| 212 |
+
f"Train Loss: {train_loss:.4f}, "
|
| 213 |
+
f"Valid Loss: {valid_loss:.4f}, "
|
| 214 |
+
f"Best loss at Epoch: {best_loss_epoch + 1}"
|
| 215 |
+
+ (", saving checkpoint" if is_save_ckp else "")
|
| 216 |
+
)
|
| 217 |
+
train_losses = np.append(train_losses, train_loss)
|
| 218 |
+
valid_losses = np.append(valid_losses, valid_loss)
|
| 219 |
+
np.save(train_loss_file, train_losses)
|
| 220 |
+
np.save(valid_loss_file, valid_losses)
|
| 221 |
+
|
| 222 |
+
if epoch - best_loss_epoch > cfg.patience:
|
| 223 |
+
print(f"Loss has not decrease in {cfg.patience} epochs, stopping training...")
|
| 224 |
+
exit()
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def save_checkpoint(model, optimizer, scheduler, best_valid_loss, best_loss_epoch, model_path):
|
| 228 |
+
model_to_save = model.module if hasattr(model, "module") else model
|
| 229 |
+
state = {"model_state_dict": model_to_save.state_dict(),
|
| 230 |
+
"optimizer_state_dict": optimizer.state_dict(),
|
| 231 |
+
"scheduler_state_dict": scheduler.state_dict(),
|
| 232 |
+
"best_valid_loss": best_valid_loss,
|
| 233 |
+
"best_loss_epoch": best_loss_epoch,
|
| 234 |
+
}
|
| 235 |
+
torch.save(state, f"{model_path}/model.pth")
|
| 236 |
+
### the weight file saving may interrupted due to DCU queue limit, get a backup to ensure there at least has one model
|
| 237 |
+
os.system(f"mv {model_path}/model.pth {model_path}/model_bak.pth")
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
if __name__ == "__main__":
|
| 241 |
+
current_path = os.getcwd()
|
| 242 |
+
sys.path.append(current_path)
|
| 243 |
+
main()
|
weight/.gitkeep
ADDED
|
File without changes
|