Download native/weather/encoder.py from gatilin/mkb_weather_encoder_decoder: direct link, hf CLI and curl.
- Browser
- Download file 20.4 kB
-
https://huggingface.co/gatilin/mkb_weather_encoder_decoder/resolve/main/native/weather/encoder.py
- Command line
-
hf download hf://gatilin/mkb_weather_encoder_decoder/native/weather/encoder.py
-
curl -L -o encoder.py https://huggingface.co/gatilin/mkb_weather_encoder_decoder/resolve/main/native/weather/encoder.py
20.4 kB
| """Weather encoder: Polaris Swin-ViT + meteo merger (projector to LLM hidden). | |
| Contract: | |
| * ``forward(input_ids, attention_mask, **extra_kwargs) -> (latent, mask)``. | |
| ``input_ids`` / ``attention_mask`` are placeholders sized to the number of | |
| meteo tokens; the real meteorological tensors arrive through ``extra_kwargs`` | |
| (``meteo_values``, ``times``, ``lead_hours``, β¦) under the ``weather_`` prefix. | |
| * The encoder pre-computes ``condition_embed`` and the patch embed and stores | |
| them in ``self._step_cache`` for the matching ``WeatherDecoder`` to reuse. | |
| * Mean / std / nanmask buffers (from ``inject_meteorological_context``) are | |
| stored on the encoder so ``unnormalize`` / lat-weighting / channel masking | |
| run without any external wrapper. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from typing import Any, Dict, List, 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, | |
| CubeEmbedConv, | |
| LayerNorm, | |
| PatchEmbedConv, | |
| TimestepEmbed, | |
| ) | |
| from .internal.polaris_swin import SwinBlock | |
| from .internal.helpers import round_to_multiple | |
| try: | |
| from transformers.models.qwen2.modeling_qwen2 import Qwen2RMSNorm as _MergerNorm | |
| except Exception: # pragma: no cover | |
| _MergerNorm = nn.LayerNorm | |
| logger = logging.getLogger(__name__) | |
| class PolarisMeteoPatchMerger(nn.Module): | |
| """Cross-modal projector ('merger'): Swin hidden β LLM hidden. | |
| Same arithmetic as the original Polaris ``PolarisMeteoPatchMerger``; | |
| serves as the modality projector for the ``ModalityRouter`` (an | |
| ``IdentityProjector`` is registered separately so the router can scatter | |
| its output directly). | |
| """ | |
| def __init__(self, dim: int, context_dim: int) -> None: | |
| super().__init__() | |
| self.hidden_size = context_dim | |
| self.ln_q = _MergerNorm(context_dim, eps=1e-6) | |
| self.mlp = nn.Sequential( | |
| nn.Linear(self.hidden_size, self.hidden_size), | |
| nn.GELU(), | |
| nn.Linear(self.hidden_size, dim), | |
| ) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| bsz, seqlen, _ = x.shape | |
| x = self.ln_q(x) | |
| x = x.view(-1, self.hidden_size) | |
| x = self.mlp(x) | |
| x = x.view(bsz, seqlen, -1) | |
| return x | |
| class WeatherEncoder(nn.Module): | |
| """Polaris-style Swin encoder + meteo merger. | |
| The forward signature is dictated by ``ModalityRouter.encode_and_project``: | |
| it takes ``input_ids`` / ``attention_mask`` (here used only to derive a | |
| batch size; their real meaning is conveyed by the placeholder pad | |
| tokens scattered into the LLM input embeddings) and a number of | |
| keyword-only extras forwarded by the collator under the ``weather_`` | |
| prefix (already stripped by the router). | |
| """ | |
| # Exposed so the router can route per-modality state_dict keys cleanly. | |
| is_image_like = True | |
| def __init__(self, config): | |
| super().__init__() | |
| self.config = config | |
| self.hidden_size = config.hidden_size | |
| self.image_size = config.image_size | |
| self.patch_size = config.patch_size | |
| self.window_size = config.window_size | |
| self.encoder_depth = config.encoder_depth | |
| self.num_heads = config.num_heads | |
| self.const_chans = config.const_chans | |
| self.patch_embed = CubeEmbedConv( | |
| in_chans=config.in_chans, | |
| out_chans=self.hidden_size, | |
| in_frames=config.in_frames, | |
| norm_func=None, | |
| flatten=True, | |
| patch_size=self.patch_size, | |
| ) | |
| if self.const_chans > 0: | |
| self.const_embed = PatchEmbedConv( | |
| self.const_chans, self.hidden_size, config.patch_size, | |
| norm_func=LayerNorm, flatten=True, | |
| ) | |
| self.embed_mode = config.embed_mode | |
| self.embed_types = list(config.embed_types) | |
| self.embed_freq = config.embed_freq | |
| self._init_time_embeddings() | |
| 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) | |
| self.encoder_layers = nn.ModuleList() | |
| for i in range(self.encoder_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=self.window_size, | |
| shift_size=0 if i % 2 == 0 else self.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.encoder_layers.append(blk) | |
| # Cross-modal merger: Swin hidden β LLM hidden (qwenvl_dim). | |
| # `ModalityRouter` registers an IdentityProjector for `weather` since | |
| # the upscaling already happens here. | |
| self.meteo_merger = PolarisMeteoPatchMerger( | |
| dim=config.qwenvl_dim, context_dim=self.hidden_size, | |
| ) | |
| 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.gradient_checkpointing = False | |
| # ββ Lazy data buffers (mean/std/nanmask/const/weight/...) βββ | |
| # Populated by ``inject_meteorological_context``. Held in fp32 | |
| # CPU tensors and copied to device on demand to keep memory low. | |
| self._polaris_data_loaded = False | |
| self.channels: List[str] = [] | |
| self.indices: Dict[str, List[int]] = {} | |
| self.coords: Dict[str, list] = {} | |
| self._fp32_master: Dict[str, torch.Tensor] = {} | |
| # Per-forward cache shared with the matching WeatherDecoder. | |
| # Cleared at the start of every encoder forward to avoid leaking | |
| # graph-disconnected tensors across steps. | |
| self._step_cache: Dict[str, Any] = {} | |
| # ------------------------------------------------------------------ | |
| # RoPE buffer self-heal | |
| # ------------------------------------------------------------------ | |
| def _ensure_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 is finite (large | |
| floats) which an isfinite-only check would miss. | |
| """ | |
| 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 not needs_recompute: | |
| return | |
| head_dim = self.hidden_size // self.num_heads | |
| max_seq_len = self.freqs_cos.shape[0] | |
| new_cos, new_sin = precompute_freqs_cis(head_dim, max_seq_len) | |
| with torch.no_grad(): | |
| self.freqs_cos.copy_(new_cos.to(self.freqs_cos.device, self.freqs_cos.dtype)) | |
| self.freqs_sin.copy_(new_sin.to(self.freqs_sin.device, self.freqs_sin.dtype)) | |
| # ------------------------------------------------------------------ | |
| # Time-conditioning | |
| # ------------------------------------------------------------------ | |
| def _init_time_embeddings(self): | |
| if self.embed_mode == "add": | |
| self.embed_dim = self.hidden_size | |
| elif self.embed_mode == "cat": | |
| self.embed_dim = round_to_multiple(self.hidden_size // len(self.embed_types)) * len(self.embed_types) | |
| else: | |
| raise ValueError(f"Invalid embed_mode: {self.embed_mode}") | |
| # Optional dropout on time-embedding outputs (anti-overfit for | |
| # ``hour`` / ``doy``). Read from config; default 0.0 keeps the | |
| # legacy zero-dropout behaviour identical to upstream Polaris. | |
| time_embed_dropout = float(getattr(self.config, "time_embed_dropout", 0.0)) | |
| for k in self.embed_types: | |
| embed_layer = TimestepEmbed( | |
| self.embed_dim, frequency=self.embed_freq, | |
| is_periodic=(k in ["hour", "doy"]), sinusoidal=True, | |
| dropout=time_embed_dropout, | |
| ) | |
| self.add_module(f"{k}_embed", embed_layer) | |
| def _forward_embedding(self, conds: Dict[str, torch.Tensor]) -> torch.Tensor: | |
| if self.embed_mode == "add": | |
| embed = 0 | |
| for k in self.embed_types: | |
| embedding = getattr(self, f"{k}_embed") | |
| embed = embed + embedding(conds[k]) | |
| return embed | |
| # cat | |
| embeds = [getattr(self, f"{k}_embed")(conds[k]) for k in self.embed_types] | |
| return torch.cat(embeds, dim=1) | |
| # ------------------------------------------------------------------ | |
| # Meteo data buffer management | |
| # ------------------------------------------------------------------ | |
| def inject_meteorological_context(self, channels, indices, coords, buffers): | |
| """Inject ERA5 mean/std/nanmask buffers onto this encoder. | |
| Stores fp32 tensors on CPU; ``_get_fp32_const`` copies to device on | |
| demand. Mirrors the original Polaris API so external loaders | |
| (``polaris_data_utils.load_meteorological_buffers``) work unchanged. | |
| """ | |
| self.channels = channels | |
| self.indices = indices | |
| self.coords = coords | |
| for k, v in buffers.items(): | |
| if isinstance(v, np.ndarray): | |
| t = torch.from_numpy(v).float() | |
| elif torch.is_tensor(v): | |
| t = v.detach().float() | |
| else: | |
| setattr(self, k, v) | |
| continue | |
| self._fp32_master[k] = t.cpu() | |
| self._polaris_data_loaded = True | |
| def _get_fp32_const(self, name: str, device: torch.device) -> torch.Tensor: | |
| if not self._polaris_data_loaded or name not in self._fp32_master: | |
| raise RuntimeError( | |
| f"WeatherEncoder data not loaded! Call inject_meteorological_context() " | |
| f"before using '{name}'." | |
| ) | |
| return self._fp32_master[name].to(device=device, dtype=torch.float32, non_blocking=True) | |
| # ------------------------------------------------------------------ | |
| # Input pre-processing (replicates Polaris reset_input / nan handling) | |
| # ------------------------------------------------------------------ | |
| def _reset_output(self, x: torch.Tensor) -> torch.Tensor: | |
| if "nanmask" in self._fp32_master: | |
| nanmask = self._fp32_master["nanmask"].to(x.device)[: x.shape[-3]] | |
| x = x * (~nanmask) | |
| if "channel_mask" in self._fp32_master: | |
| cmask = self._fp32_master["channel_mask"].to(x.device)[: x.shape[-3]] | |
| x = x * cmask | |
| return x | |
| def _reset_input(self, x: torch.Tensor) -> torch.Tensor: | |
| x = self._reset_output(x) | |
| accumid = self.indices.get("accumid", []) | |
| if len(accumid) > 0: | |
| if torch.is_grad_enabled(): | |
| x = x.clone() | |
| x[:, :, accumid] = 0 | |
| return x | |
| # ------------------------------------------------------------------ | |
| # Conditioning factory (hour, doy, step, lead_hour, optional const) | |
| # ------------------------------------------------------------------ | |
| def get_condition( | |
| self, | |
| t: int, | |
| times, | |
| lead_hour, | |
| device: torch.device, | |
| dtype: torch.dtype, | |
| ) -> Dict[str, torch.Tensor]: | |
| times = pd.DatetimeIndex(times) | |
| B = len(times) | |
| if isinstance(lead_hour, torch.Tensor): | |
| lead_np = lead_hour.detach().to(dtype=torch.float32, device="cpu").numpy() | |
| else: | |
| lead_np = np.asarray(lead_hour, dtype=np.float32) | |
| if lead_np.ndim == 0: | |
| lead_np = np.full((B,), float(lead_np), dtype=np.float32) | |
| elif lead_np.size == 1 and B > 1: | |
| lead_np = np.full((B,), float(lead_np.reshape(-1)[0]), dtype=np.float32) | |
| elif lead_np.size != B: | |
| raise ValueError(f"lead_hour size mismatch: got {lead_np.size}, expected {B}") | |
| cur_times = times + pd.to_timedelta(lead_np * t, unit="h") | |
| used_times = cur_times | |
| hour_np = used_times.hour.values.astype(np.float32) / 24.0 | |
| doy_np = (np.minimum(365, used_times.dayofyear.values).astype(np.float32) / 365.0) | |
| hour = torch.tensor(hour_np, dtype=dtype, device=device) | |
| doy = torch.tensor(doy_np, dtype=dtype, device=device) | |
| Tmax = getattr(self.config, "max_rollout_steps", 200) | |
| step_scalar = float(np.log1p(float(t)) / np.log1p(float(Tmax))) | |
| step = torch.full((B,), step_scalar, dtype=dtype, device=device) | |
| if self.training: | |
| step = step + torch.empty((B,), device=device, dtype=dtype).uniform_(-0.005, 0.005) | |
| lead_hour_t = torch.tensor(lead_np, dtype=dtype, device=device) | |
| conds = dict(hour=hour, doy=doy, step=step, lead_hour=lead_hour_t) | |
| if "const" in self._fp32_master: | |
| conds["const"] = self._get_fp32_const("const", device).type(dtype) | |
| return conds | |
| # ------------------------------------------------------------------ | |
| # Encoder forward (ModalityRouter contract) | |
| # ------------------------------------------------------------------ | |
| def forward( | |
| self, | |
| input_ids: torch.Tensor, | |
| attention_mask: torch.Tensor, | |
| meteo_values: Optional[torch.Tensor] = None, | |
| targets: Optional[torch.Tensor] = None, | |
| times: Optional[Any] = None, | |
| lead_hours: Optional[torch.Tensor] = None, | |
| polaris_task: Optional[Any] = None, | |
| channel_mask: Optional[torch.Tensor] = None, | |
| step_idx: int = 0, | |
| **_unused, | |
| ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: | |
| """Encode meteorological inputs. | |
| Args: | |
| input_ids / attention_mask: dummy placeholders sized | |
| ``[B, swin_H * swin_W]``. Required by the router signature | |
| but not used computationally. | |
| meteo_values: ``[B, T_in, C, H, W]`` float32 (smuggled as int32 by | |
| the collator and reinterpreted upstream) β already on device. | |
| times: pandas timestamps for the current batch (one per sample). | |
| lead_hours: ``[B]`` int / float β forecast lead hours. | |
| channel_mask: optional ``[B, C, 1, 1]`` channel mask. | |
| step_idx: int rollout step index (0 for single-step training). | |
| Returns: | |
| latent: ``[B, swin_H * swin_W, qwenvl_dim]`` after meteo_merger. | |
| latent_mask: None. | |
| """ | |
| if meteo_values is None: | |
| raise ValueError("WeatherEncoder.forward requires `meteo_values` in extras.") | |
| # Rebuild RoPE freqs if the checkpoint load left them uninitialised | |
| # (non-persistent buffers under a meta-device load come up as garbage). | |
| self._ensure_rope_buffers() | |
| # NaNβzero, fp32 normalisation | |
| if meteo_values.dtype == torch.int32: | |
| meteo_values = meteo_values.contiguous().view(torch.float32) | |
| meteo_values = meteo_values.contiguous().to(torch.float32) | |
| nanmask_local = torch.isnan(meteo_values[-1, -1]) | |
| if torch.any(nanmask_local): | |
| self._fp32_master["nanmask"] = nanmask_local.cpu() | |
| meteo_values = torch.nan_to_num(meteo_values) | |
| meteo_values = self._reset_input(meteo_values) | |
| if channel_mask is not None: | |
| self._fp32_master["channel_mask"] = channel_mask[0].cpu() | |
| device = meteo_values.device | |
| # Compute conditions & feed encoder. Use the encoder's compute dtype | |
| # (e.g. bf16) β meteo_values is fp32 here due to the upstream | |
| # NaN/normalisation cast, but the embedding MLPs run in bf16 and | |
| # would otherwise see a dtype mismatch in F.linear. | |
| conds = self.get_condition(step_idx, times, lead_hours, device=device, dtype=self._embed_dtype()) | |
| meteo_compute = meteo_values.to(self._embed_dtype()) | |
| meteo_feature = self.patch_embed(meteo_compute, conds["lead_hour"]) | |
| patch_embed = meteo_feature | |
| if self.const_chans > 0 and "const" in conds: | |
| const = conds["const"].to(device=device, dtype=meteo_compute.dtype) | |
| meteo_feature = meteo_feature + self.const_embed(const) | |
| condition_embed = self._forward_embedding(conds) | |
| # Skip gradient checkpointing under torch.no_grad() β e.g. rollout | |
| # warm-up steps β otherwise PyTorch emits a per-block "None of the | |
| # inputs have requires_grad=True" warning and `checkpoint` adds | |
| # pointless bookkeeping for a path that won't backprop anyway. | |
| use_gc = ( | |
| self.gradient_checkpointing | |
| and self.training | |
| and torch.is_grad_enabled() | |
| ) | |
| for blk in self.encoder_layers: | |
| if use_gc: | |
| meteo_feature = checkpoint( | |
| blk, meteo_feature, condition_embed, self.freqs_cos, self.freqs_sin, | |
| use_reentrant=False, | |
| ) | |
| else: | |
| meteo_feature = blk(meteo_feature, condition_embed, self.freqs_cos, self.freqs_sin) | |
| # `meteo_feature` is the post-swin encoder output (Polaris's | |
| # `meteo_embeds`). The matching head uses it as the skip connection | |
| # *after* mlp_qwen2swin, so cache before applying the merger. | |
| meteo_embeds_post_swin = meteo_feature | |
| merged = self.meteo_merger(meteo_feature) # β [B, L, qwenvl_dim] | |
| # Cache for the decoder. We deliberately keep references to all | |
| # the intermediate tensors needed by the head so the decoder can | |
| # reuse them without recomputing the encoder side. | |
| # ``patch_embed_post_swin`` matches the Polaris ``meteo_embeds`` | |
| # variable used in ``polaris_head(meteo_feature_patch_embed=...)`` | |
| # under the use_language=True flow β i.e. the encoder swin output. | |
| self._step_cache = { | |
| "condition_embed": condition_embed, | |
| "patch_embed_post_swin": meteo_embeds_post_swin, | |
| "meteo_values": meteo_values, | |
| "targets": targets, | |
| "times": times, | |
| "lead_hours": lead_hours, | |
| "input_size": meteo_values.shape[-2:], | |
| "T_in": meteo_values.shape[1], | |
| } | |
| # Mask is None β every meteo token is valid; the router will infer | |
| # the count from the pad-token mask. | |
| return merged, None | |
| def _embed_dtype(self) -> torch.dtype: | |
| # Canonical encoder compute dtype (handles bf16 / fp32 uniformly). | |
| for p in self.parameters(): | |
| return p.dtype | |
| return torch.float32 | |
| def unnormalize(self, x: torch.Tensor, fill_type: str = "zero") -> torch.Tensor: | |
| """Undo channel-wise mean/std normalisation. fp32 throughout.""" | |
| mean = self._get_fp32_const("mean", x.device) | |
| std = self._get_fp32_const("std", x.device) | |
| x = x.to(torch.float32) * std + mean | |
| logid = self.indices.get("logid", []) | |
| if len(logid) > 0: | |
| v = x[:, :, logid].clamp(min=0, max=7) | |
| x[:, :, logid] = torch.expm1(v) | |
| return self._reset_output(x) | |