multimodalart HF Staff commited on
Commit
cb6df4f
·
verified ·
1 Parent(s): 44fa161

Delete folder code/qwenvl/modalities/weather with huggingface_hub

Browse files
code/qwenvl/modalities/weather/__init__.py DELETED
@@ -1,86 +0,0 @@
1
- """Weather modality package.
2
-
3
- Polaris-style global forecast support, integrated as a standard modality
4
- under the ``ModalityRouter`` framework.
5
-
6
- The package contributes:
7
-
8
- * ``WeatherEncoder`` — Polaris ``CubeEmbedConv`` + Swin encoder + meteo merger.
9
- * ``IdentityWeatherProjector`` — pass-through; merger already projects to LLM hidden.
10
- * ``WeatherDecoder`` — Polaris meteo head + Charbonnier regression loss
11
- (registers ``compute_loss_from_hidden`` for the router).
12
-
13
- Special tokens: ``<|weather_start|>``, ``<|weather_end|>``, ``<|weather_pad|>``.
14
-
15
- Adding the modality at runtime:
16
-
17
- --weather_config_path qwenvl/modalities/weather/configs/config_weather_0p25_channel70.json
18
- --tune_weather_encoder True --tune_weather_decoder True
19
- """
20
-
21
- import logging
22
-
23
- from .encoder import WeatherEncoder
24
- from .projector import IdentityWeatherProjector
25
- from .decoder import WeatherDecoder
26
- from .processor import WeatherProcessor
27
-
28
- logger = logging.getLogger(__name__)
29
-
30
- MODALITY_CONFIG_KEY = "weather_config"
31
-
32
- TOKEN_DEFS = {
33
- "weather": {
34
- "start": "<|weather_start|>",
35
- "end": "<|weather_end|>",
36
- "pad": "<|weather_pad|>",
37
- },
38
- }
39
-
40
- __all__ = [
41
- "WeatherEncoder",
42
- "IdentityWeatherProjector",
43
- "WeatherDecoder",
44
- "WeatherProcessor",
45
- "register_modality",
46
- "MODALITY_CONFIG_KEY",
47
- "TOKEN_DEFS",
48
- ]
49
-
50
-
51
- def register_modality(router, config, llm_hidden_size: int):
52
- """Register the weather modality with the ``ModalityRouter``.
53
-
54
- Args:
55
- router: ModalityRouter instance.
56
- config: Qwen3VLWeatherConfig (Polaris-style hyperparams).
57
- llm_hidden_size: Hidden size of the LLM backbone — overrides
58
- ``config.qwenvl_dim`` so the merger / mlp_qwen2swin
59
- bridges are sized to the actual LLM in use.
60
- """
61
- # Force qwenvl_dim to match the active LLM, ignoring whatever was saved
62
- # in the JSON config. (The merger is randomly initialised when
63
- # init_weather_from is used, so 3584→4096 transitions are seamless.)
64
- if config.qwenvl_dim != llm_hidden_size:
65
- logger.info(
66
- f"[weather] overriding qwenvl_dim {config.qwenvl_dim} → {llm_hidden_size} "
67
- f"(active LLM hidden size)"
68
- )
69
- config.qwenvl_dim = llm_hidden_size
70
-
71
- encoder = WeatherEncoder(config)
72
- projector = IdentityWeatherProjector(qwenvl_dim=llm_hidden_size)
73
- decoder = WeatherDecoder(config, encoder=encoder)
74
-
75
- router.register_modality(
76
- "weather",
77
- encoder=encoder,
78
- projector=projector,
79
- decoder=decoder,
80
- is_image_like=True,
81
- )
82
- logger.info(
83
- f"[weather] registered: in_chans={config.in_chans}, "
84
- f"image_size={config.image_size}, patch_size={config.patch_size}, "
85
- f"hidden={config.hidden_size}, qwenvl_dim={config.qwenvl_dim}"
86
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/configs/config_weather_0p25_channel70.json DELETED
@@ -1,45 +0,0 @@
1
- {
2
- "polaris_vision_config": {
3
- "in_chans": 70,
4
- "in_frames": 1,
5
- "const_chans": 6,
6
-
7
- "upper_chans": 70,
8
-
9
- "hidden_size": 2048,
10
- "image_size": [721, 1440],
11
- "patch_size": 6,
12
- "window_size": 20,
13
- "encoder_depth": 12,
14
- "decoder_depth": 12,
15
- "num_heads": 32,
16
- "mlp_ratio": 4.0,
17
- "attn_type": "flash",
18
- "mask_type": "h",
19
- "norm_type": "adarms",
20
- "ffn_type": "geglu_ffn",
21
- "n_kv_heads": null,
22
-
23
- "embed_mode": "add",
24
- "embed_types": ["step", "hour", "doy", "lead_hour"],
25
- "embed_freq": 256,
26
-
27
- "frame_interval": "6h",
28
-
29
- "qwenvl_dim": 3584,
30
-
31
- "meteo_out_channels": 70,
32
- "meteo_output_size": [721, 1440],
33
- "meteo_token_id": 151655,
34
-
35
- "meteo_loss_type": "l1_channel",
36
- "meteo_loss_weight": 1.0,
37
- "meteo_loss_coef": 1.0,
38
- "lead_hour_scaling": true,
39
- "pl_chans": null,
40
-
41
- "meteo_data_path": "<PATH_TO_ERA5_ZARR>",
42
-
43
- "_attn_implementation": "eager"
44
- }
45
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/data/__init__.py DELETED
@@ -1,3 +0,0 @@
1
- """Weather data utilities (ERA5 dataset / collator / context loader)."""
2
-
3
- from .polaris_data_utils import load_meteorological_buffers # noqa: F401
 
 
 
 
code/qwenvl/modalities/weather/data/era5_dataset.py DELETED
@@ -1,96 +0,0 @@
1
- """Bio_qwen3vl-style ERA5 dataset.
2
-
3
- Produces samples ready for the bio_qwen3vl pipeline:
4
-
5
- * ``input_ids`` / ``attention_mask`` / ``position_ids`` / ``labels``:
6
- the chat template with ``<|weather_start|><|weather_pad|>*N<|weather_end|>``
7
- expanded to ``meteo_num_tokens`` pad tokens.
8
-
9
- * ``weather_input_ids`` / ``weather_attention_mask`` / ``weather_grid_thw``:
10
- dummy placeholders sized to ``swin_H * swin_W`` so the
11
- ``ModalityRouter.scatter_all`` finds them under the standard kwargs.
12
-
13
- * The actual meteorological data — ``meteo_values`` / ``targets`` / ``times`` /
14
- ``lead_hours`` / ``polaris_task`` / optional ``channel_mask`` — is
15
- carried in a ``"meteo_data"`` dict so the collator can stack across
16
- the batch in fp32.
17
-
18
- The collator unpacks ``meteo_data`` into the canonical
19
- ``weather_meteo_values`` / ``weather_targets`` / ``weather_times`` /
20
- ``weather_lead_hours`` / ``weather_polaris_task`` kwargs that the encoder
21
- reads.
22
- """
23
-
24
- from __future__ import annotations
25
-
26
- import json
27
- from dataclasses import dataclass
28
- from typing import Any, Dict, List, Optional, Sequence
29
-
30
- import numpy as np
31
- import pandas as pd
32
- import torch
33
- import transformers
34
-
35
-
36
- IGNORE_INDEX = -100
37
- DEFAULT_IMAGE_TOKEN = "<|image_pad|>" # only used for compatibility; the
38
- # weather pipeline uses <|weather_pad|>
39
-
40
-
41
- # ---------------------------------------------------------------------------
42
- # Chat-template builder for the meteo task
43
- # ---------------------------------------------------------------------------
44
-
45
- def _build_meteo_messages(
46
- input_text: str,
47
- meteo_pad_token: str,
48
- weather_start_token: str,
49
- weather_end_token: str,
50
- meteo_num_tokens: int,
51
- ) -> List[Dict[str, Any]]:
52
- """Build a 2-turn system+user message list with the meteo placeholder.
53
-
54
- The placeholder string is the same as what Polaris originally used,
55
- swapping ``<|vision_start|>``→``<|weather_start|>`` and
56
- ``<|image_pad|>``→``<|weather_pad|>`` so the bio_qwen3vl router knows
57
- where to scatter the encoder output.
58
- """
59
- pads = meteo_pad_token * meteo_num_tokens
60
- user_text = (
61
- f"{weather_start_token}{pads}\n{weather_end_token}"
62
- f"Describe this image."
63
- )
64
- return [
65
- {"role": "system",
66
- "content": [{"type": "text", "text": f"forecast\n{input_text}"}]},
67
- {"role": "user",
68
- "content": [{"type": "text", "text": user_text}]},
69
- ]
70
-
71
-
72
- def preprocess_meteo_chat(
73
- input_text: str,
74
- processor,
75
- meteo_pad_token: str,
76
- weather_start_token: str,
77
- weather_end_token: str,
78
- meteo_num_tokens: int,
79
- add_assistant_prompt: bool = True,
80
- ) -> Dict[str, torch.Tensor]:
81
- msgs = _build_meteo_messages(
82
- input_text, meteo_pad_token, weather_start_token,
83
- weather_end_token, meteo_num_tokens,
84
- )
85
- full = processor.apply_chat_template(
86
- msgs,
87
- tokenize=True,
88
- return_dict=True,
89
- return_tensors="pt",
90
- add_generation_prompt=add_assistant_prompt,
91
- )
92
- input_ids = full["input_ids"]
93
- if isinstance(input_ids, list):
94
- input_ids = torch.tensor(input_ids).unsqueeze(0)
95
- full["input_ids"] = input_ids
96
- return full
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/data/polaris_data_utils.py DELETED
@@ -1,191 +0,0 @@
1
- import os
2
- import numpy as np
3
- import pandas as pd
4
- import torch
5
- import torch.nn.functional as F
6
- import xarray as xr
7
- from einops import rearrange
8
-
9
- __all__ = [
10
- "make_seq", "crop_dataarray", "filter_dataset_channels", "load_meteorological_buffers",
11
- "TYPHOON_PAD_MULTIPLE", "pad_spatial_to_multiple", "crop_spatial_pad",
12
- "build_channel_map", "pad_channels", "extract_valid_channels",
13
- ]
14
-
15
- TYPHOON_PAD_MULTIPLE = 120
16
-
17
-
18
- def pad_spatial_to_multiple(x: torch.Tensor, multiple: int = TYPHOON_PAD_MULTIPLE):
19
- H, W = x.shape[-2], x.shape[-1]
20
- pad_h = (multiple - H % multiple) % multiple
21
- pad_w = (multiple - W % multiple) % multiple
22
- pt, pb = pad_h // 2, pad_h - pad_h // 2
23
- pl, pr = pad_w // 2, pad_w - pad_w // 2
24
- if pad_h > 0 or pad_w > 0:
25
- x = F.pad(x, (pl, pr, pt, pb), mode="constant", value=0)
26
- return x, (pt, pb, pl, pr)
27
-
28
-
29
- def crop_spatial_pad(x: torch.Tensor, pad_info):
30
- pt, pb, pl, pr = pad_info
31
- hs = slice(pt, x.shape[-2] - pb if pb > 0 else x.shape[-2])
32
- ws = slice(pl, x.shape[-1] - pr if pr > 0 else x.shape[-1])
33
- return x[..., hs, ws]
34
-
35
-
36
- def build_channel_map(src_names, ref_names):
37
- ref_idx = {n: i for i, n in enumerate(ref_names)}
38
- src_to_ref = np.array([ref_idx.get(str(n), -1) for n in src_names], dtype=np.int64)
39
- mask = np.zeros(len(ref_names), dtype=bool)
40
- for i in src_to_ref:
41
- if 0 <= i < len(ref_names):
42
- mask[i] = True
43
- return src_to_ref, mask
44
-
45
-
46
- def pad_channels(x_src, src_to_ref, ref_c):
47
- c = x_src.shape[0]
48
- out = np.zeros((ref_c,) + x_src.shape[1:], dtype=np.float32)
49
- for j in range(c):
50
- i = int(src_to_ref[j])
51
- if 0 <= i < ref_c:
52
- out[i] = x_src[j]
53
- return out
54
-
55
-
56
- def extract_valid_channels(x_ref, src_to_ref, src_c):
57
- out = np.zeros((src_c,) + x_ref.shape[1:], dtype=np.float32)
58
- for j in range(src_c):
59
- i = int(src_to_ref[j])
60
- if 0 <= i < x_ref.shape[0]:
61
- out[j] = x_ref[i]
62
- return out
63
-
64
- # 🚨 需要剔除的通道
65
- DEFAULT_REMOVE_CHANNELS = [
66
- "q2m", "d2m", "sst", "ws100m", "u100m", "v100m",
67
- "lcc", "mcc", "hcc", "tcc", "ssr", "ssrd", "fdir", "ttr", "tcw", "tp"
68
- ]
69
-
70
- def make_seq(ds, total_frames, frame_interval, frame_step=1, ignore_times=[]):
71
- i = 0
72
- inds = []
73
- times = ds.time.values
74
- ignore_timestamps = {pd.to_datetime(t) for t in ignore_times}
75
-
76
- while i < len(times) - total_frames:
77
- cur_sequence = []
78
- for j in range(i, i + total_frames - 1):
79
- if times[j+1] - times[j] == frame_interval:
80
- current_time = pd.to_datetime(times[j])
81
- if current_time in ignore_timestamps:
82
- continue
83
- cur_sequence.append(j)
84
-
85
- if len(cur_sequence) == total_frames - 1:
86
- inds.append(i)
87
-
88
- i += frame_step
89
-
90
- return np.array(inds, dtype=np.int32)
91
-
92
- def crop_dataarray(ds, image_size=None, latlon_range=None):
93
- if "lat" in ds.dims:
94
- ilats = np.arange(ds.lat.size)
95
- if latlon_range is not None:
96
- lat_min, lat_max, lon_min, lon_max = latlon_range
97
- ilats = np.where((ds.lat >= lat_min) & (ds.lat <= lat_max))[0]
98
- if image_size is not None:
99
- ilats = ilats[:image_size[0]]
100
- ds = ds.isel(lat=ilats)
101
-
102
- if "lon" in ds.dims:
103
- ilons = np.arange(ds.lon.size)
104
- if latlon_range is not None:
105
- lat_min, lat_max, lon_min, lon_max = latlon_range
106
- ilons = np.where((ds.lon >= lon_min) & (ds.lon <= lon_max))[0]
107
- if image_size is not None:
108
- ilons = ilons[:image_size[1]]
109
- ds = ds.isel(lon=ilons)
110
- return ds
111
-
112
- def filter_dataset_channels(ds, remove_channels=None):
113
- """
114
- 统一的通道过滤函数:Dataset 和 Buffer 读取都调用这个函数。
115
- """
116
- if remove_channels is None:
117
- remove_channels = DEFAULT_REMOVE_CHANNELS
118
-
119
- if isinstance(ds, xr.Dataset):
120
- ds = ds[list(ds.data_vars)[0]]
121
-
122
- all_channels = ds.channel.values.tolist()
123
- keep_channels = [c for c in all_channels if c not in remove_channels]
124
- keep_inds = [i for i, c in enumerate(all_channels) if c in keep_channels]
125
-
126
- out_ds = ds.sel(channel=keep_channels)
127
- return out_ds, keep_channels, keep_inds
128
-
129
- def load_meteorological_buffers(
130
- data_path: str,
131
- image_size=None,
132
- latlon_range=None,
133
- remove_channels=None,
134
- buffer_types=None,
135
- index_names=None
136
- ):
137
- """
138
- 统一的数据提取。
139
- """
140
- if buffer_types is None:
141
- buffer_types = [
142
- "mean", "std", "diff_mean", "diff_std", "climate",
143
- "const", "weight", "channel_mask", "land_mask", "station_mask", "rps_mask", "dry_mask"
144
- ]
145
- if index_names is None:
146
- index_names = dict(logid=[], uid=["u10m"], vid=["v10m"], accumid=[])
147
-
148
- ds = xr.open_zarr(data_path)
149
- if "level" in ds.dims:
150
- ds = ds.rename({"level": "channel"})
151
- ds = crop_dataarray(ds, image_size, latlon_range)
152
-
153
- # 🚨 调用统一过滤函数
154
- ds, keep_channels, keep_inds = filter_dataset_channels(ds, remove_channels)
155
-
156
- indices = {}
157
- for k, prefixes in index_names.items():
158
- inds = [i for i, name in enumerate(keep_channels) if name in prefixes]
159
- if len(inds) > 0:
160
- indices[k] = inds
161
-
162
- coords = dict(lat=ds.lat.values.tolist(), lon=ds.lon.values.tolist())
163
-
164
- buffers = {}
165
- for k in buffer_types:
166
- file_name = os.path.join(data_path, f"{k}.nc")
167
- if not os.path.exists(file_name):
168
- continue
169
-
170
- da = xr.open_dataarray(file_name)
171
- da = crop_dataarray(da, image_size, latlon_range)
172
-
173
- if k in ["mean", "std", "diff_mean", "diff_std"]:
174
- if "channel" in da.dims:
175
- da = da.sel(channel=keep_channels)
176
- else:
177
- da_vals = da.values[keep_inds]
178
- da = xr.DataArray(da_vals, coords={"channel": keep_channels}, dims=("channel",))
179
-
180
- values = da if k == "climate" else da.values
181
-
182
- if k == "const":
183
- values = rearrange(values, "c h w -> 1 c h w")
184
- elif k == "weight" and values.ndim == 1:
185
- values = rearrange(values, "h -> 1 h 1")
186
- elif k in ["mean", "std", "diff_mean", "diff_std"]:
187
- values = rearrange(values, "c -> c 1 1")
188
-
189
- buffers[k] = values
190
-
191
- return keep_channels, indices, coords, buffers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/decoder.py DELETED
@@ -1,256 +0,0 @@
1
- """Weather decoder: Polaris meteo head + regression-style loss.
2
-
3
- Adapts ``PolarisMeteoHead`` to the bio_qwen3vl ``ModalityRouter``. The
4
- decoder is invoked by ``compute_decoder_losses`` via the custom
5
- ``compute_loss_from_hidden`` hook (see ``qwenvl/registry/modality_router.py``):
6
- that path bypasses the standard CE-on-decode-logits flow used by RNA /
7
- protein / mol since meteo prediction is a regression problem on a 5D
8
- tensor field.
9
-
10
- Inputs needed at loss time live in two places:
11
-
12
- * ``hidden_states`` (LLM output) — selects the meteo-token positions.
13
- * ``self.encoder._step_cache`` — set by ``WeatherEncoder.forward`` and
14
- contains ``condition_embed`` / ``patch_embed_pre_swin`` /
15
- ``meteo_values`` / ``targets`` / ``lead_hours``.
16
-
17
- The encoder reference is wired up in ``register_modality`` so the decoder
18
- does not have to be passed it through every forward.
19
- """
20
-
21
- from __future__ import annotations
22
-
23
- import logging
24
- from typing import Any, Dict, Optional, Tuple
25
-
26
- import numpy as np
27
- import pandas as pd
28
- import torch
29
- import torch.nn as nn
30
- import torch.nn.functional as F
31
- from einops import rearrange
32
- from torch.utils.checkpoint import checkpoint
33
-
34
- from .internal.polaris_attention import precompute_freqs_cis
35
- from .internal.polaris_layers import AdaLN, DoubleDeconvHead, remove_small_scales
36
- from .internal.polaris_swin import SwinBlock
37
- from .internal.helpers import round_to_multiple
38
-
39
- logger = logging.getLogger(__name__)
40
-
41
-
42
- class WeatherDecoder(nn.Module):
43
- """Polaris meteo head + regression loss path.
44
-
45
- The original Polaris ``PolarisMeteoHead.forward`` is wrapped in
46
- ``compute_loss_from_hidden`` so it can pull the matching encoder cache
47
- and produce a scalar loss directly from LLM hidden states.
48
- """
49
-
50
- def __init__(self, config, encoder):
51
- super().__init__()
52
- self.config = config
53
- # Hold the encoder reference in a 1-element list so PyTorch's
54
- # automatic Module-attribute registration does NOT pull the
55
- # encoder's parameters in as decoder children (which would
56
- # duplicate them under both encoders.weather.* and decoders.weather.*
57
- # in the state_dict).
58
- self._encoder_ref = [encoder]
59
-
60
- self.hidden_size = config.hidden_size
61
- self.num_heads = config.num_heads
62
- self.qwenvl_dim = config.qwenvl_dim
63
- self.patch_size = config.patch_size
64
- self.upper_chans = config.upper_chans
65
- self.lower_chans = config.lower_chans
66
- self.decoder_depth = config.decoder_depth
67
-
68
- if isinstance(config.image_size, int):
69
- in_h = in_w = config.image_size
70
- else:
71
- in_h, in_w = config.image_size
72
- if self.patch_size == 1:
73
- swin_H = in_h // 2 * 2
74
- swin_W = in_w
75
- else:
76
- swin_H = in_h // self.patch_size
77
- swin_W = in_w // self.patch_size
78
- self._swin_HW = (swin_H, swin_W)
79
-
80
- max_seq_len = swin_H * swin_W
81
- freqs_cos, freqs_sin = precompute_freqs_cis(self.hidden_size // self.num_heads, max_seq_len)
82
- self.register_buffer("freqs_cos", freqs_cos, persistent=False)
83
- self.register_buffer("freqs_sin", freqs_sin, persistent=False)
84
- self._rope_buffers_checked = False
85
-
86
- # Cross-modal back projector: LLM hidden → swin hidden.
87
- # Trained at ``mm_projector_lr`` (matched on substring "mlp_qwen2swin").
88
- self.mlp_qwen2swin = nn.Sequential(
89
- nn.Linear(self.qwenvl_dim, self.qwenvl_dim),
90
- nn.GELU(),
91
- nn.Linear(self.qwenvl_dim, self.hidden_size),
92
- )
93
-
94
- if config.embed_mode == "add":
95
- self.embed_dim = config.hidden_size
96
- elif config.embed_mode == "cat":
97
- self.embed_dim = round_to_multiple(config.hidden_size // len(config.embed_types)) * len(config.embed_types)
98
- else:
99
- raise ValueError(f"Invalid embed_mode: {config.embed_mode}")
100
-
101
- self.decoder_layers = nn.ModuleList()
102
- for i in range(self.decoder_depth):
103
- blk = SwinBlock(
104
- dim=self.hidden_size, embed_dim=self.embed_dim, num_heads=self.num_heads,
105
- input_size=(swin_H, swin_W), window_size=config.window_size,
106
- shift_size=0 if i % 2 == 0 else config.window_size // 2,
107
- mlp_ratio=config.mlp_ratio, attn_type=config.attn_type,
108
- mask_type=config.mask_type, norm_type=config.norm_type,
109
- ffn_type=config.ffn_type, n_kv_heads=config.n_kv_heads,
110
- attn_implementation="eager",
111
- )
112
- self.decoder_layers.append(blk)
113
-
114
- self.norm_layer = AdaLN(self.hidden_size, embed_dim=self.embed_dim)
115
- self.pred_layer = DoubleDeconvHead(
116
- in_chans=self.hidden_size, upper_chans=self.upper_chans,
117
- lower_chans=self.lower_chans, patch_size=self.patch_size,
118
- )
119
-
120
- self.gradient_checkpointing = False
121
-
122
- # Provide the same context-injection API the encoder has, so the
123
- # training entry can call it on the decoder too without checking
124
- # which side actually owns the buffers.
125
- def inject_meteorological_context(self, *args, **kwargs):
126
- # The encoder is the canonical owner of mean/std/nanmask/...
127
- # We ignore here to avoid duplicate state.
128
- return
129
-
130
- def _ensure_finite_rope_buffers(self) -> None:
131
- """Rebuild the RoPE tables if the non-persistent buffers came up broken.
132
-
133
- ``freqs_cos`` / ``freqs_sin`` are ``persistent=False`` (not in the
134
- checkpoint) and are meant to be filled at ``__init__``. Under a
135
- meta-device ``from_pretrained`` load they can be ``to_empty``'d to
136
- uninitialised memory. A valid table has ``cos[0]==1`` and ``sin[0]==0``
137
- for every column — checking row 0 catches garbage that happens to be
138
- finite (large floats) which an isfinite-only check would miss.
139
- """
140
- if self._rope_buffers_checked:
141
- return
142
- with torch.no_grad():
143
- row0 = self.freqs_cos[0].detach().to(torch.float32)
144
- row0_sin = self.freqs_sin[0].detach().to(torch.float32)
145
- needs_recompute = (
146
- not torch.isfinite(self.freqs_cos).all()
147
- or not torch.isfinite(self.freqs_sin).all()
148
- or not torch.allclose(row0, torch.ones_like(row0), atol=1e-5)
149
- or not torch.allclose(row0_sin, torch.zeros_like(row0_sin), atol=1e-5)
150
- )
151
- if needs_recompute:
152
- logger.warning(
153
- "[weather] broken decoder RoPE buffers detected "
154
- "(nan/inf/zeros); recomputing freqs_cos/freqs_sin"
155
- )
156
- max_seq_len = int(self._swin_HW[0] * self._swin_HW[1])
157
- freqs_cos, freqs_sin = precompute_freqs_cis(
158
- self.hidden_size // self.num_heads, max_seq_len,
159
- )
160
- with torch.no_grad():
161
- self.freqs_cos.copy_(freqs_cos.to(self.freqs_cos.device, self.freqs_cos.dtype))
162
- self.freqs_sin.copy_(freqs_sin.to(self.freqs_sin.device, self.freqs_sin.dtype))
163
- self._rope_buffers_checked = True
164
-
165
- # ------------------------------------------------------------------
166
- # Loss computation (the only ModalityRouter-callable entry point)
167
- # ------------------------------------------------------------------
168
-
169
- def predict_from_hidden(
170
- self,
171
- hidden_states: torch.Tensor,
172
- **kwargs,
173
- ) -> Optional[torch.Tensor]:
174
- """Decode LLM hidden states into a meteo prediction tensor.
175
-
176
- Pure inference: no loss, no NaN-guard side effects. Reusable by
177
- the rollout engine, which needs to call this multiple times within
178
- one optimizer step (warm-up steps + selected step).
179
-
180
- Returns ``meteo_output`` of shape ``[B, C, H, W]`` (the ``pred_layer``
181
- currently emits a single-frame prediction; rollout stitches frames
182
- externally), or ``None`` if no weather pad tokens are present.
183
- """
184
- encoder = self._encoder_ref[0]
185
- cache = getattr(encoder, "_step_cache", None) or {}
186
- if not cache:
187
- return None
188
-
189
- self._ensure_finite_rope_buffers()
190
-
191
- condition_embed = cache["condition_embed"]
192
- patch_embed_post_swin = cache["patch_embed_post_swin"]
193
- meteo_values = cache["meteo_values"]
194
- lead_hours = cache["lead_hours"]
195
- input_size = cache["input_size"]
196
- T_in = cache["T_in"]
197
-
198
- input_ids = kwargs.get("input_ids")
199
- weather_pad_id = self._weather_pad_id_from_config(kwargs)
200
- if input_ids is None or weather_pad_id is None:
201
- raise RuntimeError(
202
- "WeatherDecoder.predict_from_hidden: input_ids / weather pad id missing."
203
- )
204
-
205
- meteo_mask = (input_ids == weather_pad_id)
206
- B, _, D = hidden_states.shape
207
- L_per_sample = int(meteo_mask.sum(dim=1)[0].item())
208
- if L_per_sample == 0:
209
- return None
210
- meteo_states = hidden_states[meteo_mask].view(B, L_per_sample, D)
211
-
212
- h = self.mlp_qwen2swin(meteo_states)
213
- if patch_embed_post_swin is not None:
214
- h = h + patch_embed_post_swin
215
-
216
- # Skip gradient checkpointing under torch.no_grad() (e.g. rollout
217
- # warm-up) — checkpoint() warns and offers no benefit when the
218
- # outputs don't require grad.
219
- use_gc = (
220
- self.gradient_checkpointing
221
- and self.training
222
- and torch.is_grad_enabled()
223
- )
224
- for blk in self.decoder_layers:
225
- if use_gc:
226
- h = checkpoint(
227
- blk, h, condition_embed, self.freqs_cos, self.freqs_sin,
228
- use_reentrant=False,
229
- )
230
- else:
231
- h = blk(h, condition_embed, self.freqs_cos, self.freqs_sin)
232
-
233
- h = self.norm_layer(h, condition_embed)
234
- h = rearrange(h, "n (h w) c -> n c h w", h=input_size[0] // self.patch_size // 2 * 2)
235
- meteo_output = self.pred_layer(
236
- h,
237
- residual=meteo_values[:, -1],
238
- input_size=input_size,
239
- lead_hour=condition_embed.new_tensor([0.0]) if lead_hours is None else lead_hours,
240
- target_frames=T_in - 1,
241
- )
242
- return meteo_output
243
-
244
- # ------------------------------------------------------------------
245
- # Helpers
246
- # ------------------------------------------------------------------
247
-
248
- @staticmethod
249
- def _weather_pad_id_from_config(kwargs) -> Optional[int]:
250
- """Extract ``<|weather_pad|>`` id.
251
-
252
- We rely on the model upstream to pass it under the key
253
- ``__weather_pad_id__`` (set by the model wrapper before calling
254
- ``compute_decoder_losses``). Falls back to None when missing.
255
- """
256
- return kwargs.get("__weather_pad_id__")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/encoder.py DELETED
@@ -1,479 +0,0 @@
1
- """Weather encoder: Polaris Swin-ViT + meteo merger (projector to LLM hidden).
2
-
3
- Contract:
4
-
5
- * ``forward(input_ids, attention_mask, **extra_kwargs) -> (latent, mask)``.
6
- ``input_ids`` / ``attention_mask`` are placeholders sized to the number of
7
- meteo tokens; the real meteorological tensors arrive through ``extra_kwargs``
8
- (``meteo_values``, ``times``, ``lead_hours``, …) under the ``weather_`` prefix.
9
-
10
- * The encoder pre-computes ``condition_embed`` and the patch embed and stores
11
- them in ``self._step_cache`` for the matching ``WeatherDecoder`` to reuse.
12
-
13
- * Mean / std / nanmask buffers (from ``inject_meteorological_context``) are
14
- stored on the encoder so ``unnormalize`` / lat-weighting / channel masking
15
- run without any external wrapper.
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- import logging
21
- from typing import Any, Dict, List, Optional, Tuple
22
-
23
- import numpy as np
24
- import pandas as pd
25
- import torch
26
- import torch.nn as nn
27
- import torch.nn.functional as F
28
- from einops import rearrange
29
- from torch.utils.checkpoint import checkpoint
30
-
31
- from .internal.polaris_attention import precompute_freqs_cis
32
- from .internal.polaris_layers import (
33
- AdaLN,
34
- CubeEmbedConv,
35
- LayerNorm,
36
- PatchEmbedConv,
37
- TimestepEmbed,
38
- )
39
- from .internal.polaris_swin import SwinBlock
40
- from .internal.helpers import round_to_multiple
41
-
42
- try:
43
- from transformers.models.qwen2.modeling_qwen2 import Qwen2RMSNorm as _MergerNorm
44
- except Exception: # pragma: no cover
45
- _MergerNorm = nn.LayerNorm
46
-
47
- logger = logging.getLogger(__name__)
48
-
49
-
50
- class PolarisMeteoPatchMerger(nn.Module):
51
- """Cross-modal projector ('merger'): Swin hidden → LLM hidden.
52
-
53
- Same arithmetic as the original Polaris ``PolarisMeteoPatchMerger``;
54
- serves as the modality projector for the ``ModalityRouter`` (an
55
- ``IdentityProjector`` is registered separately so the router can scatter
56
- its output directly).
57
- """
58
-
59
- def __init__(self, dim: int, context_dim: int) -> None:
60
- super().__init__()
61
- self.hidden_size = context_dim
62
- self.ln_q = _MergerNorm(context_dim, eps=1e-6)
63
- self.mlp = nn.Sequential(
64
- nn.Linear(self.hidden_size, self.hidden_size),
65
- nn.GELU(),
66
- nn.Linear(self.hidden_size, dim),
67
- )
68
-
69
- def forward(self, x: torch.Tensor) -> torch.Tensor:
70
- bsz, seqlen, _ = x.shape
71
- x = self.ln_q(x)
72
- x = x.view(-1, self.hidden_size)
73
- x = self.mlp(x)
74
- x = x.view(bsz, seqlen, -1)
75
- return x
76
-
77
-
78
- class WeatherEncoder(nn.Module):
79
- """Polaris-style Swin encoder + meteo merger.
80
-
81
- The forward signature is dictated by ``ModalityRouter.encode_and_project``:
82
- it takes ``input_ids`` / ``attention_mask`` (here used only to derive a
83
- batch size; their real meaning is conveyed by the placeholder pad
84
- tokens scattered into the LLM input embeddings) and a number of
85
- keyword-only extras forwarded by the collator under the ``weather_``
86
- prefix (already stripped by the router).
87
- """
88
-
89
- # Exposed so the router can route per-modality state_dict keys cleanly.
90
- is_image_like = True
91
-
92
- def __init__(self, config):
93
- super().__init__()
94
- self.config = config
95
- self.hidden_size = config.hidden_size
96
- self.image_size = config.image_size
97
- self.patch_size = config.patch_size
98
- self.window_size = config.window_size
99
- self.encoder_depth = config.encoder_depth
100
- self.num_heads = config.num_heads
101
- self.const_chans = config.const_chans
102
-
103
- self.patch_embed = CubeEmbedConv(
104
- in_chans=config.in_chans,
105
- out_chans=self.hidden_size,
106
- in_frames=config.in_frames,
107
- norm_func=None,
108
- flatten=True,
109
- patch_size=self.patch_size,
110
- )
111
-
112
- if self.const_chans > 0:
113
- self.const_embed = PatchEmbedConv(
114
- self.const_chans, self.hidden_size, config.patch_size,
115
- norm_func=LayerNorm, flatten=True,
116
- )
117
-
118
- self.embed_mode = config.embed_mode
119
- self.embed_types = list(config.embed_types)
120
- self.embed_freq = config.embed_freq
121
- self._init_time_embeddings()
122
-
123
- if isinstance(config.image_size, int):
124
- in_h = in_w = config.image_size
125
- else:
126
- in_h, in_w = config.image_size
127
- if self.patch_size == 1:
128
- swin_H = in_h // 2 * 2
129
- swin_W = in_w
130
- else:
131
- swin_H = in_h // self.patch_size
132
- swin_W = in_w // self.patch_size
133
- self._swin_HW = (swin_H, swin_W)
134
-
135
- self.encoder_layers = nn.ModuleList()
136
- for i in range(self.encoder_depth):
137
- blk = SwinBlock(
138
- dim=self.hidden_size, embed_dim=self.embed_dim, num_heads=self.num_heads,
139
- input_size=(swin_H, swin_W), window_size=self.window_size,
140
- shift_size=0 if i % 2 == 0 else self.window_size // 2,
141
- mlp_ratio=config.mlp_ratio, attn_type=config.attn_type,
142
- mask_type=config.mask_type, norm_type=config.norm_type,
143
- ffn_type=config.ffn_type, n_kv_heads=config.n_kv_heads,
144
- attn_implementation="eager",
145
- )
146
- self.encoder_layers.append(blk)
147
-
148
- # Cross-modal merger: Swin hidden → LLM hidden (qwenvl_dim).
149
- # `ModalityRouter` registers an IdentityProjector for `weather` since
150
- # the upscaling already happens here.
151
- self.meteo_merger = PolarisMeteoPatchMerger(
152
- dim=config.qwenvl_dim, context_dim=self.hidden_size,
153
- )
154
-
155
- max_seq_len = swin_H * swin_W
156
- freqs_cos, freqs_sin = precompute_freqs_cis(self.hidden_size // self.num_heads, max_seq_len)
157
- self.register_buffer("freqs_cos", freqs_cos, persistent=False)
158
- self.register_buffer("freqs_sin", freqs_sin, persistent=False)
159
-
160
- self.gradient_checkpointing = False
161
-
162
- # ── Lazy data buffers (mean/std/nanmask/const/weight/...) ───
163
- # Populated by ``inject_meteorological_context``. Held in fp32
164
- # CPU tensors and copied to device on demand to keep memory low.
165
- self._polaris_data_loaded = False
166
- self.channels: List[str] = []
167
- self.indices: Dict[str, List[int]] = {}
168
- self.coords: Dict[str, list] = {}
169
- self._fp32_master: Dict[str, torch.Tensor] = {}
170
-
171
- # Per-forward cache shared with the matching WeatherDecoder.
172
- # Cleared at the start of every encoder forward to avoid leaking
173
- # graph-disconnected tensors across steps.
174
- self._step_cache: Dict[str, Any] = {}
175
-
176
- # ------------------------------------------------------------------
177
- # RoPE buffer self-heal
178
- # ------------------------------------------------------------------
179
-
180
- def _ensure_rope_buffers(self) -> None:
181
- """Rebuild the RoPE tables if the non-persistent buffers came up broken.
182
-
183
- ``freqs_cos`` / ``freqs_sin`` are ``persistent=False`` (not in the
184
- checkpoint) and are meant to be filled at ``__init__``. Under a
185
- meta-device ``from_pretrained`` load they can be ``to_empty``'d to
186
- uninitialised memory. A valid table has ``cos[0]==1`` and ``sin[0]==0``
187
- for every column — checking row 0 catches garbage that is finite (large
188
- floats) which an isfinite-only check would miss.
189
- """
190
- with torch.no_grad():
191
- row0 = self.freqs_cos[0].detach().to(torch.float32)
192
- row0_sin = self.freqs_sin[0].detach().to(torch.float32)
193
- needs_recompute = (
194
- not torch.isfinite(self.freqs_cos).all()
195
- or not torch.isfinite(self.freqs_sin).all()
196
- or not torch.allclose(row0, torch.ones_like(row0), atol=1e-5)
197
- or not torch.allclose(row0_sin, torch.zeros_like(row0_sin), atol=1e-5)
198
- )
199
- if not needs_recompute:
200
- return
201
- head_dim = self.hidden_size // self.num_heads
202
- max_seq_len = self.freqs_cos.shape[0]
203
- new_cos, new_sin = precompute_freqs_cis(head_dim, max_seq_len)
204
- with torch.no_grad():
205
- self.freqs_cos.copy_(new_cos.to(self.freqs_cos.device, self.freqs_cos.dtype))
206
- self.freqs_sin.copy_(new_sin.to(self.freqs_sin.device, self.freqs_sin.dtype))
207
-
208
- # ------------------------------------------------------------------
209
- # Time-conditioning
210
- # ------------------------------------------------------------------
211
-
212
- def _init_time_embeddings(self):
213
- if self.embed_mode == "add":
214
- self.embed_dim = self.hidden_size
215
- elif self.embed_mode == "cat":
216
- self.embed_dim = round_to_multiple(self.hidden_size // len(self.embed_types)) * len(self.embed_types)
217
- else:
218
- raise ValueError(f"Invalid embed_mode: {self.embed_mode}")
219
-
220
- # Optional dropout on time-embedding outputs (anti-overfit for
221
- # ``hour`` / ``doy``). Read from config; default 0.0 keeps the
222
- # legacy zero-dropout behaviour identical to upstream Polaris.
223
- time_embed_dropout = float(getattr(self.config, "time_embed_dropout", 0.0))
224
- for k in self.embed_types:
225
- embed_layer = TimestepEmbed(
226
- self.embed_dim, frequency=self.embed_freq,
227
- is_periodic=(k in ["hour", "doy"]), sinusoidal=True,
228
- dropout=time_embed_dropout,
229
- )
230
- self.add_module(f"{k}_embed", embed_layer)
231
-
232
- def _forward_embedding(self, conds: Dict[str, torch.Tensor]) -> torch.Tensor:
233
- if self.embed_mode == "add":
234
- embed = 0
235
- for k in self.embed_types:
236
- embedding = getattr(self, f"{k}_embed")
237
- embed = embed + embedding(conds[k])
238
- return embed
239
- # cat
240
- embeds = [getattr(self, f"{k}_embed")(conds[k]) for k in self.embed_types]
241
- return torch.cat(embeds, dim=1)
242
-
243
- # ------------------------------------------------------------------
244
- # Meteo data buffer management
245
- # ------------------------------------------------------------------
246
-
247
- def inject_meteorological_context(self, channels, indices, coords, buffers):
248
- """Inject ERA5 mean/std/nanmask buffers onto this encoder.
249
-
250
- Stores fp32 tensors on CPU; ``_get_fp32_const`` copies to device on
251
- demand. Mirrors the original Polaris API so external loaders
252
- (``polaris_data_utils.load_meteorological_buffers``) work unchanged.
253
- """
254
- self.channels = channels
255
- self.indices = indices
256
- self.coords = coords
257
- for k, v in buffers.items():
258
- if isinstance(v, np.ndarray):
259
- t = torch.from_numpy(v).float()
260
- elif torch.is_tensor(v):
261
- t = v.detach().float()
262
- else:
263
- setattr(self, k, v)
264
- continue
265
- self._fp32_master[k] = t.cpu()
266
- self._polaris_data_loaded = True
267
-
268
- def _get_fp32_const(self, name: str, device: torch.device) -> torch.Tensor:
269
- if not self._polaris_data_loaded or name not in self._fp32_master:
270
- raise RuntimeError(
271
- f"WeatherEncoder data not loaded! Call inject_meteorological_context() "
272
- f"before using '{name}'."
273
- )
274
- return self._fp32_master[name].to(device=device, dtype=torch.float32, non_blocking=True)
275
-
276
- # ------------------------------------------------------------------
277
- # Input pre-processing (replicates Polaris reset_input / nan handling)
278
- # ------------------------------------------------------------------
279
-
280
- def _reset_output(self, x: torch.Tensor) -> torch.Tensor:
281
- if "nanmask" in self._fp32_master:
282
- nanmask = self._fp32_master["nanmask"].to(x.device)[: x.shape[-3]]
283
- x = x * (~nanmask)
284
- if "channel_mask" in self._fp32_master:
285
- cmask = self._fp32_master["channel_mask"].to(x.device)[: x.shape[-3]]
286
- x = x * cmask
287
- return x
288
-
289
- def _reset_input(self, x: torch.Tensor) -> torch.Tensor:
290
- x = self._reset_output(x)
291
- accumid = self.indices.get("accumid", [])
292
- if len(accumid) > 0:
293
- if torch.is_grad_enabled():
294
- x = x.clone()
295
- x[:, :, accumid] = 0
296
- return x
297
-
298
- # ------------------------------------------------------------------
299
- # Conditioning factory (hour, doy, step, lead_hour, optional const)
300
- # ------------------------------------------------------------------
301
-
302
- def get_condition(
303
- self,
304
- t: int,
305
- times,
306
- lead_hour,
307
- device: torch.device,
308
- dtype: torch.dtype,
309
- ) -> Dict[str, torch.Tensor]:
310
- times = pd.DatetimeIndex(times)
311
- B = len(times)
312
-
313
- if isinstance(lead_hour, torch.Tensor):
314
- lead_np = lead_hour.detach().to(dtype=torch.float32, device="cpu").numpy()
315
- else:
316
- lead_np = np.asarray(lead_hour, dtype=np.float32)
317
-
318
- if lead_np.ndim == 0:
319
- lead_np = np.full((B,), float(lead_np), dtype=np.float32)
320
- elif lead_np.size == 1 and B > 1:
321
- lead_np = np.full((B,), float(lead_np.reshape(-1)[0]), dtype=np.float32)
322
- elif lead_np.size != B:
323
- raise ValueError(f"lead_hour size mismatch: got {lead_np.size}, expected {B}")
324
-
325
- cur_times = times + pd.to_timedelta(lead_np * t, unit="h")
326
- used_times = cur_times
327
-
328
- hour_np = used_times.hour.values.astype(np.float32) / 24.0
329
- doy_np = (np.minimum(365, used_times.dayofyear.values).astype(np.float32) / 365.0)
330
-
331
- hour = torch.tensor(hour_np, dtype=dtype, device=device)
332
- doy = torch.tensor(doy_np, dtype=dtype, device=device)
333
-
334
- Tmax = getattr(self.config, "max_rollout_steps", 200)
335
- step_scalar = float(np.log1p(float(t)) / np.log1p(float(Tmax)))
336
- step = torch.full((B,), step_scalar, dtype=dtype, device=device)
337
- if self.training:
338
- step = step + torch.empty((B,), device=device, dtype=dtype).uniform_(-0.005, 0.005)
339
-
340
- lead_hour_t = torch.tensor(lead_np, dtype=dtype, device=device)
341
-
342
- conds = dict(hour=hour, doy=doy, step=step, lead_hour=lead_hour_t)
343
- if "const" in self._fp32_master:
344
- conds["const"] = self._get_fp32_const("const", device).type(dtype)
345
- return conds
346
-
347
- # ------------------------------------------------------------------
348
- # Encoder forward (ModalityRouter contract)
349
- # ------------------------------------------------------------------
350
-
351
- def forward(
352
- self,
353
- input_ids: torch.Tensor,
354
- attention_mask: torch.Tensor,
355
- meteo_values: Optional[torch.Tensor] = None,
356
- targets: Optional[torch.Tensor] = None,
357
- times: Optional[Any] = None,
358
- lead_hours: Optional[torch.Tensor] = None,
359
- polaris_task: Optional[Any] = None,
360
- channel_mask: Optional[torch.Tensor] = None,
361
- step_idx: int = 0,
362
- **_unused,
363
- ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
364
- """Encode meteorological inputs.
365
-
366
- Args:
367
- input_ids / attention_mask: dummy placeholders sized
368
- ``[B, swin_H * swin_W]``. Required by the router signature
369
- but not used computationally.
370
- meteo_values: ``[B, T_in, C, H, W]`` float32 (smuggled as int32 by
371
- the collator and reinterpreted upstream) — already on device.
372
- times: pandas timestamps for the current batch (one per sample).
373
- lead_hours: ``[B]`` int / float — forecast lead hours.
374
- channel_mask: optional ``[B, C, 1, 1]`` channel mask.
375
- step_idx: int rollout step index (0 for single-step training).
376
-
377
- Returns:
378
- latent: ``[B, swin_H * swin_W, qwenvl_dim]`` after meteo_merger.
379
- latent_mask: None.
380
- """
381
- if meteo_values is None:
382
- raise ValueError("WeatherEncoder.forward requires `meteo_values` in extras.")
383
-
384
- # Rebuild RoPE freqs if the checkpoint load left them uninitialised
385
- # (non-persistent buffers under a meta-device load come up as garbage).
386
- self._ensure_rope_buffers()
387
-
388
- # NaN→zero, fp32 normalisation
389
- if meteo_values.dtype == torch.int32:
390
- meteo_values = meteo_values.contiguous().view(torch.float32)
391
- meteo_values = meteo_values.contiguous().to(torch.float32)
392
- nanmask_local = torch.isnan(meteo_values[-1, -1])
393
- if torch.any(nanmask_local):
394
- self._fp32_master["nanmask"] = nanmask_local.cpu()
395
- meteo_values = torch.nan_to_num(meteo_values)
396
- meteo_values = self._reset_input(meteo_values)
397
- if channel_mask is not None:
398
- self._fp32_master["channel_mask"] = channel_mask[0].cpu()
399
-
400
- device = meteo_values.device
401
- # Compute conditions & feed encoder. Use the encoder's compute dtype
402
- # (e.g. bf16) — meteo_values is fp32 here due to the upstream
403
- # NaN/normalisation cast, but the embedding MLPs run in bf16 and
404
- # would otherwise see a dtype mismatch in F.linear.
405
- conds = self.get_condition(step_idx, times, lead_hours, device=device, dtype=self._embed_dtype())
406
-
407
- meteo_compute = meteo_values.to(self._embed_dtype())
408
- meteo_feature = self.patch_embed(meteo_compute, conds["lead_hour"])
409
- patch_embed = meteo_feature
410
-
411
- if self.const_chans > 0 and "const" in conds:
412
- const = conds["const"].to(device=device, dtype=meteo_compute.dtype)
413
- meteo_feature = meteo_feature + self.const_embed(const)
414
-
415
- condition_embed = self._forward_embedding(conds)
416
-
417
- # Skip gradient checkpointing under torch.no_grad() — e.g. rollout
418
- # warm-up steps — otherwise PyTorch emits a per-block "None of the
419
- # inputs have requires_grad=True" warning and `checkpoint` adds
420
- # pointless bookkeeping for a path that won't backprop anyway.
421
- use_gc = (
422
- self.gradient_checkpointing
423
- and self.training
424
- and torch.is_grad_enabled()
425
- )
426
- for blk in self.encoder_layers:
427
- if use_gc:
428
- meteo_feature = checkpoint(
429
- blk, meteo_feature, condition_embed, self.freqs_cos, self.freqs_sin,
430
- use_reentrant=False,
431
- )
432
- else:
433
- meteo_feature = blk(meteo_feature, condition_embed, self.freqs_cos, self.freqs_sin)
434
-
435
- # `meteo_feature` is the post-swin encoder output (Polaris's
436
- # `meteo_embeds`). The matching head uses it as the skip connection
437
- # *after* mlp_qwen2swin, so cache before applying the merger.
438
- meteo_embeds_post_swin = meteo_feature
439
-
440
- merged = self.meteo_merger(meteo_feature) # → [B, L, qwenvl_dim]
441
-
442
- # Cache for the decoder. We deliberately keep references to all
443
- # the intermediate tensors needed by the head so the decoder can
444
- # reuse them without recomputing the encoder side.
445
- # ``patch_embed_post_swin`` matches the Polaris ``meteo_embeds``
446
- # variable used in ``polaris_head(meteo_feature_patch_embed=...)``
447
- # under the use_language=True flow — i.e. the encoder swin output.
448
- self._step_cache = {
449
- "condition_embed": condition_embed,
450
- "patch_embed_post_swin": meteo_embeds_post_swin,
451
- "meteo_values": meteo_values,
452
- "targets": targets,
453
- "times": times,
454
- "lead_hours": lead_hours,
455
- "input_size": meteo_values.shape[-2:],
456
- "T_in": meteo_values.shape[1],
457
- }
458
-
459
- # Mask is None — every meteo token is valid; the router will infer
460
- # the count from the pad-token mask.
461
- return merged, None
462
-
463
- def _embed_dtype(self) -> torch.dtype:
464
- # Canonical encoder compute dtype (handles bf16 / fp32 uniformly).
465
- for p in self.parameters():
466
- return p.dtype
467
- return torch.float32
468
-
469
- @torch.no_grad()
470
- def unnormalize(self, x: torch.Tensor, fill_type: str = "zero") -> torch.Tensor:
471
- """Undo channel-wise mean/std normalisation. fp32 throughout."""
472
- mean = self._get_fp32_const("mean", x.device)
473
- std = self._get_fp32_const("std", x.device)
474
- x = x.to(torch.float32) * std + mean
475
- logid = self.indices.get("logid", [])
476
- if len(logid) > 0:
477
- v = x[:, :, logid].clamp(min=0, max=7)
478
- x[:, :, logid] = torch.expm1(v)
479
- return self._reset_output(x)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/internal/__init__.py DELETED
@@ -1,11 +0,0 @@
1
- # SPDX-License-Identifier: Apache-2.0
2
- # Portions adapted from the Polaris weather-forecasting codebase (released here
3
- # under Apache-2.0 with the authors' permission) and from the Swin Transformer
4
- # (Microsoft, MIT). See THIRD_PARTY_LICENSES.md at the repo root.
5
- """Internal Polaris support modules (attention, layers, swin, helpers).
6
-
7
- These are adapted from the original Polaris codebase and keep the Swin
8
- Transformer window-attention / patch-merging design. They are kept as a private
9
- submodule so the public weather encoder/decoder/projector can pin against a
10
- stable internal API.
11
- """
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/internal/helpers.py DELETED
@@ -1,45 +0,0 @@
1
- import collections.abc
2
- from itertools import repeat
3
- from inspect import isfunction
4
- from torch.nn import functional as F
5
-
6
- __all__ = ["to_2tuple", "round_to_multiple", "exists", "default", "append_dims"]
7
-
8
- def _ntuple(n):
9
- def parse(x):
10
- if isinstance(x, collections.abc.Iterable):
11
- return x
12
- return tuple(repeat(x, n))
13
- return parse
14
-
15
- to_1tuple = _ntuple(1)
16
- to_2tuple = _ntuple(2)
17
- to_3tuple = _ntuple(3)
18
- to_4tuple = _ntuple(4)
19
- to_ntuple = _ntuple
20
-
21
-
22
- def round_to_multiple(x: int, multiple_of: int = 32) -> int:
23
- """Round up x to the nearest multiple of multiple_of."""
24
- return int(multiple_of * ((x + multiple_of - 1) // multiple_of))
25
-
26
-
27
- def exists(val):
28
- return val is not None
29
-
30
-
31
- def default(val, d):
32
- if exists(val):
33
- return val
34
- return d() if isfunction(d) else d
35
-
36
- def append_dims(x, target_dims):
37
- """Appends dimensions to the end of a tensor until it has target_dims dimensions."""
38
- dims_to_append = target_dims - x.ndim
39
- if dims_to_append < 0:
40
- raise ValueError(
41
- f"input has {x.ndim} dims but target_dims is {target_dims}, which is less"
42
- )
43
- return x[(...,) + (None,) * dims_to_append]
44
-
45
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/internal/polaris_attention.py DELETED
@@ -1,198 +0,0 @@
1
-
2
- import torch
3
- from typing import Optional
4
- from torch import nn
5
- import torch.nn.functional as F
6
- from einops import rearrange
7
-
8
-
9
- __all__ = ["FlashAttention", "precompute_freqs_cis"]
10
-
11
-
12
- class RMSNorm(nn.Module):
13
- def __init__(self, dim: int, eps: float = 1e-6, compile: bool = False):
14
- super().__init__()
15
- self.eps = eps
16
- self.weight = nn.Parameter(torch.ones(dim))
17
- self.rmsnorm_fn = (
18
- torch.compile(self.compute_rmsnorm, fullgraph=True)
19
- if compile
20
- else self.compute_rmsnorm
21
- )
22
-
23
- @staticmethod
24
- def compute_rmsnorm(x: torch.Tensor, weight: torch.Tensor, eps: float):
25
- def _norm(x, eps):
26
- return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + eps)
27
-
28
- output = _norm(x.float(), eps).type_as(x)
29
- return output * weight
30
-
31
- def forward(self, x: torch.Tensor):
32
- return self.rmsnorm_fn(x, self.weight, self.eps)
33
-
34
- def reset_parameters(self):
35
- torch.nn.init.ones_(self.weight) # type: ignore
36
-
37
-
38
- def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):
39
- freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
40
- t = torch.arange(end, device=freqs.device) # type: ignore
41
- freqs = torch.outer(t, freqs).float() # type: ignore
42
- freqs_cos = torch.cos(freqs) # real part
43
- freqs_sin = torch.sin(freqs) # imaginary part
44
- return freqs_cos, freqs_sin
45
-
46
-
47
- def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor):
48
- ndim = x.ndim
49
- assert 0 <= 1 < ndim
50
- assert freqs_cis.shape == (x.shape[1], x.shape[-1])
51
- shape = [d if i == 1 or i == ndim - 1 else 1 for i, d in enumerate(x.shape)]
52
- return freqs_cis.view(shape)
53
-
54
-
55
- def apply_rotary_emb(xq: torch.Tensor, xk: torch.Tensor, freqs_cos: torch.Tensor, freqs_sin: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
56
- xq, xk = xq.contiguous(), xk.contiguous()
57
- freqs_cos, freqs_sin = freqs_cos.contiguous(), freqs_sin.contiguous()
58
-
59
- xq_r, xq_i = xq.float().reshape(*xq.shape[:-1], -1, 2).unbind(-1)
60
- xk_r, xk_i = xk.float().reshape(*xk.shape[:-1], -1, 2).unbind(-1)
61
-
62
- seq_len = xq_r.shape[1]
63
- assert seq_len <= freqs_cos.shape[0], (
64
- f"seq_len={seq_len} exceeds precomputed freqs buffer size={freqs_cos.shape[0]}"
65
- )
66
- freqs_cos = freqs_cos[:seq_len]
67
- freqs_sin = freqs_sin[:seq_len]
68
-
69
- freqs_cos = rearrange(freqs_cos, "n c -> 1 n 1 c")
70
- freqs_sin = rearrange(freqs_sin, "n c -> 1 n 1 c")
71
-
72
- # Apply rotation using real numbers
73
- xq_out_r = xq_r * freqs_cos - xq_i * freqs_sin
74
- xq_out_i = xq_r * freqs_sin + xq_i * freqs_cos
75
- xk_out_r = xk_r * freqs_cos - xk_i * freqs_sin
76
- xk_out_i = xk_r * freqs_sin + xk_i * freqs_cos
77
-
78
- # Combine real and imaginary parts
79
- xq_out = torch.stack([xq_out_r, xq_out_i], dim=-1).flatten(-2)
80
- xk_out = torch.stack([xk_out_r, xk_out_i], dim=-1).flatten(-2)
81
-
82
- return xq_out.to(xq.dtype), xk_out.to(xk.dtype)
83
-
84
-
85
- def repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor:
86
- """torch.repeat_interleave(x, dim=2, repeats=n_rep)"""
87
- bs, slen, n_kv_heads, head_dim = x.shape
88
- if n_rep == 1:
89
- return x
90
- return (
91
- x[:, :, :, None, :]
92
- .expand(bs, slen, n_kv_heads, n_rep, head_dim)
93
- .reshape(bs, slen, n_kv_heads * n_rep, head_dim)
94
- )
95
-
96
-
97
-
98
- class FlashAttention(nn.Module):
99
- def __init__(
100
- self,
101
- dim: int,
102
- n_heads: int = 32,
103
- n_kv_heads: Optional[int] = None,
104
- max_seq_len: Optional[int] = None,
105
- dropout: float = 0.0,
106
- is_causal: bool = False,
107
- attn_implementation = None,
108
- ):
109
- super().__init__()
110
-
111
- self.n_heads = n_heads
112
- self.n_kv_heads = n_heads if n_kv_heads is None else n_kv_heads
113
- self.n_rep = self.n_heads // self.n_kv_heads
114
- self.head_dim = dim // n_heads
115
-
116
- self.wq = nn.Linear(dim, n_heads * self.head_dim, bias=False)
117
- self.wk = nn.Linear(dim, self.n_kv_heads * self.head_dim, bias=False)
118
- self.wv = nn.Linear(dim, self.n_kv_heads * self.head_dim, bias=False)
119
- self.wo = nn.Linear(n_heads * self.head_dim, dim, bias=False)
120
-
121
- self.attn_dropout = nn.Dropout(dropout)
122
- self.resid_dropout = nn.Dropout(dropout)
123
- self.dropout = dropout
124
- self.max_seq_len = max_seq_len
125
- self.is_causal = is_causal
126
-
127
- def init_weights(self, init_std: float):
128
- for linear in (self.wq, self.wk, self.wv):
129
- nn.init.trunc_normal_(linear.weight, mean=0.0, std=0.02)
130
- nn.init.trunc_normal_(self.wo.weight, mean=0.0, std=init_std)
131
-
132
-
133
- def forward(
134
- self,
135
- x: torch.Tensor,
136
- freqs_cos: torch.Tensor=None,
137
- freqs_sin: torch.Tensor=None,
138
- mask: torch.Tensor = None,
139
- ):
140
- bsz, seq_len, _ = x.shape
141
- # QKV
142
- xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)
143
-
144
- if self.max_seq_len is None:
145
- xq = xq.view(bsz, seq_len, self.n_heads, self.head_dim)
146
- xk = xk.view(bsz, seq_len, self.n_kv_heads, self.head_dim)
147
- xv = xv.view(bsz, seq_len, self.n_kv_heads, self.head_dim)
148
- else:
149
- nseq = self.max_seq_len // seq_len
150
- xq = xq.view(-1, nseq*seq_len, self.n_heads, self.head_dim)
151
- xk = xk.view(-1, nseq*seq_len, self.n_kv_heads, self.head_dim)
152
- xv = xv.view(bsz, seq_len, self.n_kv_heads, self.head_dim)
153
-
154
- # RoPE relative positional embeddings
155
- if not (freqs_cos is None or freqs_sin is None):
156
- xq, xk = apply_rotary_emb(xq, xk, freqs_cos, freqs_sin)
157
-
158
- if self.max_seq_len is not None:
159
- xq = xq.view(bsz, seq_len, self.n_heads, self.head_dim)
160
- xk = xk.view(bsz, seq_len, self.n_kv_heads, self.head_dim)
161
-
162
- # # grouped multiquery attention: expand out keys and values
163
- # xk = repeat_kv(xk, self.n_rep) # (bs, seq_len, n_heads, head_dim)
164
- # xv = repeat_kv(xv, self.n_rep) # (bs, seq_len, n_heads, head_dim)
165
-
166
- # Repeat KV heads if necessary
167
- if self.n_heads != self.n_kv_heads:
168
- xk = xk.repeat_interleave(self.n_rep, dim=2)
169
- xv = xv.repeat_interleave(self.n_rep, dim=2)
170
-
171
- # make heads into a batch dimension
172
- xq = xq.transpose(1, 2) # (bs, n_heads, seq_len, head_dim)
173
- xk = xk.transpose(1, 2)
174
- xv = xv.transpose(1, 2)
175
-
176
- if mask is not None:
177
- mask = mask.to(xq).unsqueeze(1).contiguous()
178
- nW = mask.shape[0]
179
- if nW < bsz:
180
- mask = mask.repeat([bsz // nW, 1, 1, 1])
181
-
182
- output = F.scaled_dot_product_attention(
183
- xq, xk, xv,
184
- attn_mask=mask,
185
- dropout_p=self.dropout if self.training else 0.0,
186
- is_causal=self.is_causal
187
- )
188
-
189
- # restore time as batch dimension and concat heads
190
- output = output.transpose(1, 2).contiguous().view(bsz, seq_len, -1)
191
-
192
- # final projection into the residual stream
193
- output = self.wo(output)
194
- output = self.resid_dropout(output)
195
- return output
196
-
197
-
198
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/internal/polaris_layers.py DELETED
@@ -1,448 +0,0 @@
1
- # polaris_layers.py
2
- from __future__ import annotations
3
-
4
- import math
5
- from typing import Optional, Tuple, Union, List
6
-
7
- import torch
8
- import torch.nn as nn
9
- import torch.nn.functional as F
10
- from einops import rearrange
11
-
12
- # ---------------------------------------------------------
13
- # 基础归一化与嵌入模块 (保留兼容性)
14
- # ---------------------------------------------------------
15
- class LayerNorm(nn.Module):
16
- def __init__(self, dim, eps=1e-6, elementwise_affine=True):
17
- super().__init__()
18
- self.eps = eps
19
- self.elementwise_affine = elementwise_affine
20
- if self.elementwise_affine:
21
- self.weight = nn.Parameter(torch.ones(dim))
22
-
23
- def _norm(self, x, dim=-1):
24
- u = x.mean(dim, keepdim=True)
25
- s = x.var(dim, keepdim=True)
26
- return (x - u) / torch.sqrt(s + self.eps)
27
-
28
- def forward(self, x):
29
- output = self._norm(x.float()).to(x)
30
- if self.elementwise_affine:
31
- output = output * self.weight
32
- return output
33
-
34
- class RMS_norm(nn.Module):
35
- def __init__(self, dim, eps=1e-6, bias=False):
36
- super().__init__()
37
- self.eps = eps
38
- self.weight = nn.Parameter(torch.ones(dim))
39
- self.bias = nn.Parameter(torch.zeros(dim)) if bias else None
40
-
41
- def forward(self, x):
42
- # normalize over last dim
43
- rms = x.pow(2).mean(dim=-1, keepdim=True).add(self.eps).sqrt()
44
- y = x / rms * self.weight
45
- if self.bias is not None:
46
- y = y + self.bias
47
- return y
48
-
49
- class RMSNorm(nn.Module):
50
- def __init__(self, dim: int, eps: float = 1e-6):
51
- super().__init__()
52
- self.eps = eps
53
- self.weight = nn.Parameter(torch.ones(dim))
54
-
55
- def _norm(self, x, dim=-1):
56
- return x * torch.rsqrt(x.pow(2).mean(dim, keepdim=True) + self.eps)
57
-
58
- def forward(self, x: torch.Tensor):
59
- output = self._norm(x.float()).type_as(x)
60
- return output * self.weight
61
-
62
- class PatchEmbedConv(nn.Module):
63
- def __init__(
64
- self,
65
- in_chans,
66
- out_chans,
67
- patch_size=4,
68
- norm_func=None,
69
- flatten=False,
70
- ):
71
- super().__init__()
72
- self.patch_size = (patch_size, patch_size)
73
- self.flatten = flatten
74
-
75
- # ⭐ 关键:patch_size=1 时保持老行为;否则按 patch_size 下采样
76
- if self.patch_size == (1, 1):
77
- # 保留你原来的设计:kernel_size=(2,1), stride=1
78
- kernel_size = (2, 1)
79
- stride = (1, 1)
80
- padding = (0, 0)
81
- else:
82
- # 正常的 patch 下采样:kernel_size=stride=patch_size
83
- kernel_size = self.patch_size
84
- stride = self.patch_size
85
- padding = (0, 0)
86
-
87
- self.proj = nn.Conv2d(
88
- in_chans,
89
- out_chans,
90
- kernel_size=kernel_size,
91
- stride=stride,
92
- padding=padding,
93
- )
94
- self.norm = norm_func(out_chans) if norm_func else nn.Identity()
95
-
96
- def forward(self, x):
97
- embed = self.proj(x)
98
- if self.flatten:
99
- embed = rearrange(embed, 'n c h w -> n (h w) c')
100
- return self.norm(embed)
101
-
102
- class WeatherLayerNorm(nn.Module):
103
- """
104
- 专为气象 (B, C, H, W) 格式设计的高效 LayerNorm (Channel-first)。
105
- 适用于深层全卷积潜空间演化。
106
- """
107
- def __init__(self, normalized_shape, eps=1e-6):
108
- super().__init__()
109
- self.weight = nn.Parameter(torch.ones(normalized_shape))
110
- self.bias = nn.Parameter(torch.zeros(normalized_shape))
111
- self.eps = eps
112
-
113
- def forward(self, x):
114
- # x: (B, C, H, W)
115
- u = x.mean(1, keepdim=True)
116
- s = (x - u).pow(2).mean(1, keepdim=True)
117
- x = (x - u) / torch.sqrt(s + self.eps)
118
- return self.weight[:, None, None] * x + self.bias[:, None, None]
119
-
120
- class AdaLN(nn.Module):
121
- def __init__(self, dim, embed_dim=None):
122
- super().__init__()
123
- self.norm = LayerNorm(dim, eps=1e-6, elementwise_affine=False)
124
- in_dim = embed_dim if embed_dim else dim
125
- self.scale_shift = nn.Sequential(
126
- nn.SiLU(),
127
- nn.Linear(in_dim, 2 * dim, bias=True),
128
- )
129
-
130
- def forward(self, x, embed):
131
- scale, shift = self.scale_shift(embed).chunk(2, dim=-1)
132
- x = self.norm(x) * (1 + scale[:, None]) + shift[:, None]
133
- return x
134
-
135
- def sincos_embedding(x, embed_dim, max_period=10000, is_periodic=False):
136
- omega = torch.arange(embed_dim//2, dtype=x.dtype, device=x.device)
137
- if is_periodic:
138
- x = 2 * torch.pi * x
139
- else:
140
- omega /= embed_dim / 2.
141
- omega = 1. / max_period ** omega
142
- out = torch.einsum('m,d->md', x.reshape(-1), omega)
143
- emb_sin = torch.sin(out)
144
- emb_cos = torch.cos(out)
145
- emb = torch.cat([emb_sin, emb_cos], dim=1)
146
- return emb
147
-
148
- class TimestepEmbed(nn.Module):
149
- def __init__(self, hidden_size, frequency=256, is_periodic=False, sinusoidal=True, dropout=0.0):
150
- super().__init__()
151
- self.mlp = nn.Sequential(
152
- nn.Linear(frequency, hidden_size),
153
- nn.SiLU(),
154
- nn.Linear(hidden_size, hidden_size),
155
- )
156
- self.frequency = frequency
157
- self.is_periodic = is_periodic
158
- self.sinusoidal = sinusoidal
159
- # Optional output-side dropout. Helps prevent the encoder from
160
- # latching onto specific (hour, doy) combinations when training
161
- # data spans many years and most doy values appear repeatedly.
162
- # ``dropout=0.0`` (default) is a no-op so existing checkpoints
163
- # see identical behaviour.
164
- self.dropout = nn.Dropout(p=dropout) if dropout > 0 else nn.Identity()
165
-
166
- def forward(self, x):
167
- if not self.sinusoidal:
168
- return self.dropout(self.mlp(x))
169
- embed = sincos_embedding(
170
- x.float(), self.frequency, is_periodic=self.is_periodic
171
- ).type_as(x)
172
- embed = self.mlp(embed)
173
- return self.dropout(embed)
174
-
175
- # ---------------------------------------------------------
176
- # 气象定制:潜空间大核卷积 Block
177
- # ---------------------------------------------------------
178
- class GlobalWeatherConvBlock(nn.Module):
179
- """
180
- 经度循环大核 CNN Block,专为地球流体物理推演设计。
181
- 替代原有的 Window Attention,解决网格伪影并大幅提升感受野。
182
- """
183
- def __init__(self, dim: int, drop_path: float = 0.0):
184
- super().__init__()
185
- # 7x7 大核深度可分离卷积
186
- self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, groups=dim, bias=False)
187
- self.norm = WeatherLayerNorm(dim)
188
-
189
- # Inverted Bottleneck: 1x1 卷积放大 4 倍通道进行特征融合
190
- self.pwconv1 = nn.Conv2d(dim, 4 * dim, kernel_size=1)
191
- self.act = nn.GELU()
192
- self.pwconv2 = nn.Conv2d(4 * dim, dim, kernel_size=1)
193
-
194
- self.drop_path = nn.Dropout(drop_path) if drop_path > 0. else nn.Identity()
195
-
196
- def forward(self, x):
197
- shortcut = x
198
-
199
- x = F.pad(x, pad=(3, 3, 3, 3), mode='replicate')
200
-
201
- x = x.contiguous()
202
-
203
- x = self.dwconv(x)
204
- x = self.norm(x)
205
- x = self.pwconv1(x)
206
- x = self.act(x)
207
- x = self.pwconv2(x)
208
-
209
- return shortcut + self.drop_path(x)
210
-
211
- # ---------------------------------------------------------
212
- # Encoder: 单帧气象特征提取网络
213
- # ---------------------------------------------------------
214
- class CubeEmbedConv(nn.Module):
215
- """
216
- 重构后的单帧推演 Encoder。
217
- 包含三阶段:前置抗混叠 -> Patch下采样 -> 潜空间大核深层演化。
218
- """
219
- def __init__(
220
- self,
221
- in_chans: int = 70,
222
- out_chans: int = 2048,
223
- in_frames: int = 1, # 现固定为单帧输入
224
- patch_size: int = 6, # 默认 6 倍下采样 (120x240)
225
- depth: int = 2, # 潜空间推演层数 (建议 8-16)
226
- flatten: bool = False,
227
- norm_func: Optional[nn.Module] = None,
228
- # 兼容旧签名的冗余参数,防止外部调用报错
229
- temporal_pad_to: int = 2,
230
- attn_heads: int = 8,
231
- attn_dropout: float = 0.0,
232
- window_size: int = 8,
233
- keep_time_dim: bool = False,
234
- feat_chans: int = 2048,
235
- ):
236
- super().__init__()
237
- self.in_chans = in_chans
238
- self.out_chans = out_chans
239
- self.flatten = flatten
240
-
241
- # 阶段一:高分辨率前置特征融合 (抗混叠,避免下采样丢失高频气象细节)
242
- inter_chans = 256
243
- self.stage1_smooth = nn.Sequential(
244
- nn.Conv2d(in_chans, inter_chans, kernel_size=7, stride=1, padding=3, padding_mode='replicate'),
245
- WeatherLayerNorm(inter_chans),
246
- nn.GELU()
247
- )
248
-
249
- # 阶段二:Patch 强力下采样 (无重叠,直接拉升到目标潜空间通道数)
250
- self.stage2_patchify = nn.Sequential(
251
- nn.Conv2d(inter_chans, out_chans, kernel_size=patch_size, stride=patch_size),
252
- WeatherLayerNorm(out_chans)
253
- )
254
-
255
- # 阶段三:潜空间大核全局演化 (替代 Window Attention)
256
- self.stage3_evolution = nn.Sequential(
257
- *[GlobalWeatherConvBlock(dim=out_chans) for _ in range(depth)]
258
- )
259
-
260
- self.norm = norm_func(out_chans) if norm_func is not None else nn.Identity()
261
-
262
- def forward(
263
- self,
264
- x: torch.Tensor,
265
- lead_hour: Optional[int] = None, # 兼容签名
266
- lead_hour_nums: Optional[int] = None, # 兼容签名
267
- ) -> torch.Tensor:
268
- """
269
- x: (B, T, C, H, W) 或 (B, C, H, W). 期望 T==1。
270
- """
271
- # 1. 处理维度,兼容 T 维度
272
- if x.ndim == 5:
273
- assert x.shape[1] == 1, f"CubeEmbedConv expects single frame (T=1), got {x.shape}"
274
- x = x.squeeze(1) # 转换为 (B, C, H, W)
275
-
276
- # print("x0",x.shape)
277
- # 2. 三阶段推演
278
- x = self.stage1_smooth(x)
279
- # print("x1",x.shape)
280
- x = self.stage2_patchify(x)
281
- # print("x2",x.shape)
282
- z = self.stage3_evolution(x)
283
- # print("z",z.shape)
284
-
285
- # 3. 输出格式化
286
- if self.flatten:
287
- z_tok = rearrange(z, "b c h w -> b (h w) c")
288
- return self.norm(z_tok)
289
-
290
- return self.norm(z)
291
-
292
- # ---------------------------------------------------------
293
- # Decoder: 气象场重构网络
294
- # ---------------------------------------------------------
295
- def remove_small_scales(x, scale_factor=0.5, mode="bilinear", random_scale=False):
296
- import numpy as np
297
- if random_scale:
298
- scale_factor = np.random.choice([1 / s for s in range(1, 13)])
299
- if scale_factor < 1:
300
- down = F.interpolate(x, scale_factor=scale_factor, mode=mode, align_corners=False)
301
- x = F.interpolate(down, size=x.shape[-2:], mode=mode, align_corners=False)
302
- return x
303
-
304
- class SmoothDeconv(nn.Module):
305
- """
306
- 数学精确匹配 patch_size 的平滑反卷积。
307
- 彻底修复原版本中存在的空间偏移(Phase Shift)和形状裁切问题。
308
- """
309
- def __init__(
310
- self,
311
- in_channels: int,
312
- out_channels: int,
313
- kernel_size: Union[int, tuple],
314
- stride: Union[int, tuple],
315
- bias: bool = True,
316
- ):
317
- super().__init__()
318
- # 直接使用与 stride 完全相等的 kernel_size,避免 checkerboard artifacts
319
- self.deconv = nn.ConvTranspose2d(
320
- in_channels,
321
- out_channels,
322
- kernel_size=kernel_size,
323
- stride=stride,
324
- padding=0, # 完美对齐,不需要人为 padding
325
- output_padding=0,
326
- bias=bias
327
- )
328
- # 可选:后接一层普通卷积做进一步平滑
329
- self.smooth = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, padding_mode='replicate')
330
-
331
- def forward(self, x, output_size=None):
332
- x = self.deconv(x)
333
- x = self.smooth(x)
334
- # 如果有极微小的边界舍入误差,进行安全裁切
335
- if output_size is not None and x.shape[-2:] != output_size:
336
- x = x[..., :output_size[0], :output_size[1]]
337
- return x
338
-
339
-
340
- class ProgressiveUpsample(nn.Module):
341
- """
342
- 阶梯式渐进上采样模块。
343
- 完美平衡“信息保留”与“防显存溢出(OOM)”的矛盾,严格遵循流体力学的连续性。
344
- """
345
- def __init__(self, in_channels: int, out_channels: int):
346
- super().__init__()
347
-
348
- # 阶段一:空间放大 2 倍,通道 2048 -> 512
349
- self.stage1_conv = nn.Sequential(
350
- nn.Conv2d(in_channels, 512, kernel_size=3, padding=1, padding_mode='replicate'),
351
- nn.GELU()
352
- )
353
-
354
- # 阶段二:空间放大 3 倍,通道 512 -> 128
355
- self.stage2_conv = nn.Sequential(
356
- nn.Conv2d(512, 128, kernel_size=3, padding=1, padding_mode='replicate'),
357
- nn.GELU()
358
- )
359
-
360
- # 最终映射:128 -> 具体的物理变量通道数 (如 upper_chans)
361
- self.head = nn.Conv2d(128, out_channels, kernel_size=3, padding=1, padding_mode='replicate')
362
-
363
- def forward(self, x, target_size=(721, 1440)):
364
- # x 初始状态: (B, 2048, 120, 240)
365
-
366
- # --- Stage 1: 先插值放大空间,再卷积融合降维 (保留高频信息) ---
367
- x = F.interpolate(x, scale_factor=2.0, mode='bilinear', align_corners=False)
368
- x = self.stage1_conv(x) # 此时形状: (B, 512, 240, 480)
369
-
370
- # --- Stage 2: 再次插值放大空间,再降维 ---
371
- x = F.interpolate(x, scale_factor=3.0, mode='bilinear', align_corners=False)
372
- x = self.stage2_conv(x) # 此时形状: (B, 128, 720, 1440)
373
-
374
- # --- 解决 720 与 721 的地球极点拓扑问题 ---
375
- current_h, current_w = x.shape[-2:]
376
- target_h, target_w = target_size
377
-
378
- if current_h != target_h or current_w != target_w:
379
- pad_h = target_h - current_h # 721 - 720 = 1
380
- pad_w = target_w - current_w # 1440 - 1440 = 0
381
- x = F.pad(x, (0, pad_w, 0, pad_h), mode='replicate')
382
-
383
- # --- 最终映射输出 ---
384
- x = self.head(x)
385
- return x
386
-
387
-
388
- class DoubleDeconvHead(nn.Module):
389
- def __init__(
390
- self,
391
- in_chans: int,
392
- upper_chans: int,
393
- lower_chans: int,
394
- patch_size: int,
395
- ):
396
- super().__init__()
397
- self.lower_chans = lower_chans
398
- self.upper_chans = upper_chans
399
-
400
- # 换用阶梯式渐进上采样
401
- self.upper_head = ProgressiveUpsample(in_channels=in_chans, out_channels=upper_chans)
402
-
403
- if self.lower_chans > 0:
404
- self.lower_head = ProgressiveUpsample(in_channels=in_chans, out_channels=lower_chans)
405
-
406
- def forward(self, h, residual=None, input_size=(721, 1440), lead_hour=None, target_frames: int = 1):
407
-
408
- # ==========================================
409
- # 🚨 修复 1:安全处理 Batch Tensor 级别的 lead_hour
410
- # ==========================================
411
- if lead_hour is not None:
412
- # lead_hour shape: (B,)
413
- # 等价于 max(lead_hour // 6, 1) 的 Tensor 写法
414
- lead_step = torch.clamp(lead_hour // 6, min=1.0)
415
- # 广播到 (B, 1, 1, 1) 以便与图像特征相乘
416
- lead_step = lead_step.view(-1, 1, 1, 1).to(h.dtype)
417
- else:
418
- lead_step = 1.0
419
-
420
- # ==========================================
421
- # 2. 解码预测变化量 (Delta)
422
- # ==========================================
423
- pred = self.upper_head(h, target_size=input_size)
424
- output = pred * lead_step
425
-
426
- if self.lower_chans > 0:
427
- lower = self.lower_head(h, target_size=input_size)
428
- # 如果下层也需要乘时间步,取消下面的注释:
429
- # lower = lower * lead_step
430
- output = torch.cat([output, lower], dim=1)
431
-
432
- # ==========================================
433
- # 🚨 修复 2:残差相加必须在 FP32 精度下进行!
434
- # ==========================================
435
- if residual is not None:
436
-
437
- res_frame = residual[:, -1] if residual.ndim == 5 else residual
438
- res_frame = remove_small_scales(res_frame)
439
-
440
-
441
- # 强制提升至 FP32 抵抗 bf16 的精度吞噬
442
- output = output.to(torch.float32)
443
- res_frame = res_frame.to(torch.float32)
444
-
445
- output = output + res_frame
446
-
447
- # 返回 fp32 结果,直接交给外面的 MSE Loss 计算(Loss也需要 fp32 保证稳定)
448
- return output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/internal/polaris_swin.py DELETED
@@ -1,527 +0,0 @@
1
- import numpy as np
2
- import torch
3
- import torch.nn as nn
4
- import torch.nn.functional as F
5
- from .polaris_attention import FlashAttention
6
- from .polaris_layers import LayerNorm, RMSNorm
7
- from .helpers import to_2tuple
8
-
9
- __all__ = ["SwinBlock", "GeGLU_FFN"]
10
-
11
-
12
- class GeGLU(nn.Module):
13
- def forward(self, x):
14
- x, gate = x.chunk(2, dim = -1)
15
- return F.gelu(gate) * x
16
-
17
- class GeGLU_FFN(nn.Module):
18
- def __init__(
19
- self,
20
- dim,
21
- hidden_dim=None,
22
- multiple_of: int = 32,
23
- dropout=0,
24
- ):
25
- super().__init__()
26
- if hidden_dim is None:
27
- hidden_dim = 4 * dim
28
- hidden_dim = int(2 * hidden_dim / 3)
29
- hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
30
- self.fc1 = nn.Linear(dim, hidden_dim * 2, bias=False)
31
- self.act = GeGLU()
32
- self.fc2 = nn.Linear(hidden_dim, dim, bias=False)
33
- self.drop = nn.Dropout(dropout)
34
-
35
- def init_weights(self, init_std: float):
36
- nn.init.trunc_normal_(self.fc1.weight, mean=0.0, std=0.02)
37
- nn.init.trunc_normal_(self.fc2.weight, mean=0.0, std=init_std)
38
-
39
- def forward(self, x):
40
- return self.drop(self.fc2(self.act(self.fc1(x))))
41
-
42
-
43
- class SwiGLU_FFN(nn.Module):
44
- def __init__(
45
- self,
46
- dim: int,
47
- hidden_dim = None,
48
- multiple_of: int = 32,
49
- dropout: float = 0.0,
50
- ):
51
- super().__init__()
52
- if hidden_dim is None:
53
- hidden_dim = 4 * dim
54
- hidden_dim = int(2 * hidden_dim / 3)
55
- hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
56
- self.w1 = nn.Linear(dim, hidden_dim, bias=False)
57
- self.w2 = nn.Linear(hidden_dim, dim, bias=False)
58
- self.w3 = nn.Linear(dim, hidden_dim, bias=False)
59
- self.dropout = nn.Dropout(dropout)
60
-
61
- def forward(self, x):
62
- return self.dropout(self.w2(F.silu(self.w1(x)) * self.w3(x)))
63
-
64
- def init_weights(self, init_std: float):
65
- nn.init.trunc_normal_(self.w1.weight, mean=0.0, std=0.02)
66
- for linear in (self.w2, self.w3):
67
- nn.init.trunc_normal_(linear.weight, mean=0.0, std=init_std)
68
-
69
-
70
- def window_partition(x, window_size):
71
- """
72
- Args:
73
- x: (B, H, W, C)
74
- window_size: (win_h, win_w)
75
-
76
- Returns:
77
- windows: (num_windows*B, win_h, win_w, C)
78
- """
79
- B, H, W, C = x.shape
80
- x = x.view(B, H // window_size[0], window_size[0], W // window_size[1], window_size[1], C)
81
- windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size[0], window_size[1], C)
82
- return windows
83
-
84
-
85
- def window_reverse(windows, window_size, H, W):
86
- """
87
- Args:
88
- windows: (num_windows*B, window_size, window_size, C)
89
- window_size: (win_h, win_w)
90
- H (int): Height of image
91
- W (int): Width of image
92
-
93
- Returns:
94
- x: (B, H, W, C)
95
- """
96
- B = int(windows.shape[0] / (H * W / window_size[0] / window_size[1]))
97
- x = windows.view(B, H // window_size[0], W // window_size[1], window_size[0], window_size[1], -1)
98
- x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1)
99
- return x
100
-
101
-
102
-
103
- class WindowAttention(nn.Module):
104
- r""" Window based multi-head self attention (W-MSA) module with relative position bias.
105
- It supports both of shifted and non-shifted window.
106
-
107
- Args:
108
- dim (int): Number of input channels.
109
- window_size (tuple[int]): The height and width of the window.
110
- num_heads (int): Number of attention heads.
111
- qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True
112
- qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set
113
- attn_drop (float, optional): Dropout ratio of attention weight. Default: 0.0
114
- proj_drop (float, optional): Dropout ratio of output. Default: 0.0
115
- """
116
-
117
- def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, attn_drop=0., proj_drop=0.):
118
-
119
- super().__init__()
120
- self.dim = dim
121
- self.window_size = window_size # Wh, Ww
122
- self.num_heads = num_heads
123
- head_dim = dim // num_heads
124
- self.scale = qk_scale or head_dim ** -0.5
125
-
126
- # define a parameter table of relative position bias
127
- self.relative_position_bias_table = nn.Parameter(
128
- torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads)) # 2*Wh-1 * 2*Ww-1, nH
129
-
130
- # get pair-wise relative position index for each token inside the window
131
- coords_h = torch.arange(self.window_size[0])
132
- coords_w = torch.arange(self.window_size[1])
133
- coords = torch.stack(torch.meshgrid([coords_h, coords_w], indexing='ij')) # 2, Wh, Ww
134
- coords_flatten = torch.flatten(coords, 1) # 2, Wh*Ww
135
- relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww
136
- relative_coords = relative_coords.permute(1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2
137
- relative_coords[:, :, 0] += self.window_size[0] - 1 # shift to start from 0
138
- relative_coords[:, :, 1] += self.window_size[1] - 1
139
- relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1
140
- relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww
141
- self.register_buffer("relative_position_index", relative_position_index)
142
-
143
- self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
144
- self.attn_drop = nn.Dropout(attn_drop)
145
- self.proj = nn.Linear(dim, dim)
146
- self.proj_drop = nn.Dropout(proj_drop)
147
- nn.init.trunc_normal_(self.relative_position_bias_table, std=.02)
148
-
149
-
150
- def forward(self, x, mask=None, **kwargs):
151
- """
152
- Args:
153
- x: input features with shape of (num_windows*B, N, C)
154
- mask: (0/-inf) mask with shape of (num_windows, Wh*Ww, Wh*Ww) or None
155
- """
156
- B_, N, C = x.shape
157
-
158
- qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
159
- q, k, v = qkv[0], qkv[1], qkv[2] # make torchscript happy (cannot use tensor as tuple)
160
-
161
- q = q * self.scale
162
- attn = (q @ k.transpose(-2, -1))
163
-
164
- relative_position_bias = self.relative_position_bias_table[self.relative_position_index.view(-1)].view(
165
- self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1) # Wh*Ww,Wh*Ww,nH
166
- relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous() # nH, Wh*Ww, Wh*Ww
167
- attn = attn + relative_position_bias.unsqueeze(0)
168
-
169
- if mask is not None:
170
- nW = mask.shape[0]
171
- attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0)
172
- attn = attn.view(-1, self.num_heads, N, N)
173
- attn = attn.softmax(dim=-1)
174
- else:
175
- attn = attn.softmax(dim=-1)
176
-
177
- attn = self.attn_drop(attn)
178
-
179
- x = (attn @ v).transpose(1, 2).reshape(B_, N, C)
180
- x = self.proj(x)
181
- x = self.proj_drop(x)
182
- return x
183
-
184
-
185
-
186
- class WindowAttentionV2(nn.Module):
187
- r""" Window based multi-head self attention (W-MSA) module with relative position bias.
188
- It supports both of shifted and non-shifted window.
189
- Args:
190
- dim (int): Number of input channels.
191
- window_size (tuple[int]): The height and width of the window.
192
- num_heads (int): Number of attention heads.
193
- qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True
194
- attn_drop (float, optional): Dropout ratio of attention weight. Default: 0.0
195
- proj_drop (float, optional): Dropout ratio of output. Default: 0.0
196
- """
197
-
198
- def __init__(self, dim, window_size, num_heads, attn_drop=0., proj_drop=0.):
199
-
200
- super().__init__()
201
- self.dim = dim
202
- self.window_size = window_size # Wh, Ww
203
- self.num_heads = num_heads
204
-
205
- self.logit_scale = nn.Parameter(torch.log(10 * torch.ones((num_heads, 1, 1))), requires_grad=True)
206
-
207
- # mlp to generate continuous relative position bias
208
- self.cpb_mlp = nn.Sequential(nn.Linear(2, 512, bias=True),
209
- nn.ReLU(inplace=True),
210
- nn.Linear(512, num_heads, bias=False))
211
-
212
- # get relative_coords_table
213
- relative_coords_h = torch.arange(-(self.window_size[0] - 1), self.window_size[0], dtype=torch.float32)
214
- relative_coords_w = torch.arange(-(self.window_size[1] - 1), self.window_size[1], dtype=torch.float32)
215
-
216
- relative_coords_table = torch.stack(
217
- torch.meshgrid([relative_coords_h,relative_coords_w], indexing='ij')
218
- ).permute(1, 2, 0).contiguous().unsqueeze(0) # 1, 2*Wh-1, 2*Ww-1, 2
219
-
220
-
221
- relative_coords_table[:, :, :, 0] /= (self.window_size[0] - 1)
222
- relative_coords_table[:, :, :, 1] /= (self.window_size[1] - 1)
223
- relative_coords_table *= 8 # normalize to -8, 8
224
-
225
- relative_coords_table = torch.sign(relative_coords_table) * torch.log2(
226
- torch.abs(relative_coords_table) + 1.0) / np.log2(8)
227
-
228
- self.register_buffer("relative_coords_table", relative_coords_table)
229
-
230
- # get pair-wise relative position index for each token inside the window
231
- coords_h = torch.arange(self.window_size[0])
232
- coords_w = torch.arange(self.window_size[1])
233
- coords = torch.stack(torch.meshgrid([coords_h, coords_w], indexing='ij')) # 2, Wh, Ww
234
- coords_flatten = torch.flatten(coords, 1) # 2, Wh*Ww
235
- relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww
236
- relative_coords = relative_coords.permute(1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2
237
- relative_coords[:, :, 0] += self.window_size[0] - 1 # shift to start from 0
238
- relative_coords[:, :, 1] += self.window_size[1] - 1
239
- relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1
240
- relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww
241
- self.register_buffer("relative_position_index", relative_position_index)
242
-
243
-
244
- self.qkv = nn.Linear(dim, dim * 3, bias=False)
245
- self.q_bias = nn.Parameter(torch.zeros(dim))
246
- self.v_bias = nn.Parameter(torch.zeros(dim))
247
- self.register_buffer("k_bias", torch.zeros(dim).half())
248
- # self.register_buffer('k_bias', torch.zeros(dim), persistent=False)
249
-
250
- self.attn_drop = nn.Dropout(attn_drop)
251
- self.out_proj = nn.Linear(dim, dim)
252
- self.proj_drop = nn.Dropout(proj_drop)
253
- self.softmax = nn.Softmax(dim=-1)
254
-
255
-
256
- def forward(self, x, mask=None, **kwargs):
257
- """
258
- Args:
259
- x: input features with shape of (num_windows*B, N, C)
260
- mask: (0/-inf) mask with shape of (num_windows, Wh*Ww, Wh*Ww) or None
261
- """
262
- B_, N, C = x.shape
263
-
264
- k_bias = self.k_bias.to(self.q_bias)
265
- qkv_bias = torch.cat([self.q_bias, k_bias, self.v_bias])
266
- # qkv_bias = torch.cat((self.q_bias, self.k_bias, self.v_bias))
267
-
268
- qkv = F.linear(input=x, weight=self.qkv.weight, bias=qkv_bias)
269
- qkv = qkv.reshape(B_, N, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4)
270
- q, k, v = qkv[0], qkv[1], qkv[2] # make torchscript happy (cannot use tensor as tuple)
271
-
272
- # cosine attention
273
- attn = (F.normalize(q, dim=-1) @ F.normalize(k, dim=-1).transpose(-2, -1))
274
-
275
- logit_scale = self.logit_scale.clamp(max=np.log(1. / 0.01)).exp()
276
- attn = attn * logit_scale
277
-
278
- relative_position_bias_table = self.cpb_mlp(self.relative_coords_table.to(x)).view(-1, self.num_heads)
279
- relative_position_bias = relative_position_bias_table[self.relative_position_index.view(-1)].view(
280
- self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1) # Wh*Ww,Wh*Ww,nH
281
- relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous() # nH, Wh*Ww, Wh*Ww
282
- relative_position_bias = 16 * torch.sigmoid(relative_position_bias)
283
-
284
- attn = attn + relative_position_bias.unsqueeze(0)
285
-
286
-
287
- if mask is not None:
288
- nW = mask.shape[0]
289
- attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0)
290
- attn = attn.view(-1, self.num_heads, N, N)
291
- attn = self.softmax(attn)
292
- else:
293
- attn = self.softmax(attn)
294
-
295
- attn = self.attn_drop(attn).to(v)
296
-
297
- x = (attn @ v).transpose(1, 2).reshape(B_, N, C)
298
- x = self.out_proj(x)
299
- x = self.proj_drop(x)
300
- return x
301
-
302
- def _compute_attn_mask(H, W, window_size, shift_size, mask_type):
303
- img_mask = torch.zeros((1, H, W, 1))
304
- h_slices = (slice(0, -window_size[0]),
305
- slice(-window_size[0], -shift_size[0]),
306
- slice(-shift_size[0], None))
307
- w_slices = (slice(0, -window_size[1]),
308
- slice(-window_size[1], -shift_size[1]),
309
- slice(-shift_size[1], None))
310
- cnt = 0
311
- for h in h_slices:
312
- for w in w_slices:
313
- if mask_type == 'h':
314
- img_mask[:, h, :, :] = cnt
315
- elif mask_type == 'w':
316
- img_mask[:, :, w, :] = cnt
317
- elif mask_type == 'hw':
318
- img_mask[:, h, w, :] = cnt
319
- cnt += 1
320
- mask_windows = window_partition(img_mask, window_size)
321
- mask_windows = mask_windows.view(-1, window_size[0] * window_size[1])
322
- attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2)
323
- attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0))
324
- return attn_mask
325
-
326
-
327
- from transformers.modeling_layers import GradientCheckpointingLayer
328
- class SwinBlock(GradientCheckpointingLayer):
329
- def __init__(
330
- self, dim, num_heads,
331
- input_size, window_size=7, shift_size=0, embed_dim=None,
332
- attn_type='v1', mask_type='hw', norm_type="ln", ffn_type="geglu_ffn",
333
- qk_scale=None, n_kv_heads=None,
334
- mlp_ratio=4., drop=0., attn_drop=0.,
335
- attn_implementation="flash_attention_2",
336
- **kwargs
337
- ):
338
- super().__init__()
339
- if embed_dim is None:
340
- embed_dim = dim
341
- self.dim = dim
342
- self.default_input_size = tuple(int(s) for s in input_size)
343
- self.num_heads = num_heads
344
- self.window_size = to_2tuple(window_size)
345
- self.shift_size = to_2tuple(shift_size)
346
- self.mlp_ratio = mlp_ratio
347
- self.norm_type = norm_type
348
- self.attn_type = attn_type
349
- self.ffn_type = ffn_type
350
- self.mask_type = mask_type
351
- self._attn_implementation = attn_implementation
352
-
353
- default_full_window = all(
354
- w == s for w, s in zip(self.window_size, self.default_input_size)
355
- )
356
-
357
- assert 0 <= self.shift_size[0] < self.window_size[0], "shift_size must in 0-window_size"
358
- assert 0 <= self.shift_size[1] < self.window_size[1], "shift_size must in 0-window_size"
359
-
360
- if norm_type == "ln":
361
- self.norm1 = LayerNorm(dim, eps=1e-6)
362
- self.norm2 = LayerNorm(dim, eps=1e-6)
363
- elif norm_type == "adaln":
364
- self.norm1 = LayerNorm(dim, eps=1e-6, elementwise_affine=False)
365
- self.norm2 = LayerNorm(dim, eps=1e-6, elementwise_affine=False)
366
- adaln_linear = nn.Linear(embed_dim, 6 * dim, bias=True)
367
- nn.init.zeros_(adaln_linear.weight)
368
- nn.init.zeros_(adaln_linear.bias)
369
- self.adaln = nn.Sequential(nn.SiLU(), adaln_linear)
370
- elif norm_type == "adarms":
371
- self.norm1 = RMSNorm(dim, eps=1e-5)
372
- self.norm2 = RMSNorm(dim, eps=1e-5)
373
- adaln_linear = nn.Linear(embed_dim, 4 * dim, bias=True)
374
- nn.init.zeros_(adaln_linear.weight)
375
- nn.init.zeros_(adaln_linear.bias)
376
- self.adaln = nn.Sequential(nn.SiLU(), adaln_linear)
377
- else:
378
- raise ValueError(f"norm_type {norm_type} not supported")
379
-
380
- if attn_type == 'v1':
381
- self.attn = WindowAttention(
382
- dim, window_size=self.window_size, num_heads=num_heads,
383
- qk_scale=qk_scale, attn_drop=attn_drop, proj_drop=drop)
384
- elif attn_type == 'v2':
385
- self.attn = WindowAttentionV2(
386
- dim, window_size=self.window_size, num_heads=num_heads,
387
- attn_drop=attn_drop, proj_drop=drop)
388
- elif attn_type == "flash":
389
- self.attn = FlashAttention(
390
- dim=dim,
391
- n_heads=num_heads,
392
- n_kv_heads=n_kv_heads,
393
- attn_implementation=self._attn_implementation,
394
- dropout=attn_drop,
395
- is_causal=False,
396
- max_seq_len=None if default_full_window else int(np.prod(self.default_input_size)),
397
- )
398
- else:
399
- raise ValueError(f"attn_type {attn_type} not supported")
400
-
401
-
402
- if ffn_type == "geglu_ffn":
403
- self.mlp = GeGLU_FFN(dim)
404
- elif ffn_type == "swiglu_ffn":
405
- self.mlp = SwiGLU_FFN(dim)
406
- else:
407
- raise ValueError(f"ffn_type {ffn_type} not supported")
408
-
409
- if not default_full_window and max(self.shift_size) > 0:
410
- H, W = self.default_input_size
411
- attn_mask = _compute_attn_mask(H, W, self.window_size, self.shift_size, mask_type)
412
- else:
413
- attn_mask = None
414
- self.register_buffer("attn_mask", attn_mask, persistent=False)
415
-
416
- self._attn_mask_cache = {}
417
-
418
- def init_weights(self, init_std):
419
- for norm in (self.norm1, self.norm2):
420
- norm.reset_parameters()
421
- self.attn.init_weights(init_std)
422
- self.mlp.init_weights(init_std)
423
-
424
- def _get_attn_mask(self, H, W, device):
425
- if max(self.shift_size) == 0:
426
- return None
427
- if (H, W) == tuple(self.default_input_size):
428
- return self.attn_mask
429
- key = (H, W)
430
- if key not in self._attn_mask_cache:
431
- self._attn_mask_cache[key] = _compute_attn_mask(
432
- H, W, self.window_size, self.shift_size, self.mask_type
433
- )
434
- return self._attn_mask_cache[key].to(device)
435
-
436
- def _resolve_input_size(self, L):
437
- if L == self.default_input_size[0] * self.default_input_size[1]:
438
- return self.default_input_size
439
- wh, ww = self.window_size
440
- sqrt_l = int(L ** 0.5)
441
- for H in range(sqrt_l, 0, -1):
442
- if L % H != 0:
443
- continue
444
- W = L // H
445
- if H % wh == 0 and W % ww == 0:
446
- return (H, W)
447
- raise ValueError(
448
- f"Cannot resolve seq_len={L} into (H, W) divisible by window_size={self.window_size}"
449
- )
450
-
451
- def window_attention(self, x, freqs_cos, freqs_sin, input_size):
452
- H, W = input_size
453
- B, L, C = x.shape
454
-
455
- x = x.view(B, H, W, C)
456
-
457
- if max(self.shift_size) > 0:
458
- shifted_x = torch.roll(x, shifts=(-self.shift_size[0], -self.shift_size[1]), dims=(1, 2))
459
- else:
460
- shifted_x = x
461
-
462
- x_windows = window_partition(shifted_x, self.window_size)
463
- x_windows = x_windows.view(-1, self.window_size[0] * self.window_size[1], C)
464
-
465
- attn_mask = self._get_attn_mask(H, W, x.device)
466
-
467
- if self.attn_type == "flash" and (H, W) != tuple(self.default_input_size):
468
- old_max_seq = self.attn.max_seq_len
469
- self.attn.max_seq_len = None
470
- attn_windows = self.attn(x_windows, freqs_cos=freqs_cos, freqs_sin=freqs_sin, mask=attn_mask)
471
- self.attn.max_seq_len = old_max_seq
472
- else:
473
- attn_windows = self.attn(x_windows, freqs_cos=freqs_cos, freqs_sin=freqs_sin, mask=attn_mask)
474
-
475
- attn_windows = attn_windows.view(-1, self.window_size[0], self.window_size[1], C)
476
- shifted_x = window_reverse(attn_windows, self.window_size, H, W)
477
-
478
- if max(self.shift_size) > 0:
479
- x = torch.roll(shifted_x, shifts=(self.shift_size[0], self.shift_size[1]), dims=(1, 2))
480
- else:
481
- x = shifted_x
482
-
483
- x = x.view(B, H * W, C)
484
- return x
485
-
486
-
487
- def forward(
488
- self,
489
- x: torch.Tensor,
490
- embed: torch.Tensor=None,
491
- freqs_cos: torch.Tensor=None,
492
- freqs_sin: torch.Tensor=None,
493
- **kwargs,
494
- ):
495
- B, L, C = x.shape
496
- input_size = self._resolve_input_size(L)
497
- is_full_window = all(w == s for w, s in zip(self.window_size, input_size))
498
-
499
- shortcut = x
500
-
501
- if self.norm_type == "adaln":
502
- gamma = self.adaln(embed).unsqueeze(1)
503
- scale_msa, shift_msa, gate_msa, scale_mlp, shift_mlp, gate_mlp = gamma.chunk(6, dim=-1)
504
- x = self.norm1(x) * (1 + scale_msa) + shift_msa
505
- elif self.norm_type == "adarms":
506
- scale_msa, gate_msa, scale_mlp, gate_mlp = self.adaln(embed).unsqueeze(1).chunk(4, dim=-1)
507
- x = self.norm1(x) * (1 + scale_msa)
508
- else:
509
- x = self.norm1(x)
510
-
511
- if is_full_window:
512
- x = self.attn(x, freqs_cos, freqs_sin)
513
- else:
514
- x = self.window_attention(x, freqs_cos, freqs_sin, input_size)
515
-
516
- if self.norm_type == "adaln":
517
- x = shortcut + gate_msa * x
518
- x = x + gate_mlp*self.mlp(self.norm2(x)*(1+scale_mlp)+shift_mlp)
519
- elif self.norm_type == "adarms":
520
- x = shortcut + gate_msa * x
521
- x = x + gate_mlp*self.mlp(self.norm2(x)*(1+scale_mlp))
522
- else:
523
- x = shortcut + x
524
- x = x + self.mlp(self.norm2(x))
525
-
526
- return x
527
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/processor.py DELETED
@@ -1,32 +0,0 @@
1
- """Weather processor (placeholder).
2
-
3
- The bio modalities use a per-modality processor to turn raw inputs into
4
- encoder tensors. Weather is different: the ``ERA5QwenVLDataset`` (in
5
- ``qwenvl/modalities/weather/data/era5_dataset.py``) directly emits all
6
- encoder tensors plus the chat-template placeholder. This file exists
7
- only so that ``BaseProcessor``-style callers — if any — see a concrete
8
- class for the modality.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- from typing import Any, Dict, Optional, Tuple
14
-
15
-
16
- class WeatherProcessor:
17
- """No-op processor; the dataset class does the real work."""
18
-
19
- modality_name = "weather"
20
-
21
- def process_input(self, raw_input: Any, **kwargs) -> Dict[str, Any]:
22
- raise NotImplementedError(
23
- "Weather inputs come straight from ERA5QwenVLDataset; the dataset "
24
- "produces encoder-ready tensors directly."
25
- )
26
-
27
- def build_placeholder(
28
- self, raw_input: Any, is_output: bool = False, **kwargs,
29
- ) -> Tuple[str, Optional[Any]]:
30
- raise NotImplementedError(
31
- "Use the chat-template helper in qwenvl/modalities/weather/data/era5_dataset.py."
32
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/qwenvl/modalities/weather/projector.py DELETED
@@ -1,29 +0,0 @@
1
- """Identity projector for the weather modality.
2
-
3
- The Polaris-style ``meteo_merger`` already runs inside ``WeatherEncoder``
4
- and projects swin hidden → ``qwenvl_dim``. The router still expects a
5
- projector module so we register a no-op ``IdentityWeatherProjector`` to
6
- keep the registration logic uniform with the bio modalities.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import torch
12
- import torch.nn as nn
13
-
14
-
15
- class IdentityWeatherProjector(nn.Module):
16
- """Pass-through projector. Encoder already outputs qwenvl_dim."""
17
-
18
- def __init__(self, qwenvl_dim: int):
19
- super().__init__()
20
- self.qwenvl_dim = qwenvl_dim
21
- # Single dummy parameter so that an "is the projector trainable"
22
- # check does not return False unexpectedly when tune_weather_projector
23
- # is left at its default True; the router treats projectors with
24
- # no trainable params as frozen which would short-circuit the
25
- # dummy_forward gradient sync path.
26
- self._noop = nn.Parameter(torch.zeros(1), requires_grad=False)
27
-
28
- def forward(self, x: torch.Tensor) -> torch.Tensor:
29
- return x