File size: 10,736 Bytes
95c892c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
"""Weather decoder: Polaris meteo head + regression-style loss.

Adapts ``PolarisMeteoHead`` to the bio_qwen3vl ``ModalityRouter``.  The
decoder is invoked by ``compute_decoder_losses`` via the custom
``compute_loss_from_hidden`` hook (see ``mkb/registry/modality_router.py``):
that path bypasses the standard CE-on-decode-logits flow used by RNA /
protein / mol since meteo prediction is a regression problem on a 5D
tensor field.

Inputs needed at loss time live in two places:

* ``hidden_states`` (LLM output) — selects the meteo-token positions.
* ``self.encoder._step_cache`` — set by ``WeatherEncoder.forward`` and
  contains ``condition_embed`` / ``patch_embed_pre_swin`` /
  ``meteo_values`` / ``targets`` / ``lead_hours``.

The encoder reference is wired up in ``register_modality`` so the decoder
does not have to be passed it through every forward.
"""

from __future__ import annotations

import logging
from typing import Any, Dict, Optional, Tuple

import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
from torch.utils.checkpoint import checkpoint

from .internal.polaris_attention import precompute_freqs_cis
from .internal.polaris_layers import AdaLN, DoubleDeconvHead, remove_small_scales
from .internal.polaris_swin import SwinBlock
from .internal.helpers import round_to_multiple

logger = logging.getLogger(__name__)


