zhangrenchao commited on
Commit
efe4fbe
·
verified ·
1 Parent(s): 36e4eb5

Publish MetNet-2 reproduction

Browse files
.gitattributes CHANGED
@@ -1,35 +1,3 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
  *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.pt filter=lfs diff=lfs merge=lfs -text
2
+ *.npz filter=lfs diff=lfs merge=lfs -text
3
+ *.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
README.md ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - earth-science
8
+ - precipitation-forecasting
9
+ - MetNet-2
10
+ frameworks: PyTorch
11
+ ---
12
+
13
+ <p align="center">
14
+ <strong><span style="font-size: 30px;">MetNet-2</span></strong>
15
+ </p>
16
+
17
+ # Model Introduction
18
+
19
+ MetNet-2 generates high-resolution probabilistic precipitation forecasts up to 12 hours ahead from radar, satellite, and atmospheric-state inputs for short-range forecasting and extreme-precipitation risk analysis.
20
+
21
+ Paper: Deep learning for twelve hour precipitation forecasts
22
+ https://doi.org/10.1038/s41467-022-32483-x
23
+
24
+ # Model Description
25
+
26
+ The method was proposed by the Google Research team. The paper constructs its 2017-2020 training and test data from MRMS radar, GOES satellite imagery, and HRRR assimilated atmospheric states. The model predicts grid-cell precipitation probability distributions at two-minute intervals up to 12 hours ahead.
27
+
28
+ # Use Cases
29
+
30
+ | Use Case | Description |
31
+ | :---: | :--- |
32
+ | Categorical precipitation forecasting | Validate the 512-category conditional distribution and 12-hour lead-time protocol. |
33
+ | Core-method validation | Validate ConvLSTM, lead-time FiLM, and multiscale dilated residual stacks. |
34
+ | Local engineering validation | Exercise the complete logical `641×512×512` contract through a deterministic procedural field without materializing the full input. |
35
+ | ModelScope/OneCode execution | Validate structured data, training, inference, probabilistic precipitation metrics, and visualization in ModelScope or OneCode environments. |
36
+ | Multi-GPU training | Validate distributed training and the checkpoint workflow through `torchrun`. |
37
+
38
+ # Usage Instructions
39
+
40
+ ## 1.OneCode
41
+
42
+ Experience intelligent, one-click AI4S programming through the OneCode online environment:
43
+
44
+ [Try intelligent, one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
45
+
46
+ ## 2. Download and Installation
47
+
48
+ ```bash
49
+ hf download OneScience-Group/MetNet-2 --local-dir ./MetNet-2
50
+ cd MetNet-2
51
+ ```
52
+
53
+ ### Environment Dependencies
54
+
55
+ **Hardware Requirements**
56
+
57
+ - A GPU or DCU is recommended.
58
+ - A CPU can be used for connectivity validation with the default small-sample configuration.
59
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version matching the current cluster, is recommended.
60
+
61
+ **DCU Environment**
62
+
63
+ ```bash
64
+ # Activate DTK and Conda first
65
+ conda create -n onescience311 python=3.11 -y
66
+ conda activate onescience311
67
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
68
+ ```
69
+
70
+ **GPU Environment**
71
+
72
+ ```bash
73
+ # Activate Conda first
74
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
75
+ conda activate onescience311
76
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
77
+ ```
78
+
79
+ ### Training Data
80
+
81
+ The paper training inputs combine MRMS, HRRR, GOES, static geography, and temporal information under a 641-channel, `512×512` spatial-domain protocol. This repository creates eight deterministic window records and constructs only the selected `32×32` windows plus their halos at runtime while retaining the complete logical shape and channel grouping. The synthetic data validate engineering connectivity only and do not represent real meteorological distributions, paper-scale training, or paper performance.
82
+
83
+ ```bash
84
+ python scripts/fake_data.py
85
+ ```
86
+
87
+ ### Training
88
+
89
+ For single-GPU training, use:
90
+
91
+ ```bash
92
+ python scripts/train.py
93
+ ```
94
+
95
+ For multi-GPU training, use:
96
+
97
+ ```bash
98
+ 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
99
+ ```
100
+
101
+ The default reduces sample count, network width, residual blocks, and training steps without reducing the 641 channels, 512 precipitation categories, or 12-hour lead protocol. Formal experiments require real MRMS, GOES, and HRRR data and full computing resources, with artifacts saved to:
102
+
103
+ ```text
104
+ result/checkpoints/metnet_2.pt
105
+ result/training/metrics.json
106
+ ```
107
+
108
+ ### Trained Weights
109
+
110
+ The paper does not provide directly loadable official model weights, and this repository bundles no weights under `weight/`. The locally trained checkpoint is saved to `result/checkpoints/metnet_2.pt` and must not be represented as an official pretrained weight.
111
+
112
+ ### Inference
113
+
114
+ ```bash
115
+ python scripts/inference.py
116
+ ```
117
+
118
+ Inference loads the training checkpoint and generates 512-category precipitation probabilities and their CDF for selected spatial windows while preserving coverage and completeness metadata. Complete numerical results are saved to:
119
+
120
+ ```text
121
+ result/output/predictions.npz
122
+ ```
123
+
124
+ ### Evaluation and Visualization
125
+
126
+ ```bash
127
+ python scripts/result.py
128
+ ```
129
+
130
+ Evaluation computes discrete CRPS, Brier Score, and CSI at multiple precipitation thresholds and generates target, expected-rate, and error comparisons. Synthetic-data results validate engineering only and do not represent paper performance; outputs are saved to:
131
+
132
+ ```text
133
+ result/evaluation/metrics.json
134
+ result/evaluation/comparison.png
135
+ ```
136
+
137
+ # Official OneScience Information
138
+
139
+ | Platform | OneScience Main Repository | Skills Repository |
140
+ | --- | --- | --- |
141
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
142
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
143
+
144
+ # Citation and License
145
+
146
+ This repository is an independent engineering reproduction of the public MetNet-2 specifications.
147
+
148
+ Use of this repository's code, official model weights, and data remains subject to the licenses and terms of their respective projects.
README_zh.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - zh
5
+ - en
6
+ tags:
7
+ - OneScience
8
+ - 地球科学
9
+ - 降水预报
10
+ - MetNet-2
11
+ frameworks: PyTorch
12
+ ---
13
+
14
+ <p align="center">
15
+ <strong><span style="font-size: 30px;">MetNet-2</span></strong>
16
+ </p>
17
+
18
+ # 模型介绍
19
+
20
+ MetNet-2 根据雷达、卫星和大气状态生成未来 12 小时的高分辨率概率降水预报,主要用于短时降水预测和极端降水风险分析。
21
+
22
+ 论文:Deep learning for twelve hour precipitation forecasts
23
+ https://doi.org/10.1038/s41467-022-32483-x
24
+
25
+ # 模型描述
26
+
27
+ 该方法由 Google Research 团队提出。论文使用 MRMS 雷达、GOES 卫星和 HRRR 同化大气状态构建 2017-2020 年训练与测试数据。模型适用于以 2 分钟间隔预测未来 12 小时的逐网格降水概率分布。
28
+
29
+ # 适用场景
30
+
31
+ | 场景 | 说明 |
32
+ | :---: | :--- |
33
+ | 降水分类预报 | 验证 512 类条件降水分布及 12 小时时效协议。 |
34
+ | 核心方法验证 | 验证 ConvLSTM、lead-time FiLM 和多尺度膨胀残差栈。 |
35
+ | 本地工程验证 | 使用确定性 procedural field 验证完整 `641×512×512` 逻辑协议,不物化完整输入。 |
36
+ | ModelScope/OneCode 运行 | 在 ModelScope 或 OneCode 环境中验证结构化数据、训练、推理、概率降水指标和可视化流程。 |
37
+ | 多卡训练 | 通过 `torchrun` 验证分布式训练和 checkpoint 流程。 |
38
+
39
+ # 使用说明
40
+
41
+ ## 1.OneCode
42
+
43
+ 可通过 OneCode 在线环境体验智能化一键式 AI4S 编程:
44
+
45
+ [点击体验智能化一键式 AI4S 编程](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
46
+
47
+ ## 2.下载安装
48
+
49
+ ```bash
50
+ modelscope download --model OneScience/MetNet-2 --local_dir ./MetNet-2
51
+ cd MetNet-2
52
+ ```
53
+
54
+ ### 环境依赖
55
+
56
+ **硬件要求**
57
+
58
+ - 推荐使用 GPU 或 DCU 运行。
59
+ - CPU 可用于默认小样本配置的连通性验证。
60
+ - DCU 用户需预先安装 DTK,建议使用 DTK 25.04.2 以上版本或与当前集群匹配的 OneScience 推荐版本。
61
+
62
+ **DCU环境**
63
+
64
+ ```bash
65
+ # 请首先激活 DTK 及 Conda
66
+ conda create -n onescience311 python=3.11 -y
67
+ conda activate onescience311
68
+ pip install onescience[earth-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
69
+ ```
70
+
71
+ **GPU环境**
72
+
73
+ ```bash
74
+ # 请首先激活 Conda
75
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
76
+ conda activate onescience311
77
+ pip install onescience[earth-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
78
+ ```
79
+
80
+ ### 训练数据介绍
81
+
82
+ 论文训练数据由 MRMS、HRRR、GOES、静态地理和时间信息组成,并遵循 641 通道、`512×512` 空间域协议。本仓库生成 8 条确定性窗口记录,运行时仅构造选定的 `32×32` 窗口及 halo,同时保留完整逻辑形状和通道分组。合成数据只验证工程连通性,不代表真实气象数据分布、论文训练规模或论文性能。
83
+
84
+ ```bash
85
+ python scripts/fake_data.py
86
+ ```
87
+
88
+ ### 训练
89
+
90
+ 单卡训练可使用:
91
+
92
+ ```bash
93
+ python scripts/train.py
94
+ ```
95
+
96
+ 多卡训练可使用:
97
+
98
+ ```bash
99
+ 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
100
+ ```
101
+
102
+ 默认配置仅缩小样本数、网络宽度、残差块数量和训练步数,不缩小 641 通道、512 个降水类别和 12 小时时效协议。正式实验需要真实 MRMS、GOES 和 HRRR 数据及完整计算资源,训练产物保存到:
103
+
104
+ ```text
105
+ result/checkpoints/metnet_2.pt
106
+ result/training/metrics.json
107
+ ```
108
+
109
+ ### 训练权重
110
+
111
+ 论文未提供可直接加载的官方模型权重,本仓库不在 `weight/` 中内置权重。本地训练生成的 checkpoint 保存到 `result/checkpoints/metnet_2.pt`,不得描述为官方预训练权重。
112
+
113
+ ### 推理
114
+
115
+ ```bash
116
+ python scripts/inference.py
117
+ ```
118
+
119
+ 推理加载训练 checkpoint,为选定空间窗口生成 512 类降水概率及对应 CDF,并明确保存覆盖范围和完整性标记。完整数值结果保存到:
120
+
121
+ ```text
122
+ result/output/predictions.npz
123
+ ```
124
+
125
+ ### 评估和可视化
126
+
127
+ ```bash
128
+ python scripts/result.py
129
+ ```
130
+
131
+ 评估计算离散 CRPS、Brier Score 和不同降水阈值下的 CSI,并生成目标、期望降水率和误差对比图。虚拟数据结果仅用于验证工程流程,不代表论文正式性能;结果保存到:
132
+
133
+ ```text
134
+ result/evaluation/metrics.json
135
+ result/evaluation/comparison.png
136
+ ```
137
+
138
+ # OneScience官方信息
139
+
140
+ | 平台 | OneScience 主仓库 | Skills 仓库 |
141
+ | --- | --- | --- |
142
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
143
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
144
+
145
+ # 引用与许可证
146
+
147
+ 本仓库为 MetNet-2 公开规格的独立工程复现版本。
148
+
149
+ 本仓库代码、官方模型权重和数据的使用仍应以各自项目中的许可证及使用条款为准。
conf/config.yaml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 2026
2
+ data:
3
+ path: data/metnet2_fake.npz
4
+ format_version: metnet2_selected_windows_v1
5
+ logical_shape: [641, 512, 512]
6
+ channel_groups: {mrms_radar_history: 33, hrrr_atmosphere_history: 484, goes_satellite_history: 96, static_geography: 24, time_coordinates: 4}
7
+ window: 32
8
+ halo: 8
9
+ samples: 8
10
+ model:
11
+ input_channels: 641
12
+ classes: 512
13
+ width: 8
14
+ stacks: 1
15
+ dilations: [1, 2, 4, 8, 16, 32, 64, 128]
16
+ lead_max_minutes: 720
17
+ training:
18
+ batch_size: 1
19
+ steps: 1
20
+ learning_rate: 0.001
21
+ class_chunk: 64
22
+ runtime:
23
+ device: auto
24
+ num_threads: 2
25
+ inference:
26
+ lead_minutes: 2
27
+ coverage: selected 32x32 window
28
+ is_complete: false
29
+ paper_model:
30
+ input_channels: 641
31
+ classes: 512
32
+ stacks: 3
33
+ dilations: [1, 2, 4, 8, 16, 32, 64, 128]
34
+ lead_times_minutes: {start: 2, stop: 720, step: 2}
35
+ spatial_upscale: 4
36
+ methods: [ConvLSTM, lead-time FiLM, dilated residual stacks, center crop, bilinear upsampling]
37
+ paths:
38
+ checkpoint: result/checkpoints/metnet_2.pt
39
+ training_metrics: result/training/metrics.json
40
+ predictions: result/output/predictions.npz
41
+ evaluation_metrics: result/evaluation/metrics.json
42
+ comparison: result/evaluation/comparison.png
config.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "MetNet-2",
3
+ "model_type": "metnet_2",
4
+ "architectures": ["MetNet2"],
5
+ "framework": "PyTorch",
6
+ "domain": "weather",
7
+ "task": "probabilistic-precipitation-forecasting",
8
+ "implementation": {
9
+ "entry_point": "model/metnet_2.py",
10
+ "scope": "core-method and logical full-dimension sampled-window engineering reproduction",
11
+ "train_script": "scripts/train.py",
12
+ "inference_script": "scripts/inference.py",
13
+ "evaluation_script": "scripts/result.py",
14
+ "synthetic_data_script": "scripts/fake_data.py"
15
+ },
16
+ "architecture": {
17
+ "logical_input_shape": ["B", 641, 512, 512],
18
+ "logical_output_shape": ["B", 512, 512, 512],
19
+ "engineering_window": [32, 32],
20
+ "classes": 512,
21
+ "lead_minutes": [2, 720, 2],
22
+ "core": ["ConvLSTM", "lead-time FiLM", "dilated residual blocks", "spatial and class chunking"]
23
+ },
24
+ "data": {
25
+ "datasets": ["MRMS", "HRRR", "GOES"],
26
+ "format_version": "metnet2_selected_windows_v1",
27
+ "input_channels": 641,
28
+ "precipitation_range_mm_h": [0.0, 102.4],
29
+ "coverage": "selected 32x32 target windows",
30
+ "is_complete_global": false,
31
+ "synthetic": true
32
+ },
33
+ "configuration_sources": [
34
+ "conf/config.yaml",
35
+ "model/metnet_2.py",
36
+ "scripts/fake_data.py",
37
+ "scripts/train.py",
38
+ "scripts/inference.py",
39
+ "scripts/result.py"
40
+ ]
41
+ }
configuration.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "framework": "PyTorch",
3
+ "task": "probabilistic precipitation forecasting",
4
+ "model": "MetNet-2",
5
+ "input_format": "logical BCHW: [B,641,512,512] with sampled windows for engineering execution",
6
+ "protocol": "lead-conditioned categorical precipitation forecasting from 2 to 720 minutes",
7
+ "default_config": "conf/config.yaml",
8
+ "training": "scripts/train.py",
9
+ "inference": "scripts/inference.py",
10
+ "evaluation": "scripts/result.py",
11
+ "visualization": "scripts/result.py"
12
+ }
model/metnet_2.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shape-faithful, memory-bounded MetNet-2 engineering implementation."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Iterable
8
+
9
+ import numpy as np
10
+ import torch
11
+ from torch import Tensor, nn
12
+ import torch.nn.functional as F
13
+ from torch.utils.data import Dataset
14
+ import yaml
15
+
16
+ CHANNEL_GROUPS = (
17
+ ("mrms_radar_history", 33), ("hrrr_atmosphere_history", 484),
18
+ ("goes_satellite_history", 96), ("static_geography", 24),
19
+ ("time_coordinates", 4),
20
+ )
21
+ LOGICAL_SHAPE = (641, 512, 512)
22
+ CLASS_RATES = np.linspace(0.0, 102.4, 512, dtype=np.float32)
23
+ assert sum(size for _, size in CHANNEL_GROUPS) == LOGICAL_SHAPE[0]
24
+
25
+
26
+ def load_config(path: str | Path = "conf/config.yaml") -> dict:
27
+ with Path(path).open(encoding="utf-8") as handle:
28
+ return yaml.safe_load(handle)
29
+
30
+
31
+ class ProceduralField:
32
+ """Generate crops of a logical [641, 512, 512] field without materializing it."""
33
+
34
+ shape = LOGICAL_SHAPE
35
+
36
+ def __init__(self, seed: int):
37
+ self.seed = int(seed)
38
+
39
+ def window(self, y: int, x: int, size: int, halo: int = 0) -> Tensor:
40
+ if size <= 0 or halo < 0 or not (0 <= y < 512 and 0 <= x < 512):
41
+ raise ValueError("invalid selected-window coordinates")
42
+ yy = torch.arange(y - halo, y + size + halo).clamp(0, 511).float()
43
+ xx = torch.arange(x - halo, x + size + halo).clamp(0, 511).float()
44
+ channels = torch.arange(641).float()[:, None, None]
45
+ return (torch.sin((channels + self.seed) * .017 + yy[None, :, None] * .031)
46
+ + torch.cos((channels + 3 * self.seed) * .011 + xx[None, None, :] * .023)).float()
47
+
48
+ def target_window(self, y: int, x: int, size: int, lead: int) -> Tensor:
49
+ yy = torch.arange(y, y + size)[:, None]
50
+ xx = torch.arange(x, x + size)[None, :]
51
+ return ((yy * 7 + xx * 11 + self.seed + lead // 2) % 512).long()
52
+
53
+
54
+ class WindowDataset(Dataset):
55
+ def __init__(self, data_path: str | Path, split: str = "train"):
56
+ with np.load(data_path) as data:
57
+ required = ("seed", "split", "y", "x", "size", "halo", "lead_minutes")
58
+ missing = set(required).difference(data.files)
59
+ if missing:
60
+ raise ValueError(f"dataset is missing fields: {sorted(missing)}")
61
+ indices = np.flatnonzero(data["split"].astype(str) == split)
62
+ self.records = [{key: data[key][i].item() for key in required} for i in indices]
63
+
64
+ def __len__(self) -> int:
65
+ return len(self.records)
66
+
67
+ def __getitem__(self, index: int) -> tuple[Tensor, Tensor, Tensor]:
68
+ record = self.records[index]
69
+ field = ProceduralField(record["seed"])
70
+ args = record["y"], record["x"], record["size"]
71
+ return (field.window(*args, record["halo"]),
72
+ field.target_window(*args, record["lead_minutes"]),
73
+ torch.tensor(record["lead_minutes"], dtype=torch.long))
74
+
75
+
76
+ def write_fake_data(path: str | Path, samples: int = 8, window: int = 32, halo: int = 8) -> Path:
77
+ if samples < 3 or window != 32 or window + 2 * halo > 512:
78
+ raise ValueError("fake data requires at least 3 selected 32x32 windows with a valid halo")
79
+ records = []
80
+ for i in range(samples):
81
+ records.append({
82
+ "id": f"sample-{i:04d}", "seed": 1000 + i,
83
+ "split": "train" if i < samples - 2 else "test",
84
+ "y": (i * 47) % (512 - window + 1), "x": (i * 83) % (512 - window + 1),
85
+ "size": window, "halo": halo, "lead_minutes": 2 + 2 * (i % 360),
86
+ })
87
+ output = Path(path)
88
+ output.parent.mkdir(parents=True, exist_ok=True)
89
+ np.savez_compressed(output, **{key: np.asarray([r[key] for r in records]) for key in records[0]})
90
+ return output
91
+
92
+
93
+ class LeadFiLMConv(nn.Module):
94
+ def __init__(self, cin: int, cout: int, dilation: int = 1):
95
+ super().__init__()
96
+ self.conv = nn.Conv2d(cin, cout, 3, padding=dilation, dilation=dilation)
97
+ self.film = nn.Linear(cout, 2 * cout)
98
+
99
+ def forward(self, x: Tensor, lead: Tensor) -> Tensor:
100
+ result = self.conv(x)
101
+ add, multiply = self.film(lead).chunk(2, dim=1)
102
+ return result * (1.0 + torch.tanh(multiply)[:, :, None, None]) + add[:, :, None, None]
103
+
104
+
105
+ class ConvLSTMCell(nn.Module):
106
+ def __init__(self, cin: int, hidden: int):
107
+ super().__init__()
108
+ self.hidden = hidden
109
+ self.gates = nn.Conv2d(cin + hidden, 4 * hidden, 3, padding=1)
110
+
111
+ def forward(self, x: Tensor, state: tuple[Tensor, Tensor] | None = None) -> tuple[Tensor, Tensor]:
112
+ if state is None:
113
+ shape = (x.shape[0], self.hidden, x.shape[-2], x.shape[-1])
114
+ state = x.new_zeros(shape), x.new_zeros(shape)
115
+ hidden, cell = state
116
+ in_gate, forget, candidate, out_gate = self.gates(torch.cat((x, hidden), 1)).chunk(4, 1)
117
+ cell = torch.sigmoid(forget) * cell + torch.sigmoid(in_gate) * torch.tanh(candidate)
118
+ return torch.sigmoid(out_gate) * torch.tanh(cell), cell
119
+
120
+
121
+ class DilatedResidualBlock(nn.Module):
122
+ def __init__(self, width: int, dilation: int):
123
+ super().__init__()
124
+ self.conv1 = LeadFiLMConv(width, width, dilation)
125
+ self.conv2 = LeadFiLMConv(width, width, dilation)
126
+
127
+ def forward(self, x: Tensor, lead: Tensor) -> Tensor:
128
+ return x + self.conv2(F.relu(self.conv1(F.relu(x), lead)), lead)
129
+
130
+
131
+ class MetNet2(nn.Module):
132
+ """MetNet-2 concept model retaining the 641-channel and 512-class contracts."""
133
+
134
+ def __init__(self, input_channels: int = 641, classes: int = 512, width: int = 8,
135
+ stacks: int = 1, dilations: Iterable[int] = (1, 2, 4, 8, 16, 32, 64, 128),
136
+ lead_max_minutes: int = 720):
137
+ super().__init__()
138
+ if input_channels != 641 or classes != 512:
139
+ raise ValueError("MetNet-2 requires 641 input channels and 512 output classes")
140
+ self.input_channels, self.classes = input_channels, classes
141
+ self.width, self.stacks = width, stacks
142
+ self.dilations = tuple(dilations)
143
+ self.lead_max_minutes, self.upscale = lead_max_minutes, 4
144
+ self.lead_embedding = nn.Sequential(nn.Linear(1, width), nn.SiLU(), nn.Linear(width, width))
145
+ self.input_projection = nn.Conv2d(input_channels, width, 1)
146
+ self.temporal = ConvLSTMCell(width, width)
147
+ self.blocks = nn.ModuleList(DilatedResidualBlock(width, dilation)
148
+ for _ in range(stacks) for dilation in self.dilations)
149
+ self.spatial = LeadFiLMConv(width, width)
150
+ self.head = nn.Conv2d(width, classes, 1)
151
+
152
+ def _lead(self, minutes: Tensor) -> Tensor:
153
+ if torch.any((minutes < 2) | (minutes > self.lead_max_minutes) | (minutes % 2 != 0)):
154
+ raise ValueError("lead time must be 2..720 minutes in 2-minute increments")
155
+ return self.lead_embedding((minutes.float() / self.lead_max_minutes).unsqueeze(1))
156
+
157
+ def _features(self, x: Tensor, lead_minutes: Tensor, output_size: int) -> Tensor:
158
+ if x.ndim != 4 or x.shape[1] != 641:
159
+ raise ValueError("x must have shape [B, 641, H, W]")
160
+ if output_size <= 0 or output_size % self.upscale:
161
+ raise ValueError("output_size must be positive and divisible by four")
162
+ lead = self._lead(lead_minutes.to(x.device))
163
+ features, _ = self.temporal(self.input_projection(x))
164
+ for block in self.blocks:
165
+ features = block(features, lead)
166
+ features = self.spatial(F.relu(features), lead)
167
+ crop = output_size // self.upscale
168
+ if min(features.shape[-2:]) < crop:
169
+ raise ValueError("input window is smaller than the requested output")
170
+ top, left = (features.shape[-2] - crop) // 2, (features.shape[-1] - crop) // 2
171
+ return F.interpolate(features[:, :, top:top + crop, left:left + crop], size=(output_size, output_size),
172
+ mode="bilinear", align_corners=False)
173
+
174
+ def forward_window(self, x: Tensor, lead_minutes: Tensor, output_size: int = 32,
175
+ class_slice: tuple[int, int] | None = None) -> Tensor:
176
+ features = self._features(x, lead_minutes, output_size)
177
+ start, end = class_slice or (0, self.classes)
178
+ if not (0 <= start < end <= self.classes):
179
+ raise ValueError("invalid class slice")
180
+ return F.conv2d(features, self.head.weight[start:end], self.head.bias[start:end])
181
+
182
+ def forward(self, x: Tensor, lead_minutes: Tensor, output_size: int = 32) -> Tensor:
183
+ return self.forward_window(x, lead_minutes, output_size)
184
+
185
+ @torch.no_grad()
186
+ def assemble_full(self, source: ProceduralField, lead_minutes: int, output_path: str | Path,
187
+ tile: int = 32, halo: int = 8, class_chunk: int = 64,
188
+ output: str = "probability", device: str | torch.device = "cpu") -> Path:
189
+ """Stream a complete [512, 512, 512] probability or CDF array to disk."""
190
+ if output not in {"probability", "cdf"}:
191
+ raise ValueError("output must be probability or cdf")
192
+ path = Path(output_path)
193
+ path.parent.mkdir(parents=True, exist_ok=True)
194
+ array = np.lib.format.open_memmap(path, mode="w+", dtype=np.float16, shape=(512, 512, 512))
195
+ self.eval().to(device)
196
+ lead = torch.tensor([lead_minutes], device=device)
197
+ for y in range(0, 512, tile):
198
+ for x0 in range(0, 512, tile):
199
+ size = min(tile, 512 - y, 512 - x0)
200
+ features = self._features(source.window(y, x0, size, halo).unsqueeze(0).to(device), lead, size)[0]
201
+ maximum = None
202
+ for start in range(0, 512, class_chunk):
203
+ logits = F.conv2d(features.unsqueeze(0), self.head.weight[start:start + class_chunk],
204
+ self.head.bias[start:start + class_chunk])[0]
205
+ value = logits.amax(0)
206
+ maximum = value if maximum is None else torch.maximum(maximum, value)
207
+ denominator = torch.zeros_like(maximum)
208
+ chunks = []
209
+ for start in range(0, 512, class_chunk):
210
+ logits = F.conv2d(features.unsqueeze(0), self.head.weight[start:start + class_chunk],
211
+ self.head.bias[start:start + class_chunk])[0]
212
+ exponent = torch.exp(logits - maximum)
213
+ denominator += exponent.sum(0)
214
+ chunks.append(exponent)
215
+ cumulative = torch.zeros_like(maximum)
216
+ for start, exponent in zip(range(0, 512, class_chunk), chunks):
217
+ values = exponent / denominator
218
+ if output == "cdf":
219
+ values = values.cumsum(0) + cumulative
220
+ cumulative = values[-1]
221
+ array[start:start + values.shape[0], y:y + size, x0:x0 + size] = values.cpu().numpy()
222
+ array.flush()
223
+ return path
224
+
225
+
226
+ def build_model(config: dict, paper: bool = False) -> MetNet2:
227
+ values = dict(config["model"])
228
+ if paper:
229
+ values.update({key: value for key, value in config["paper_model"].items()
230
+ if key in {"input_channels", "classes", "stacks", "dilations"}})
231
+ dilations = tuple(values.get("dilations", ()))
232
+ if dilations != (1, 2, 4, 8, 16, 32, 64, 128):
233
+ raise ValueError("each dilation stack must use rates 1,2,4,8,16,32,64,128")
234
+ if paper and values["stacks"] != 3:
235
+ raise ValueError("the paper model requires three dilation stacks")
236
+ return MetNet2(**values)
237
+
238
+
239
+ def categorical_nll_chunked(model: MetNet2, x: Tensor, lead: Tensor, target: Tensor,
240
+ output_size: int = 32, class_chunk: int = 64) -> Tensor:
241
+ """Compute exact categorical NLL while applying the class head in chunks."""
242
+ features = model._features(x, lead, output_size)
243
+ selected, logsumexp = torch.zeros_like(target, dtype=features.dtype), None
244
+ for start in range(0, model.classes, class_chunk):
245
+ end = min(start + class_chunk, model.classes)
246
+ logits = F.conv2d(features, model.head.weight[start:end], model.head.bias[start:end])
247
+ part = torch.logsumexp(logits, dim=1)
248
+ logsumexp = part if logsumexp is None else torch.logaddexp(logsumexp, part)
249
+ mask = (target >= start) & (target < end)
250
+ picked = logits.gather(1, (target - start).clamp(0, end - start - 1).unsqueeze(1)).squeeze(1)
251
+ selected = torch.where(mask, picked, selected)
252
+ return (logsumexp - selected).mean()
253
+
254
+
255
+ def save_checkpoint(path: str | Path, model: nn.Module, model_config: dict) -> None:
256
+ if int(os.environ.get("RANK", "0")) != 0:
257
+ return
258
+ module = model.module if hasattr(model, "module") else model
259
+ destination = Path(path)
260
+ destination.parent.mkdir(parents=True, exist_ok=True)
261
+ temporary = Path(f"{destination}.tmp")
262
+ torch.save({"model": module.state_dict(), "model_config": model_config,
263
+ "format_version": "metnet_2_v1"}, temporary)
264
+ os.replace(temporary, destination)
265
+
266
+
267
+ def load_checkpoint(path: str | Path, model: nn.Module) -> dict:
268
+ checkpoint = torch.load(path, map_location="cpu", weights_only=True)
269
+ if set(checkpoint) != {"model", "model_config", "format_version"}:
270
+ raise ValueError("checkpoint must contain model, model_config, and format_version")
271
+ model.load_state_dict(checkpoint["model"])
272
+ return checkpoint
273
+
274
+
275
+ def scores(probabilities: np.ndarray, target: np.ndarray,
276
+ thresholds: tuple[float, ...] = (.2, 1., 2., 4., 8.)) -> dict:
277
+ if probabilities.shape[0] != 512 or target.shape != probabilities.shape[1:]:
278
+ raise ValueError("expected probabilities [512,H,W] and target [H,W]")
279
+ cdf = np.cumsum(probabilities.astype(np.float32), axis=0)
280
+ observed_cdf = (np.arange(512)[:, None, None] >= target[None]).astype(np.float32)
281
+ result = {"discrete_crps": float(np.mean(np.sum((cdf - observed_cdf) ** 2, axis=0)))}
282
+ brier, csi = {}, {}
283
+ for threshold in thresholds:
284
+ index = min(511, int(round(threshold / .2)))
285
+ event_probability = 1.0 - cdf[index - 1] if index else np.ones_like(cdf[0])
286
+ observed, forecast = target >= index, event_probability >= .5
287
+ hits = np.logical_and(forecast, observed).sum()
288
+ denominator = hits + np.logical_and(forecast, ~observed).sum() + np.logical_and(~forecast, observed).sum()
289
+ brier[str(threshold)] = float(np.mean((event_probability - observed) ** 2))
290
+ csi[str(threshold)] = float(hits / denominator) if denominator else 1.0
291
+ result.update(brier=brier, csi=csi)
292
+ return result
293
+
294
+
295
+ def write_json(path: str | Path, value: dict) -> None:
296
+ destination = Path(path)
297
+ destination.parent.mkdir(parents=True, exist_ok=True)
298
+ destination.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
scripts/fake_data.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ from pathlib import Path
4
+ import sys
5
+
6
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
7
+ from model.metnet_2 import load_config, write_fake_data
8
+
9
+ parser = argparse.ArgumentParser(description="Create selected MetNet-2 validation windows")
10
+ parser.add_argument("--config", default="conf/config.yaml")
11
+ parser.add_argument("--output", default=None)
12
+ args = parser.parse_args()
13
+ config = load_config(args.config)
14
+ output = args.output or config["data"]["path"]
15
+ print(write_fake_data(output, config["data"]["samples"], config["data"]["window"], config["data"]["halo"]))
scripts/inference.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ import torch
7
+ from model.metnet_2 import CLASS_RATES, ProceduralField, build_model, load_checkpoint, load_config
8
+
9
+ parser = argparse.ArgumentParser(description="Run selected-window or streamed full-domain inference")
10
+ parser.add_argument("--config", default="conf/config.yaml")
11
+ parser.add_argument("--lead", type=int, default=None)
12
+ parser.add_argument("--full", action="store_true")
13
+ parser.add_argument("--cdf", action="store_true")
14
+ args = parser.parse_args()
15
+ config = load_config(args.config)
16
+ torch.set_num_threads(config["runtime"]["num_threads"])
17
+ device = torch.device("cuda" if config["runtime"]["device"] == "auto" and torch.cuda.is_available()
18
+ else "cpu" if config["runtime"]["device"] == "auto" else config["runtime"]["device"])
19
+ model = build_model(config).to(device)
20
+ load_checkpoint(config["paths"]["checkpoint"], model)
21
+ field, lead = ProceduralField(2001), args.lead or config["inference"]["lead_minutes"]
22
+ if args.full:
23
+ output = Path(config["paths"]["predictions"]).with_suffix(".npy")
24
+ print(model.assemble_full(field, lead, output, config["data"]["window"], config["data"]["halo"],
25
+ config["training"]["class_chunk"], "cdf" if args.cdf else "probability", device))
26
+ else:
27
+ window = config["data"]["window"]
28
+ model.eval()
29
+ with torch.no_grad():
30
+ logits = model(field.window(0, 0, window, config["data"]["halo"]).unsqueeze(0).to(device),
31
+ torch.tensor([lead], device=device), window)[0]
32
+ probabilities = logits.softmax(0).cpu().numpy().astype(np.float32)
33
+ if not np.isfinite(probabilities).all():
34
+ raise FloatingPointError("inference probabilities are not finite")
35
+ output = Path(config["paths"]["predictions"])
36
+ output.parent.mkdir(parents=True, exist_ok=True)
37
+ np.savez_compressed(output, probabilities=probabilities, cdf=np.cumsum(probabilities, axis=0),
38
+ target=field.target_window(0, 0, window, lead).numpy(), rates=CLASS_RATES,
39
+ lead_minutes=np.int32(lead), coverage=np.array(config["inference"]["coverage"]),
40
+ is_complete=np.bool_(config["inference"]["is_complete"]))
41
+ print(output)
scripts/result.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ from pathlib import Path
4
+
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ from model.metnet_2 import load_config, scores, write_json
8
+
9
+ parser = argparse.ArgumentParser(description="Evaluate and visualize MetNet-2 predictions")
10
+ parser.add_argument("--config", default="conf/config.yaml")
11
+ args = parser.parse_args()
12
+ config = load_config(args.config)
13
+ with np.load(config["paths"]["predictions"]) as data:
14
+ probabilities, target, rates = data["probabilities"], data["target"], data["rates"]
15
+ metrics = {str(int(data["lead_minutes"])): scores(probabilities, target)}
16
+ if not all(np.isfinite(value) for value in [metrics[next(iter(metrics))]["discrete_crps"],
17
+ *metrics[next(iter(metrics))]["brier"].values(),
18
+ *metrics[next(iter(metrics))]["csi"].values()]):
19
+ raise FloatingPointError("evaluation metrics are not finite")
20
+ write_json(config["paths"]["evaluation_metrics"], metrics)
21
+ expected, truth = (probabilities * rates[:, None, None]).sum(0), rates[target]
22
+ figure, axes = plt.subplots(1, 3, figsize=(11, 3.5), constrained_layout=True)
23
+ for axis, image, title in zip(axes, (truth, expected, expected - truth), ("Target", "Expected rate", "Error")):
24
+ plot = axis.imshow(image, cmap="viridis")
25
+ axis.set_title(title)
26
+ axis.set_axis_off()
27
+ figure.colorbar(plot, ax=axis, shrink=.75)
28
+ comparison = Path(config["paths"]["comparison"])
29
+ comparison.parent.mkdir(parents=True, exist_ok=True)
30
+ figure.savefig(comparison, dpi=140)
31
+ plt.close(figure)
32
+ print(config["paths"]["evaluation_metrics"], comparison)
scripts/train.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from torch.nn.parallel import DistributedDataParallel
9
+ from torch.utils.data import DataLoader, DistributedSampler
10
+ from model.metnet_2 import (WindowDataset, build_model, categorical_nll_chunked,
11
+ load_config, save_checkpoint, write_json)
12
+
13
+ parser = argparse.ArgumentParser(description="Train MetNet-2 on selected windows")
14
+ parser.add_argument("--config", default="conf/config.yaml")
15
+ parser.add_argument("--steps", type=int, default=None)
16
+ args = parser.parse_args()
17
+ config = load_config(args.config)
18
+ rank = int(os.environ.get("RANK", "0"))
19
+ world_size = int(os.environ.get("WORLD_SIZE", "1"))
20
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
21
+ distributed = world_size > 1
22
+ requested = config["runtime"]["device"]
23
+ use_cuda = (requested != "cpu" and torch.cuda.is_available()
24
+ and (not distributed or torch.cuda.device_count() >= world_size))
25
+ if distributed:
26
+ torch.distributed.init_process_group(backend="nccl" if use_cuda else "gloo")
27
+ torch.manual_seed(config["seed"] + rank)
28
+ torch.set_num_threads(config["runtime"]["num_threads"])
29
+ device = torch.device(f"cuda:{local_rank}" if use_cuda else "cpu" if requested == "auto" else requested)
30
+ if use_cuda:
31
+ device = torch.device(f"cuda:{local_rank}")
32
+ torch.cuda.set_device(device)
33
+ model = build_model(config).to(device)
34
+ if distributed:
35
+ model = DistributedDataParallel(model, device_ids=[local_rank] if device.type == "cuda" else None)
36
+ optimizer = torch.optim.Adam(model.parameters(), lr=config["training"]["learning_rate"])
37
+ dataset = WindowDataset(config["data"]["path"])
38
+ sampler = DistributedSampler(dataset, shuffle=True, seed=config["seed"]) if distributed else None
39
+ loader = DataLoader(dataset, batch_size=config["training"]["batch_size"], shuffle=sampler is None, sampler=sampler)
40
+ steps = args.steps if args.steps is not None else config["training"]["steps"]
41
+ losses = []
42
+ model.train()
43
+ for step, (inputs, target, lead) in enumerate(loader):
44
+ if step >= steps:
45
+ break
46
+ optimizer.zero_grad(set_to_none=True)
47
+ logits = model(inputs.to(device), lead.to(device), config["data"]["window"])
48
+ loss = F.cross_entropy(logits, target.to(device))
49
+ if not torch.isfinite(loss):
50
+ raise FloatingPointError("training loss is not finite")
51
+ loss.backward()
52
+ optimizer.step()
53
+ losses.append(float(loss))
54
+ print(f"step={step} nll={losses[-1]:.6f}")
55
+ if not losses:
56
+ raise RuntimeError("training produced no optimization steps")
57
+ summary = torch.tensor([sum(losses), len(losses)], dtype=torch.float64, device=device)
58
+ if distributed:
59
+ torch.distributed.all_reduce(summary)
60
+ if rank == 0:
61
+ save_checkpoint(config["paths"]["checkpoint"], model, config["model"])
62
+ write_json(config["paths"]["training_metrics"],
63
+ {"steps": int(summary[1]), "mean_nll": float(summary[0] / summary[1]), "world_size": world_size})
64
+ if distributed:
65
+ torch.distributed.destroy_process_group()
weight/.gitkeep ADDED
File without changes