cascade generator submission: checkpoint
Browse files- config.json +32 -0
- forecast_wrapper.py +174 -0
- model.py +393 -0
- weights.safetensors +3 -0
config.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"arch": "toto2-4m",
|
| 3 |
+
"toto2": {
|
| 4 |
+
"d_model": 256,
|
| 5 |
+
"num_layers": 4,
|
| 6 |
+
"num_heads": 4,
|
| 7 |
+
"head_dim": 64,
|
| 8 |
+
"patch_size": 32,
|
| 9 |
+
"mlp_expansion": 2,
|
| 10 |
+
"d_ff": 688,
|
| 11 |
+
"num_quantiles": 9,
|
| 12 |
+
"context_length": 4096,
|
| 13 |
+
"horizon": 64,
|
| 14 |
+
"max_patches": 134,
|
| 15 |
+
"layer_group_size": 4,
|
| 16 |
+
"cpm_c_max": 16,
|
| 17 |
+
"cpm_p_max": 0.4,
|
| 18 |
+
"residual_mult": 0.75
|
| 19 |
+
},
|
| 20 |
+
"quantile_levels": [
|
| 21 |
+
0.1,
|
| 22 |
+
0.2,
|
| 23 |
+
0.3,
|
| 24 |
+
0.4,
|
| 25 |
+
0.5,
|
| 26 |
+
0.6,
|
| 27 |
+
0.7,
|
| 28 |
+
0.8,
|
| 29 |
+
0.9
|
| 30 |
+
],
|
| 31 |
+
"input_transform": "arcsinh_causal"
|
| 32 |
+
}
|
forecast_wrapper.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Auto-generated by cascade Toto2Trainer. Loads the trained checkpoint and
|
| 2 |
+
decodes the full horizon in one forward pass via contiguous patch masking
|
| 3 |
+
(CPM) — no autoregressive sampling. Exposes:
|
| 4 |
+
|
| 5 |
+
forecast(history, horizon, num_samples) -> (1, num_samples, horizon)
|
| 6 |
+
the cascade validator contract — sample paths drawn once from the
|
| 7 |
+
decoded quantiles (seeded per window for validator consensus).
|
| 8 |
+
forecast_quantiles(history, horizon) -> (1, horizon, num_q)
|
| 9 |
+
forecast_quantiles_batch(histories, horizon) -> (B, horizon, num_q)
|
| 10 |
+
the quantile head directly — what benchmark CRPS consumes; batched
|
| 11 |
+
across series so eval sweeps amortize the forward passes.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import hashlib
|
| 17 |
+
import importlib.util
|
| 18 |
+
import json
|
| 19 |
+
import sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import torch
|
| 24 |
+
|
| 25 |
+
# Single-pass CPM decoding is stable to ~768 steps (Toto 2.0 tech report);
|
| 26 |
+
# longer horizons block-decode: commit the median per block, then continue.
|
| 27 |
+
STABLE_DECODE_STEPS = 768
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _load_model_module(d: Path):
|
| 31 |
+
spec = importlib.util.spec_from_file_location("cascade_ckpt_model", d / "model.py")
|
| 32 |
+
mod = importlib.util.module_from_spec(spec)
|
| 33 |
+
# Register before exec: model.py defines an @dataclass, and the dataclass
|
| 34 |
+
# machinery does sys.modules.get(cls.__module__).__dict__ during class
|
| 35 |
+
# creation — which is None (AttributeError) unless the module is registered.
|
| 36 |
+
sys.modules[spec.name] = mod
|
| 37 |
+
spec.loader.exec_module(mod)
|
| 38 |
+
return mod
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Wrapper:
|
| 42 |
+
def __init__(self, checkpoint_dir, device: str = "cpu"):
|
| 43 |
+
d = Path(checkpoint_dir)
|
| 44 |
+
self.device = device
|
| 45 |
+
cfg_obj = json.loads((d / "config.json").read_text())
|
| 46 |
+
self.m = _load_model_module(d)
|
| 47 |
+
self.cfg = self.m.Toto2Config(**cfg_obj["toto2"])
|
| 48 |
+
self.quantile_levels = [float(v) for v in cfg_obj["quantile_levels"]]
|
| 49 |
+
self.levels = torch.tensor(self.quantile_levels, dtype=torch.float32, device=device)
|
| 50 |
+
self.model = self.m.Toto2Model(self.cfg).to(device).eval()
|
| 51 |
+
from safetensors.torch import load_file
|
| 52 |
+
state = load_file(str(d / "weights.safetensors"))
|
| 53 |
+
self.model.load_state_dict(state)
|
| 54 |
+
|
| 55 |
+
# ── CPM decoding ──────────────────────────────────────────────────────────
|
| 56 |
+
|
| 57 |
+
def _prep(self, histories):
|
| 58 |
+
"""Left-pad (with the first value) or truncate each 1-D history to the
|
| 59 |
+
context window. Returns the real-space context ``(B, window_len)`` in
|
| 60 |
+
float64 — standardization happens per decode block, from full
|
| 61 |
+
precision, so large-level series keep their fluctuations."""
|
| 62 |
+
ps = self.cfg.patch_size
|
| 63 |
+
n_ctx = max(2, self.cfg.context_length // ps)
|
| 64 |
+
window_len = n_ctx * ps
|
| 65 |
+
rows = []
|
| 66 |
+
for h in histories:
|
| 67 |
+
h = np.asarray(h, dtype=np.float64).reshape(-1)
|
| 68 |
+
if h.shape[0] < window_len:
|
| 69 |
+
pad = np.full(window_len - h.shape[0], h[0] if h.size else 0.0)
|
| 70 |
+
h = np.concatenate([pad, h])
|
| 71 |
+
else:
|
| 72 |
+
h = h[-window_len:]
|
| 73 |
+
rows.append(h)
|
| 74 |
+
return torch.as_tensor(np.stack(rows), dtype=torch.float64, device=self.device)
|
| 75 |
+
|
| 76 |
+
@torch.no_grad()
|
| 77 |
+
def _decode_block_z(self, z, block: int):
|
| 78 |
+
"""One CPM forward pass: append ``block`` masked patches to the
|
| 79 |
+
normalized context ``(B, L)`` and read their z-space quantiles
|
| 80 |
+
``(B, block*patch_size, num_q)``."""
|
| 81 |
+
ps = self.cfg.patch_size
|
| 82 |
+
# keep as much context as the positional table allows
|
| 83 |
+
ctx_p = min(z.shape[1] // ps, self.cfg.max_patches - block)
|
| 84 |
+
ctx = z[:, -ctx_p * ps :].view(z.shape[0], ctx_p, ps)
|
| 85 |
+
filler = torch.zeros(z.shape[0], block, ps, dtype=ctx.dtype, device=self.device)
|
| 86 |
+
mask = torch.zeros(z.shape[0], ctx_p + block, dtype=ctx.dtype, device=self.device)
|
| 87 |
+
mask[:, ctx_p:] = 1.0
|
| 88 |
+
pred = self.model(torch.cat([ctx, filler], dim=1), mask=mask)
|
| 89 |
+
# position i predicts patch i+1 → the horizon patches come from
|
| 90 |
+
# positions ctx_p-1 .. ctx_p+block-2.
|
| 91 |
+
q = pred[:, ctx_p - 1 : ctx_p + block - 1] # (B, block, ps, nq)
|
| 92 |
+
q, _ = torch.sort(q, dim=-1) # prevent quantile crossing
|
| 93 |
+
return q.reshape(z.shape[0], block * ps, -1)
|
| 94 |
+
|
| 95 |
+
@torch.no_grad()
|
| 96 |
+
def _decode_quantiles(self, x, horizon: int):
|
| 97 |
+
"""Block-decode real-space quantiles ``(B, horizon, num_q)`` from the
|
| 98 |
+
real-space context ``x`` ``(B, L)``.
|
| 99 |
+
|
| 100 |
+
Each block re-runs the causal scaler over history + committed medians
|
| 101 |
+
and unscales with the resulting end-of-context anchor. Committed
|
| 102 |
+
patches are *observed* context for later blocks, and in training the
|
| 103 |
+
causal stats advance through every observed patch — so the anchor must
|
| 104 |
+
advance with them; reusing the pre-horizon anchor would feed blocks ≥ 2
|
| 105 |
+
a scale/location regime the model never sees in training. Clamp bounds
|
| 106 |
+
are fixed from the original context (min/max ± 1e4x anchor scale, per
|
| 107 |
+
the report) so committed medians can't widen them.
|
| 108 |
+
"""
|
| 109 |
+
ps = self.cfg.patch_size
|
| 110 |
+
stable = max(1, min(STABLE_DECODE_STEPS // ps, self.cfg.max_patches - 2))
|
| 111 |
+
remaining = -(-int(horizon) // ps)
|
| 112 |
+
lo = hi = None
|
| 113 |
+
out = []
|
| 114 |
+
while remaining > 0:
|
| 115 |
+
block = min(remaining, stable)
|
| 116 |
+
z, loc_t, scale_t = self.m.causal_standardize(x)
|
| 117 |
+
loc = loc_t[:, -1:].double().unsqueeze(-1) # (B, 1, 1)
|
| 118 |
+
scale = scale_t[:, -1:].double().unsqueeze(-1)
|
| 119 |
+
if lo is None:
|
| 120 |
+
lo = x.min(dim=-1, keepdim=True).values.unsqueeze(-1) - 1e4 * scale
|
| 121 |
+
hi = x.max(dim=-1, keepdim=True).values.unsqueeze(-1) + 1e4 * scale
|
| 122 |
+
qz = self._decode_block_z(z.to(torch.float32), block)
|
| 123 |
+
q = torch.sinh(qz.double()) * scale + loc # (B, block*ps, nq)
|
| 124 |
+
q = torch.clamp(q, min=lo, max=hi)
|
| 125 |
+
out.append(q)
|
| 126 |
+
remaining -= block
|
| 127 |
+
if remaining > 0:
|
| 128 |
+
x = torch.cat([x, q[..., q.shape[-1] // 2]], dim=1)
|
| 129 |
+
return torch.cat(out, dim=1)[:, : int(horizon)]
|
| 130 |
+
|
| 131 |
+
# ── quantile head (benchmark path) ────────────────────────────────────────
|
| 132 |
+
|
| 133 |
+
@torch.no_grad()
|
| 134 |
+
def forecast_quantiles_batch(self, histories, horizon: int) -> np.ndarray:
|
| 135 |
+
"""Decode ``len(histories)`` series in one batch → real-space quantiles
|
| 136 |
+
``(B, horizon, num_q)`` at ``self.quantile_levels``. arcsinh + affine
|
| 137 |
+
are monotone increasing, so quantiles map pointwise."""
|
| 138 |
+
q = self._decode_quantiles(self._prep(list(histories)), horizon)
|
| 139 |
+
return q.detach().cpu().numpy().astype(np.float64)
|
| 140 |
+
|
| 141 |
+
def forecast_quantiles(self, history, horizon: int) -> np.ndarray:
|
| 142 |
+
return self.forecast_quantiles_batch([history], horizon)
|
| 143 |
+
|
| 144 |
+
# ── validator contract (sample paths) ─────────────────────────────────────
|
| 145 |
+
|
| 146 |
+
@torch.no_grad()
|
| 147 |
+
def forecast(self, history, horizon: int, num_samples: int) -> np.ndarray:
|
| 148 |
+
hist = np.asarray(history, dtype=np.float64).reshape(-1)
|
| 149 |
+
# Deterministic per-window sampling: seed from the (raw history, horizon,
|
| 150 |
+
# num_samples) so every validator computes identical scores and king vs
|
| 151 |
+
# challenger share the uniform draws (paired Monte-Carlo).
|
| 152 |
+
seed_src = hist.tobytes() + int(horizon).to_bytes(8, "big") + int(num_samples).to_bytes(8, "big")
|
| 153 |
+
seed = int.from_bytes(hashlib.sha256(seed_src).digest()[:8], "big") & ((1 << 63) - 1)
|
| 154 |
+
generator = torch.Generator(device=self.device)
|
| 155 |
+
generator.manual_seed(seed)
|
| 156 |
+
|
| 157 |
+
q = self._decode_quantiles(self._prep([hist]), horizon)[0] # (h, nq) real-space
|
| 158 |
+
# One draw per step per path via the piecewise-linear inverse CDF of the
|
| 159 |
+
# decoded quantiles (already clamped and monotone in the level).
|
| 160 |
+
# Quantiles decode once; samples never feed back.
|
| 161 |
+
nq = q.shape[-1]
|
| 162 |
+
levels = self.levels
|
| 163 |
+
u = torch.rand(int(num_samples), int(horizon), device=self.device, generator=generator)
|
| 164 |
+
idx = torch.searchsorted(levels, u.clamp(levels[0].item(), levels[-1].item()))
|
| 165 |
+
idx = idx.clamp(1, nq - 1)
|
| 166 |
+
i_lo = idx - 1
|
| 167 |
+
i_hi = idx
|
| 168 |
+
qe = q.unsqueeze(0).expand(u.shape[0], -1, -1) # (ns, h, nq)
|
| 169 |
+
vl = torch.gather(qe, -1, i_lo.unsqueeze(-1)).squeeze(-1)
|
| 170 |
+
vh = torch.gather(qe, -1, i_hi.unsqueeze(-1)).squeeze(-1)
|
| 171 |
+
ql = levels[i_lo].double(); qh = levels[i_hi].double()
|
| 172 |
+
frac = ((u.double() - ql) / (qh - ql).clamp_min(1e-8)).clamp(0, 1)
|
| 173 |
+
out = vl + frac * (vh - vl) # (ns, h)
|
| 174 |
+
return out.detach().cpu().numpy().reshape(1, int(num_samples), int(horizon))
|
model.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reference Toto2-style backbone — a patch transformer trained with
|
| 2 |
+
contiguous patch masking (CPM) and a multi-quantile head, from random init.
|
| 3 |
+
|
| 4 |
+
This module is **self-contained torch** and is *copied into every checkpoint*
|
| 5 |
+
(as ``model.py``) so the validator's ``forecast_wrapper.py`` can rebuild the
|
| 6 |
+
exact architecture to load the weights. Keep it dependency-light (torch only)
|
| 7 |
+
and free of cascade imports for that reason.
|
| 8 |
+
|
| 9 |
+
It follows the Toto 2.0 recipe (arXiv:2605.20119):
|
| 10 |
+
|
| 11 |
+
* **CPM** — a per-entry binary mask channel; training masks contiguous spans,
|
| 12 |
+
inference fills the horizon with mask patches and decodes it in **one
|
| 13 |
+
forward pass** (no autoregressive sampling).
|
| 14 |
+
* **Grouped time/variate attention** — the last layer of each group of 4
|
| 15 |
+
attends over variates (full), the rest over time (causal, rotary positions);
|
| 16 |
+
this matches ``Datadog/Toto-2.0-4m``'s ``layer_group_size=4`` /
|
| 17 |
+
``num_variate_layers_per_group=1`` / ``variate_layer_first=false``. cascade
|
| 18 |
+
currently trains and scores univariate (``OPEN_QUESTIONS.md`` §8), so the
|
| 19 |
+
variate layers run at ``C = 1`` — present and trainable, dormant until
|
| 20 |
+
multivariate corpora flip on.
|
| 21 |
+
* **Attention details** — PerDimScale (learned per-dimension query scaling)
|
| 22 |
+
with ``1/d_k`` attention scaling, biases on attention projections but not
|
| 23 |
+
MLPs, ``head_dim`` fixed at 64 across the family.
|
| 24 |
+
* **Robust causal scaler** — per-step causal location/scale (mask-aware, with
|
| 25 |
+
leading-patch backfill) under an arcsinh transform; targets are anchored at
|
| 26 |
+
each patch boundary so no future value leaks into its own scaling.
|
| 27 |
+
* **Residual SiLU patch projections** at both ends, and a 9-level
|
| 28 |
+
pinball/quantile head whose levels are exactly cascade's eval objective.
|
| 29 |
+
|
| 30 |
+
Shape and detail integers are pinned to the released ``Datadog/Toto-2.0-4m``
|
| 31 |
+
``config.json``: ``d_model=256``, ``num_layers=4``, ``num_heads=4``,
|
| 32 |
+
``qk/v_dim=64``, ``patch_size=32``, ``d_ff=688``, ``attn_bias``/no
|
| 33 |
+
``mlp_bias``, ``per_dim_scale``, ``use_xpos`` (γ=0.4 decay on rotary),
|
| 34 |
+
``norm_eps=1e-4`` with weightless norms, layer grouping, and the u-μP residual
|
| 35 |
+
scheme (``residual_mult=0.75``, ``residual_attn_ratio=sqrt(S/log S)≈5.14``,
|
| 36 |
+
applied via the unit-scaled a/b residual weights of u-μP eq. 25–31). The
|
| 37 |
+
optimiser orthogonalizes with Polar Express (see ``toto2_trainer.py``).
|
| 38 |
+
Remaining known approximations vs the release: the exact FFN inner structure
|
| 39 |
+
(param count 3.3M vs 4.1M) and the full u-μP init/LR width-scaling rules
|
| 40 |
+
(we keep fan-in init and a uniform LR). Pin ``base_arch_digest`` to whatever
|
| 41 |
+
you launch with.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
from __future__ import annotations
|
| 45 |
+
|
| 46 |
+
import math
|
| 47 |
+
from dataclasses import dataclass
|
| 48 |
+
|
| 49 |
+
import torch
|
| 50 |
+
import torch.nn as nn
|
| 51 |
+
import torch.nn.functional as F
|
| 52 |
+
|
| 53 |
+
# The 9 quantile levels 0.1..0.9 — identical to cascade's eval grid so the
|
| 54 |
+
# train objective equals the score objective.
|
| 55 |
+
QUANTILE_LEVELS = (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class Toto2Config:
|
| 60 |
+
d_model: int = 256
|
| 61 |
+
num_layers: int = 4
|
| 62 |
+
num_heads: int = 4
|
| 63 |
+
head_dim: int = 64
|
| 64 |
+
patch_size: int = 32
|
| 65 |
+
mlp_expansion: int = 2
|
| 66 |
+
d_ff: int = 0 # exact FFN hidden width (0 ⇒ d_model × mlp_expansion); 4m ships 688
|
| 67 |
+
num_quantiles: int = 9
|
| 68 |
+
context_length: int = 4096
|
| 69 |
+
horizon: int = 64
|
| 70 |
+
max_patches: int = 256 # decode window capacity in patches (context + masked horizon)
|
| 71 |
+
# layer grouping (Toto-2.0 config.json: layer_group_size=4,
|
| 72 |
+
# num_variate_layers_per_group=1, variate_layer_first=false) — the last
|
| 73 |
+
# layer of each group of 4 attends over variates, the rest over time.
|
| 74 |
+
layer_group_size: int = 4
|
| 75 |
+
# CPM training-mask distribution (Toto 2.0 §2.1 sweep optima).
|
| 76 |
+
cpm_c_max: int = 16
|
| 77 |
+
cpm_p_max: float = 0.4
|
| 78 |
+
# u-μP residual scale α_res (released config: residual_mult = 0.75; the
|
| 79 |
+
# attention/FFN ratio is derived as sqrt(S/log S) from context/patch).
|
| 80 |
+
residual_mult: float = 0.75
|
| 81 |
+
|
| 82 |
+
@property
|
| 83 |
+
def ffn_hidden(self) -> int:
|
| 84 |
+
return self.d_ff if self.d_ff > 0 else self.d_model * self.mlp_expansion
|
| 85 |
+
|
| 86 |
+
@classmethod
|
| 87 |
+
def from_contract(cls, c: object) -> Toto2Config:
|
| 88 |
+
"""Build from a cascade ``TrainingContractConfig`` (duck-typed)."""
|
| 89 |
+
ctx = int(getattr(c, "context_length", 4096))
|
| 90 |
+
hz = int(getattr(c, "horizon", 64))
|
| 91 |
+
ps = int(getattr(c, "patch_size", 32))
|
| 92 |
+
return cls(
|
| 93 |
+
d_model=int(getattr(c, "d_model", 256)),
|
| 94 |
+
num_layers=int(getattr(c, "num_layers", 4)),
|
| 95 |
+
num_heads=int(getattr(c, "num_heads", 4)),
|
| 96 |
+
head_dim=int(getattr(c, "head_dim", 64)),
|
| 97 |
+
patch_size=ps,
|
| 98 |
+
mlp_expansion=int(getattr(c, "mlp_expansion", 2)),
|
| 99 |
+
d_ff=int(getattr(c, "d_ff", 0)),
|
| 100 |
+
num_quantiles=int(getattr(c, "num_quantiles", 9)),
|
| 101 |
+
context_length=ctx,
|
| 102 |
+
horizon=hz,
|
| 103 |
+
max_patches=max(8, (ctx + hz) // ps + 4),
|
| 104 |
+
cpm_c_max=int(getattr(c, "cpm_c_max", 16)),
|
| 105 |
+
cpm_p_max=float(getattr(c, "cpm_p_max", 0.4)),
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
def to_dict(self) -> dict:
|
| 109 |
+
return {k: getattr(self, k) for k in self.__dataclass_fields__}
|
| 110 |
+
|
| 111 |
+
def layer_axis(self, i: int) -> str:
|
| 112 |
+
"""Attention axis of layer ``i``: the last layer of each group of
|
| 113 |
+
``layer_group_size`` attends over variates, the rest over time."""
|
| 114 |
+
g = max(1, self.layer_group_size)
|
| 115 |
+
return "variate" if i % g == g - 1 else "time"
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# ── robust causal scaler ──────────────────────────────────────────────────────
|
| 119 |
+
|
| 120 |
+
# Saturation bound on the standardized representation z = asinh((x-loc)/scale).
|
| 121 |
+
# Realistic data lives at |z| of a few (even a 1000σ event is asinh(1000)≈7.6), so
|
| 122 |
+
# this bound is never reached by honest corpora — clamp is the identity there. It
|
| 123 |
+
# exists purely as a backstop: it guarantees a finite, bounded z (and asinh target,
|
| 124 |
+
# clamped identically in the trainer) even for a pathological jump after an
|
| 125 |
+
# eps-clamped prefix, so the loss can never NaN or spike the shared training step.
|
| 126 |
+
# 64 leaves ~25 orders of asinh dynamic range above anything real; do NOT tighten
|
| 127 |
+
# it toward single digits without intent — that would start compressing legitimate
|
| 128 |
+
# heavy tails and change the scoring surface, not just add safety.
|
| 129 |
+
Z_CLAMP = 64.0
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def causal_standardize(
|
| 133 |
+
x: torch.Tensor,
|
| 134 |
+
mask: torch.Tensor | None = None,
|
| 135 |
+
*,
|
| 136 |
+
min_obs: int = 8,
|
| 137 |
+
eps: float = 1e-5,
|
| 138 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 139 |
+
"""Toto 2.0's robust causal scaler: per-step causal location/scale under an
|
| 140 |
+
arcsinh transform.
|
| 141 |
+
|
| 142 |
+
``x`` is ``(B, L)``; ``mask`` is an optional binary ``(B, L)`` with 1 =
|
| 143 |
+
unobserved — masked entries are excluded from the statistics, so the stats
|
| 144 |
+
carry forward unchanged across masked spans (matching inference, where
|
| 145 |
+
horizon mask patches contribute nothing). Steps whose causal window holds
|
| 146 |
+
fewer than ``min_obs`` observations are backfilled with the first stable
|
| 147 |
+
stats (the paper's leading-patch backfill). Returns ``(z, loc, scale)``
|
| 148 |
+
where ``z = arcsinh((x - loc) / scale)``; all three are ``(B, L)``.
|
| 149 |
+
"""
|
| 150 |
+
B, L = x.shape
|
| 151 |
+
keep = torch.ones_like(x) if mask is None else 1.0 - mask.to(x.dtype)
|
| 152 |
+
# The cumulative E[x²]−E[x]² form cancels catastrophically once
|
| 153 |
+
# mean²/var exceeds the dtype's precision (~1e7 in float32 — routine for
|
| 154 |
+
# counter/gauge-style series at large levels with small fluctuations),
|
| 155 |
+
# collapsing scale to eps. Accumulate in float64 and shift each row to its
|
| 156 |
+
# first observation so the moments stay small regardless of series level.
|
| 157 |
+
x64 = x.double()
|
| 158 |
+
k64 = keep.double()
|
| 159 |
+
ref = x64.gather(-1, (k64 > 0).to(torch.int64).argmax(dim=-1, keepdim=True))
|
| 160 |
+
xk = (x64 - ref) * k64
|
| 161 |
+
n = k64.cumsum(dim=-1)
|
| 162 |
+
cnt = n.clamp_min(1.0)
|
| 163 |
+
loc = xk.cumsum(dim=-1) / cnt
|
| 164 |
+
var = (xk * xk).cumsum(dim=-1) / cnt - loc * loc
|
| 165 |
+
loc = loc + ref
|
| 166 |
+
scale = var.clamp_min(0.0).sqrt().clamp_min(eps)
|
| 167 |
+
ok = n >= float(min_obs)
|
| 168 |
+
has = ok.any(dim=-1)
|
| 169 |
+
first = torch.where(
|
| 170 |
+
has, ok.to(torch.int64).argmax(dim=-1), torch.full((B,), L - 1, device=x.device)
|
| 171 |
+
)[:, None]
|
| 172 |
+
loc = torch.where(ok, loc, loc.gather(-1, first))
|
| 173 |
+
scale = torch.where(ok, scale, scale.gather(-1, first))
|
| 174 |
+
z = torch.asinh((x64 - loc) / scale).clamp_(-Z_CLAMP, Z_CLAMP)
|
| 175 |
+
return z.to(x.dtype), loc.to(x.dtype), scale.to(x.dtype)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def patch_anchors(loc: torch.Tensor, scale: torch.Tensor, patch_size: int) -> tuple[torch.Tensor, torch.Tensor]:
|
| 179 |
+
"""Causal stats at the last step of each patch — the scaling a forecast of
|
| 180 |
+
the *next* patch is anchored to. ``(B, L)`` → ``(B, P)`` each."""
|
| 181 |
+
B, L = loc.shape
|
| 182 |
+
P = L // patch_size
|
| 183 |
+
return (
|
| 184 |
+
loc.view(B, P, patch_size)[:, :, -1],
|
| 185 |
+
scale.view(B, P, patch_size)[:, :, -1],
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def invert_standardize(z: torch.Tensor, loc: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
|
| 190 |
+
"""Inverse of :func:`causal_standardize` at a fixed anchor:
|
| 191 |
+
``x = sinh(z) * scale + loc``."""
|
| 192 |
+
return torch.sinh(z) * scale + loc
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
# ── building blocks ───────────────────────────────────────────────────────────
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class _ResidualMLP(nn.Module):
|
| 199 |
+
"""Two-layer SiLU MLP with a residual connection — Toto 2.0's nonlinear
|
| 200 |
+
patch projection, used at both ends of the transformer. Bias-free (biases
|
| 201 |
+
live on attention projections, not MLPs)."""
|
| 202 |
+
|
| 203 |
+
def __init__(self, dim: int, hidden: int):
|
| 204 |
+
super().__init__()
|
| 205 |
+
self.net = nn.Sequential(
|
| 206 |
+
nn.Linear(dim, hidden, bias=False),
|
| 207 |
+
nn.SiLU(),
|
| 208 |
+
nn.Linear(hidden, dim, bias=False),
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 212 |
+
return x + self.net(x)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def _xpos(
|
| 216 |
+
q: torch.Tensor, k: torch.Tensor, inv_freq: torch.Tensor, zeta: torch.Tensor,
|
| 217 |
+
scale_base: float = 512.0,
|
| 218 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 219 |
+
"""xPos (arXiv 2212.10554, ``use_xpos`` in the Toto-2.0 release): rotary
|
| 220 |
+
position embedding with per-dimension exponential decay
|
| 221 |
+
``ζ̂_i = (i/(d/2) + γ)/(1 + γ)``, γ = 0.4 — queries scaled by ``ζ̂^m`` and
|
| 222 |
+
keys by ``ζ̂^{-m}`` over the sequence axis of ``(B, H, T, hd)``. Follows
|
| 223 |
+
the official torchscale implementation: the exponent is centered and
|
| 224 |
+
divided by ``scale_base`` (512) so ``ζ̂^{±m}`` stays representable."""
|
| 225 |
+
T = q.shape[-2]
|
| 226 |
+
t = torch.arange(T, device=q.device, dtype=inv_freq.dtype)
|
| 227 |
+
freqs = torch.outer(t, inv_freq) # (T, hd/2)
|
| 228 |
+
cos = freqs.cos().repeat_interleave(2, dim=-1) # (T, hd)
|
| 229 |
+
sin = freqs.sin().repeat_interleave(2, dim=-1)
|
| 230 |
+
power = ((t - T // 2) / scale_base)[:, None] # (T, 1)
|
| 231 |
+
scale = (zeta[None, :] ** power).repeat_interleave(2, dim=-1) # (T, hd)
|
| 232 |
+
|
| 233 |
+
def rotate(x):
|
| 234 |
+
x1 = x[..., 0::2]
|
| 235 |
+
x2 = x[..., 1::2]
|
| 236 |
+
return torch.stack((-x2, x1), dim=-1).flatten(-2)
|
| 237 |
+
|
| 238 |
+
return (q * cos + rotate(q) * sin) * scale, (k * cos + rotate(k) * sin) / scale
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
class _Block(nn.Module):
|
| 242 |
+
"""Pre-norm multi-head attention + GELU MLP.
|
| 243 |
+
|
| 244 |
+
``axis="time"``: causal over the patch axis with rotary positions.
|
| 245 |
+
``axis="variate"``: full attention over the variate axis (no positions —
|
| 246 |
+
variates are unordered). Both use PerDimScale query scaling with ``1/d_k``
|
| 247 |
+
attention scaling (μP-compatible), biases on attention projections only.
|
| 248 |
+
"""
|
| 249 |
+
|
| 250 |
+
def __init__(self, cfg: Toto2Config, axis: str, block_idx: int = 0):
|
| 251 |
+
super().__init__()
|
| 252 |
+
self.cfg = cfg
|
| 253 |
+
self.axis = axis
|
| 254 |
+
inner = cfg.num_heads * cfg.head_dim
|
| 255 |
+
# norm_eps = 1e-4, norm_include_weight = false — per the released config.
|
| 256 |
+
self.norm1 = nn.LayerNorm(cfg.d_model, eps=1e-4, elementwise_affine=False)
|
| 257 |
+
self.qkv = nn.Linear(cfg.d_model, 3 * inner, bias=True)
|
| 258 |
+
self.proj = nn.Linear(inner, cfg.d_model, bias=True)
|
| 259 |
+
self.norm2 = nn.LayerNorm(cfg.d_model, eps=1e-4, elementwise_affine=False)
|
| 260 |
+
hidden = cfg.ffn_hidden
|
| 261 |
+
self.mlp = nn.Sequential(
|
| 262 |
+
nn.Linear(cfg.d_model, hidden, bias=False),
|
| 263 |
+
nn.GELU(),
|
| 264 |
+
nn.Linear(hidden, cfg.d_model, bias=False),
|
| 265 |
+
)
|
| 266 |
+
# PerDimScale: learned per-dimension query scaling; softplus(0) = ln 2
|
| 267 |
+
# normalizer so the init is an exact no-op.
|
| 268 |
+
self.per_dim_scale = nn.Parameter(torch.zeros(cfg.head_dim))
|
| 269 |
+
if axis == "time":
|
| 270 |
+
half = cfg.head_dim // 2
|
| 271 |
+
idx = torch.arange(half).float() / max(1, half)
|
| 272 |
+
self.register_buffer("inv_freq", 1.0 / (10000.0**idx), persistent=False)
|
| 273 |
+
self.register_buffer("zeta", (idx + 0.4) / 1.4, persistent=False) # xPos γ=0.4
|
| 274 |
+
|
| 275 |
+
# u-μP residual scheme (u-μP eq. 25–31; Toto 2.0 §4.4): stream and
|
| 276 |
+
# branch combine as x ← b·x + a·branch with a² + b² = 1, keeping the
|
| 277 |
+
# residual stream at unit scale. α_res = residual_mult = 0.75 and
|
| 278 |
+
# α_res-attn-ratio = sqrt(S/log S) with S = context patches — exactly
|
| 279 |
+
# the released config's residual_mult / residual_attn_ratio (≈5.136
|
| 280 |
+
# at S = 128). Branches count attention and MLP separately (L = 2·layers).
|
| 281 |
+
S = max(2.0, cfg.context_length / cfg.patch_size)
|
| 282 |
+
ratio2 = S / math.log(S) # α_res-attn-ratio²
|
| 283 |
+
af2 = 2.0 * cfg.residual_mult**2 / (ratio2 + 1.0)
|
| 284 |
+
aa2 = ratio2 * af2
|
| 285 |
+
L = 2.0 * cfg.num_layers
|
| 286 |
+
i = block_idx
|
| 287 |
+
tau2_attn = aa2 / (L / 2.0 + i * aa2 + i * af2)
|
| 288 |
+
tau2_mlp = af2 / (L / 2.0 + (i + 1) * aa2 + i * af2)
|
| 289 |
+
self.attn_a = math.sqrt(tau2_attn / (tau2_attn + 1.0))
|
| 290 |
+
self.attn_b = math.sqrt(1.0 / (tau2_attn + 1.0))
|
| 291 |
+
self.mlp_a = math.sqrt(tau2_mlp / (tau2_mlp + 1.0))
|
| 292 |
+
self.mlp_b = math.sqrt(1.0 / (tau2_mlp + 1.0))
|
| 293 |
+
|
| 294 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 295 |
+
B, T, _ = x.shape
|
| 296 |
+
h = self.norm1(x)
|
| 297 |
+
qkv = self.qkv(h).view(B, T, 3, self.cfg.num_heads, self.cfg.head_dim)
|
| 298 |
+
q, k, v = qkv.unbind(dim=2)
|
| 299 |
+
q, k, v = (t.transpose(1, 2) for t in (q, k, v)) # (B, H, T, hd)
|
| 300 |
+
if self.axis == "time":
|
| 301 |
+
q, k = _xpos(q, k, self.inv_freq, self.zeta)
|
| 302 |
+
q = q * (F.softplus(self.per_dim_scale) / math.log(2.0))
|
| 303 |
+
attn = F.scaled_dot_product_attention(
|
| 304 |
+
q, k, v, is_causal=(self.axis == "time"), scale=1.0 / self.cfg.head_dim
|
| 305 |
+
)
|
| 306 |
+
attn = attn.transpose(1, 2).reshape(B, T, self.cfg.num_heads * self.cfg.head_dim)
|
| 307 |
+
x = self.attn_b * x + self.attn_a * self.proj(attn)
|
| 308 |
+
x = self.mlp_b * x + self.mlp_a * self.mlp(self.norm2(x))
|
| 309 |
+
return x
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
class Toto2Model(nn.Module):
|
| 313 |
+
"""Patch transformer with contiguous patch masking and alternating
|
| 314 |
+
time/variate attention, predicting the next patch's per-step quantiles.
|
| 315 |
+
|
| 316 |
+
Each input patch carries a binary mask channel (1 = unobserved entry);
|
| 317 |
+
masked entries are zeroed on input, so a masked patch contributes only its
|
| 318 |
+
position and mask bits. Training masks random contiguous spans (CPM);
|
| 319 |
+
inference appends fully-masked horizon patches and reads every horizon
|
| 320 |
+
patch's quantiles from a single forward pass.
|
| 321 |
+
"""
|
| 322 |
+
|
| 323 |
+
def __init__(self, cfg: Toto2Config):
|
| 324 |
+
super().__init__()
|
| 325 |
+
self.cfg = cfg
|
| 326 |
+
# values ‖ mask channel → 2×patch_size inputs per patch.
|
| 327 |
+
self.patch_embed = nn.Linear(cfg.patch_size * 2, cfg.d_model)
|
| 328 |
+
self.embed_mlp = _ResidualMLP(cfg.d_model, cfg.ffn_hidden)
|
| 329 |
+
# grouped layers: variate-axis attention closes each group of
|
| 330 |
+
# ``layer_group_size`` (Toto-2.0's 3-time-then-1-variate pattern).
|
| 331 |
+
self.blocks = nn.ModuleList(
|
| 332 |
+
_Block(cfg, axis=cfg.layer_axis(i), block_idx=i)
|
| 333 |
+
for i in range(cfg.num_layers)
|
| 334 |
+
)
|
| 335 |
+
self.norm = nn.LayerNorm(cfg.d_model, eps=1e-4, elementwise_affine=False)
|
| 336 |
+
self.out_mlp = _ResidualMLP(cfg.d_model, cfg.ffn_hidden)
|
| 337 |
+
# each position predicts the NEXT patch: patch_size steps × num_quantiles
|
| 338 |
+
self.head = nn.Linear(cfg.d_model, cfg.patch_size * cfg.num_quantiles)
|
| 339 |
+
self.apply(self._init_weights)
|
| 340 |
+
|
| 341 |
+
def _init_weights(self, m: nn.Module) -> None:
|
| 342 |
+
# u-μP-flavoured init: linear weights ~ N(0, 1/fan_in); the operator can
|
| 343 |
+
# swap in exact u-μP multipliers and pin base_arch_digest accordingly.
|
| 344 |
+
if isinstance(m, nn.Linear):
|
| 345 |
+
fan_in = m.weight.shape[1]
|
| 346 |
+
nn.init.normal_(m.weight, mean=0.0, std=1.0 / math.sqrt(fan_in))
|
| 347 |
+
if m.bias is not None:
|
| 348 |
+
nn.init.zeros_(m.bias)
|
| 349 |
+
elif isinstance(m, nn.Embedding):
|
| 350 |
+
nn.init.normal_(m.weight, mean=0.0, std=0.02)
|
| 351 |
+
|
| 352 |
+
def forward(self, patches: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:
|
| 353 |
+
"""``patches``: ``(B, P, patch_size)`` univariate or
|
| 354 |
+
``(B, C, P, patch_size)`` multivariate; ``mask``: optional binary
|
| 355 |
+
patch-level (``(B, P)`` / ``(B, C, P)``) or per-entry (same + trailing
|
| 356 |
+
``patch_size`` axis), 1 = unobserved. Returns predicted quantiles for
|
| 357 |
+
each position's *next* patch, shaped like the input with a trailing
|
| 358 |
+
``num_q`` axis: ``(B, [C,] P, patch_size, num_q)``."""
|
| 359 |
+
squeeze_variates = patches.dim() == 3
|
| 360 |
+
if squeeze_variates:
|
| 361 |
+
patches = patches[:, None] # (B, 1, P, ps)
|
| 362 |
+
if mask is not None:
|
| 363 |
+
mask = mask[:, None]
|
| 364 |
+
B, C, P, ps = patches.shape
|
| 365 |
+
if mask is None:
|
| 366 |
+
mask = torch.zeros_like(patches)
|
| 367 |
+
else:
|
| 368 |
+
if mask.dim() == 3:
|
| 369 |
+
mask = mask[..., None].expand(B, C, P, ps)
|
| 370 |
+
mask = mask.to(patches.dtype)
|
| 371 |
+
x = torch.cat([patches * (1.0 - mask), mask], dim=-1)
|
| 372 |
+
x = self.embed_mlp(self.patch_embed(x)) # (B, C, P, d)
|
| 373 |
+
for blk in self.blocks:
|
| 374 |
+
if blk.axis == "time":
|
| 375 |
+
x = blk(x.reshape(B * C, P, -1)).view(B, C, P, -1)
|
| 376 |
+
else:
|
| 377 |
+
x = (
|
| 378 |
+
blk(x.transpose(1, 2).reshape(B * P, C, -1))
|
| 379 |
+
.view(B, P, C, -1)
|
| 380 |
+
.transpose(1, 2)
|
| 381 |
+
)
|
| 382 |
+
x = self.out_mlp(self.norm(x))
|
| 383 |
+
out = self.head(x) # (B, C, P, ps*num_q)
|
| 384 |
+
out = out.view(B, C, P, ps, self.cfg.num_quantiles)
|
| 385 |
+
return out[:, 0] if squeeze_variates else out
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def pinball_loss(pred_q: torch.Tensor, target: torch.Tensor, levels: tuple[float, ...]) -> torch.Tensor:
|
| 389 |
+
"""Mean pinball (quantile) loss. ``pred_q`` ``(..., num_q)``, ``target``
|
| 390 |
+
``(...)`` broadcast over the quantile axis."""
|
| 391 |
+
q = torch.tensor(levels, device=pred_q.device, dtype=pred_q.dtype)
|
| 392 |
+
err = target.unsqueeze(-1) - pred_q
|
| 393 |
+
return torch.maximum(q * err, (q - 1.0) * err).mean()
|
weights.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:863175c3b99c326d0baaf63f5883bb1565438c5d9035d8f60b9c66abd3cf21e3
|
| 3 |
+
size 13031648
|