class WeatherDecoder(nn.Module):
    """Polaris meteo head + regression loss path.

    The original Polaris ``PolarisMeteoHead.forward`` is wrapped in
    ``compute_loss_from_hidden`` so it can pull the matching encoder cache
    and produce a scalar loss directly from LLM hidden states.
    """

    def __init__(self, config, encoder):
        super().__init__()
        self.config = config
        # Hold the encoder reference in a 1-element list so PyTorch's
        # automatic Module-attribute registration does NOT pull the
        # encoder's parameters in as decoder children (which would
        # duplicate them under both encoders.weather.* and decoders.weather.*
        # in the state_dict).
        self._encoder_ref = [encoder]

        self.hidden_size = config.hidden_size
        self.num_heads = config.num_heads
        self.qwenvl_dim = config.qwenvl_dim
        self.patch_size = config.patch_size
        self.upper_chans = config.upper_chans
        self.lower_chans = config.lower_chans
        self.decoder_depth = config.decoder_depth

        if isinstance(config.image_size, int):
            in_h = in_w = config.image_size
        else:
            in_h, in_w = config.image_size
        if self.patch_size == 1:
            swin_H = in_h // 2 * 2
            swin_W = in_w
        else:
            swin_H = in_h // self.patch_size
            swin_W = in_w // self.patch_size
        self._swin_HW = (swin_H, swin_W)

        max_seq_len = swin_H * swin_W
        freqs_cos, freqs_sin = precompute_freqs_cis(self.hidden_size // self.num_heads, max_seq_len)
        self.register_buffer("freqs_cos", freqs_cos, persistent=False)
        self.register_buffer("freqs_sin", freqs_sin, persistent=False)
        self._rope_buffers_checked = False

        # Cross-modal back projector: LLM hidden → swin hidden.
        # Trained at ``mm_projector_lr`` (matched on substring "mlp_qwen2swin").
        self.mlp_qwen2swin = nn.Sequential(
            nn.Linear(self.qwenvl_dim, self.qwenvl_dim),
            nn.GELU(),
            nn.Linear(self.qwenvl_dim, self.hidden_size),
        )

        if config.embed_mode == "add":
            self.embed_dim = config.hidden_size
        elif config.embed_mode == "cat":
            self.embed_dim = round_to_multiple(config.hidden_size // len(config.embed_types)) * len(config.embed_types)
        else:
            raise ValueError(f"Invalid embed_mode: {config.embed_mode}")

        self.decoder_layers = nn.ModuleList()
        for i in range(self.decoder_depth):
            blk = SwinBlock(
                dim=self.hidden_size, embed_dim=self.embed_dim, num_heads=self.num_heads,
                input_size=(swin_H, swin_W), window_size=config.window_size,
                shift_size=0 if i % 2 == 0 else config.window_size // 2,
                mlp_ratio=config.mlp_ratio, attn_type=config.attn_type,
                mask_type=config.mask_type, norm_type=config.norm_type,
                ffn_type=config.ffn_type, n_kv_heads=config.n_kv_heads,
                attn_implementation="eager",
            )
            self.decoder_layers.append(blk)

        self.norm_layer = AdaLN(self.hidden_size, embed_dim=self.embed_dim)
        self.pred_layer = DoubleDeconvHead(
            in_chans=self.hidden_size, upper_chans=self.upper_chans,
            lower_chans=self.lower_chans, patch_size=self.patch_size,
        )

        self.gradient_checkpointing = False

    # Provide the same context-injection API the encoder has, so the
    # training entry can call it on the decoder too without checking
    # which side actually owns the buffers.
    def inject_meteorological_context(self, *args, **kwargs):
        # The encoder is the canonical owner of mean/std/nanmask/...
        # We ignore here to avoid duplicate state.
        return

    def _ensure_finite_rope_buffers(self) -> None:
        """Rebuild the RoPE tables if the non-persistent buffers came up broken.

        ``freqs_cos`` / ``freqs_sin`` are ``persistent=False`` (not in the
        checkpoint) and are meant to be filled at ``__init__``. Under a
        meta-device ``from_pretrained`` load they can be ``to_empty``'d to
        uninitialised memory. A valid table has ``cos[0]==1`` and ``sin[0]==0``
        for every column — checking row 0 catches garbage that happens to be
        finite (large floats) which an isfinite-only check would miss.
        """
        if self._rope_buffers_checked:
            return
        with torch.no_grad():
            row0 = self.freqs_cos[0].detach().to(torch.float32)
            row0_sin = self.freqs_sin[0].detach().to(torch.float32)
        needs_recompute = (
            not torch.isfinite(self.freqs_cos).all()
            or not torch.isfinite(self.freqs_sin).all()
            or not torch.allclose(row0, torch.ones_like(row0), atol=1e-5)
            or not torch.allclose(row0_sin, torch.zeros_like(row0_sin), atol=1e-5)
        )
        if needs_recompute:
            logger.warning(
                "[weather] broken decoder RoPE buffers detected "
                "(nan/inf/zeros); recomputing freqs_cos/freqs_sin"
            )
            max_seq_len = int(self._swin_HW[0] * self._swin_HW[1])
            freqs_cos, freqs_sin = precompute_freqs_cis(
                self.hidden_size // self.num_heads, max_seq_len,
            )
            with torch.no_grad():
                self.freqs_cos.copy_(freqs_cos.to(self.freqs_cos.device, self.freqs_cos.dtype))
                self.freqs_sin.copy_(freqs_sin.to(self.freqs_sin.device, self.freqs_sin.dtype))
        self._rope_buffers_checked = True

    # ------------------------------------------------------------------
    # Loss computation (the only ModalityRouter-callable entry point)
    # ------------------------------------------------------------------

    def predict_from_hidden(
        self,
        hidden_states: torch.Tensor,
        **kwargs,
    ) -> Optional[torch.Tensor]:
        """Decode LLM hidden states into a meteo prediction tensor.

        Pure inference: no loss, no NaN-guard side effects.  Reusable by
        the rollout engine, which needs to call this multiple times within
        one optimizer step (warm-up steps + selected step).

        Returns ``meteo_output`` of shape ``[B, C, H, W]`` (the ``pred_layer``
        currently emits a single-frame prediction; rollout stitches frames
        externally), or ``None`` if no weather pad tokens are present.
        """
        encoder = self._encoder_ref[0]
        cache = getattr(encoder, "_step_cache", None) or {}
        if not cache:
            return None

        self._ensure_finite_rope_buffers()

        condition_embed = cache["condition_embed"]
        patch_embed_post_swin = cache["patch_embed_post_swin"]
        meteo_values = cache["meteo_values"]
        lead_hours = cache["lead_hours"]
        input_size = cache["input_size"]
        T_in = cache["T_in"]

        input_ids = kwargs.get("input_ids")
        weather_pad_id = self._weather_pad_id_from_config(kwargs)
        if input_ids is None or weather_pad_id is None:
            raise RuntimeError(
                "WeatherDecoder.predict_from_hidden: input_ids / weather pad id missing."
            )

        meteo_mask = (input_ids == weather_pad_id)
        B, _, D = hidden_states.shape
        L_per_sample = int(meteo_mask.sum(dim=1)[0].item())
        if L_per_sample == 0:
            return None
        meteo_states = hidden_states[meteo_mask].view(B, L_per_sample, D)

        h = self.mlp_qwen2swin(meteo_states)
        if patch_embed_post_swin is not None:
            h = h + patch_embed_post_swin

        # Skip gradient checkpointing under torch.no_grad() (e.g. rollout
        # warm-up) — checkpoint() warns and offers no benefit when the
        # outputs don't require grad.
        use_gc = (
            self.gradient_checkpointing
            and self.training
            and torch.is_grad_enabled()
        )
        for blk in self.decoder_layers:
            if use_gc:
                h = checkpoint(
                    blk, h, condition_embed, self.freqs_cos, self.freqs_sin,
                    use_reentrant=False,
                )
            else:
                h = blk(h, condition_embed, self.freqs_cos, self.freqs_sin)

        h = self.norm_layer(h, condition_embed)
        h = rearrange(h, "n (h w) c -> n c h w", h=input_size[0] // self.patch_size // 2 * 2)
        meteo_output = self.pred_layer(
            h,
            residual=meteo_values[:, -1],
            input_size=input_size,
            lead_hour=condition_embed.new_tensor([0.0]) if lead_hours is None else lead_hours,
            target_frames=T_in - 1,
        )
        return meteo_output

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _weather_pad_id_from_config(kwargs) -> Optional[int]:
        """Extract ``<|weather_pad|>`` id.

        We rely on the model upstream to pass it under the key
        ``__weather_pad_id__`` (set by the model wrapper before calling
        ``compute_decoder_losses``).  Falls back to None when missing.
        """
        return kwargs.get("__weather_pad_id__")