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__")
|