tensorlink-dev commited on
Commit
b41f44f
·
verified ·
1 Parent(s): 18a0615

cascade generator submission: checkpoint

Browse files
Files changed (4) hide show
  1. config.json +32 -0
  2. forecast_wrapper.py +174 -0
  3. model.py +393 -0
  4. 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