cascade r8-challenger checkpoint: best public-bench artifact (GIFT-Eval CRPS 0.6009)
b428368 verified | """Auto-generated by cascade Toto2Trainer. Loads the trained checkpoint and | |
| decodes the full horizon in one forward pass via contiguous patch masking | |
| (CPM) β no autoregressive sampling. Exposes: | |
| forecast(history, horizon, num_samples) -> (1, num_samples, horizon) | |
| the cascade validator contract β sample paths drawn once from the | |
| decoded quantiles (seeded per window for validator consensus). | |
| forecast_quantiles(history, horizon) -> (1, horizon, num_q) | |
| forecast_quantiles_batch(histories, horizon) -> (B, horizon, num_q) | |
| the quantile head directly β what benchmark CRPS consumes; batched | |
| across series so eval sweeps amortize the forward passes. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import importlib.util | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| # Single-pass CPM decoding is stable to ~768 steps (Toto 2.0 tech report); | |
| # longer horizons block-decode: commit the median per block, then continue. | |
| STABLE_DECODE_STEPS = 768 | |
| def _load_model_module(d: Path): | |
| spec = importlib.util.spec_from_file_location("cascade_ckpt_model", d / "model.py") | |
| mod = importlib.util.module_from_spec(spec) | |
| # Register before exec: model.py defines an @dataclass, and the dataclass | |
| # machinery does sys.modules.get(cls.__module__).__dict__ during class | |
| # creation β which is None (AttributeError) unless the module is registered. | |
| sys.modules[spec.name] = mod | |
| spec.loader.exec_module(mod) | |
| return mod | |
| class Wrapper: | |
| def __init__(self, checkpoint_dir, device: str = "cpu"): | |
| d = Path(checkpoint_dir) | |
| self.device = device | |
| cfg_obj = json.loads((d / "config.json").read_text()) | |
| self.m = _load_model_module(d) | |
| self.cfg = self.m.Toto2Config(**cfg_obj["toto2"]) | |
| self.quantile_levels = [float(v) for v in cfg_obj["quantile_levels"]] | |
| self.levels = torch.tensor(self.quantile_levels, dtype=torch.float32, device=device) | |
| self.model = self.m.Toto2Model(self.cfg).to(device).eval() | |
| from safetensors.torch import load_file | |
| state = load_file(str(d / "weights.safetensors")) | |
| self.model.load_state_dict(state) | |
| # ββ CPM decoding ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _prep(self, histories): | |
| """Left-pad (with the first value) or truncate each 1-D history to the | |
| context window. Returns the real-space context ``(B, window_len)`` in | |
| float64 β standardization happens per decode block, from full | |
| precision, so large-level series keep their fluctuations.""" | |
| ps = self.cfg.patch_size | |
| n_ctx = max(2, self.cfg.context_length // ps) | |
| window_len = n_ctx * ps | |
| rows = [] | |
| for h in histories: | |
| h = np.asarray(h, dtype=np.float64).reshape(-1) | |
| if h.shape[0] < window_len: | |
| pad = np.full(window_len - h.shape[0], h[0] if h.size else 0.0) | |
| h = np.concatenate([pad, h]) | |
| else: | |
| h = h[-window_len:] | |
| rows.append(h) | |
| return torch.as_tensor(np.stack(rows), dtype=torch.float64, device=self.device) | |
| def _decode_block_z(self, z, block: int): | |
| """One CPM forward pass: append ``block`` masked patches to the | |
| normalized context ``(B, L)`` and read their z-space quantiles | |
| ``(B, block*patch_size, num_q)``.""" | |
| ps = self.cfg.patch_size | |
| # keep as much context as the positional table allows | |
| ctx_p = min(z.shape[1] // ps, self.cfg.max_patches - block) | |
| ctx = z[:, -ctx_p * ps :].view(z.shape[0], ctx_p, ps) | |
| filler = torch.zeros(z.shape[0], block, ps, dtype=ctx.dtype, device=self.device) | |
| mask = torch.zeros(z.shape[0], ctx_p + block, dtype=ctx.dtype, device=self.device) | |
| mask[:, ctx_p:] = 1.0 | |
| pred = self.model(torch.cat([ctx, filler], dim=1), mask=mask) | |
| # position i predicts patch i+1 β the horizon patches come from | |
| # positions ctx_p-1 .. ctx_p+block-2. | |
| q = pred[:, ctx_p - 1 : ctx_p + block - 1] # (B, block, ps, nq) | |
| q, _ = torch.sort(q, dim=-1) # prevent quantile crossing | |
| return q.reshape(z.shape[0], block * ps, -1) | |
| def _decode_quantiles(self, x, horizon: int): | |
| """Block-decode real-space quantiles ``(B, horizon, num_q)`` from the | |
| real-space context ``x`` ``(B, L)``. | |
| Each block re-runs the causal scaler over history + committed medians | |
| and unscales with the resulting end-of-context anchor. Committed | |
| patches are *observed* context for later blocks, and in training the | |
| causal stats advance through every observed patch β so the anchor must | |
| advance with them; reusing the pre-horizon anchor would feed blocks β₯ 2 | |
| a scale/location regime the model never sees in training. Clamp bounds | |
| are fixed from the original context (min/max Β± 1e4x anchor scale, per | |
| the report) so committed medians can't widen them. | |
| """ | |
| ps = self.cfg.patch_size | |
| stable = max(1, min(STABLE_DECODE_STEPS // ps, self.cfg.max_patches - 2)) | |
| remaining = -(-int(horizon) // ps) | |
| lo = hi = None | |
| out = [] | |
| while remaining > 0: | |
| block = min(remaining, stable) | |
| z, loc_t, scale_t = self.m.causal_standardize(x) | |
| loc = loc_t[:, -1:].double().unsqueeze(-1) # (B, 1, 1) | |
| scale = scale_t[:, -1:].double().unsqueeze(-1) | |
| if lo is None: | |
| lo = x.min(dim=-1, keepdim=True).values.unsqueeze(-1) - 1e4 * scale | |
| hi = x.max(dim=-1, keepdim=True).values.unsqueeze(-1) + 1e4 * scale | |
| qz = self._decode_block_z(z.to(torch.float32), block) | |
| q = torch.sinh(qz.double()) * scale + loc # (B, block*ps, nq) | |
| q = torch.clamp(q, min=lo, max=hi) | |
| out.append(q) | |
| remaining -= block | |
| if remaining > 0: | |
| x = torch.cat([x, q[..., q.shape[-1] // 2]], dim=1) | |
| return torch.cat(out, dim=1)[:, : int(horizon)] | |
| # ββ quantile head (benchmark path) ββββββββββββββββββββββββββββββββββββββββ | |
| def forecast_quantiles_batch(self, histories, horizon: int) -> np.ndarray: | |
| """Decode ``len(histories)`` series in one batch β real-space quantiles | |
| ``(B, horizon, num_q)`` at ``self.quantile_levels``. arcsinh + affine | |
| are monotone increasing, so quantiles map pointwise.""" | |
| q = self._decode_quantiles(self._prep(list(histories)), horizon) | |
| return q.detach().cpu().numpy().astype(np.float64) | |
| def forecast_quantiles(self, history, horizon: int) -> np.ndarray: | |
| return self.forecast_quantiles_batch([history], horizon) | |
| # ββ validator contract (sample paths) βββββββββββββββββββββββββββββββββββββ | |
| def forecast(self, history, horizon: int, num_samples: int) -> np.ndarray: | |
| hist = np.asarray(history, dtype=np.float64).reshape(-1) | |
| # Deterministic per-window sampling: seed from the (raw history, horizon, | |
| # num_samples) so every validator computes identical scores and king vs | |
| # challenger share the uniform draws (paired Monte-Carlo). | |
| seed_src = hist.tobytes() + int(horizon).to_bytes(8, "big") + int(num_samples).to_bytes(8, "big") | |
| seed = int.from_bytes(hashlib.sha256(seed_src).digest()[:8], "big") & ((1 << 63) - 1) | |
| generator = torch.Generator(device=self.device) | |
| generator.manual_seed(seed) | |
| q = self._decode_quantiles(self._prep([hist]), horizon)[0] # (h, nq) real-space | |
| # One draw per step per path via the piecewise-linear inverse CDF of the | |
| # decoded quantiles (already clamped and monotone in the level). | |
| # Quantiles decode once; samples never feed back. | |
| nq = q.shape[-1] | |
| levels = self.levels | |
| u = torch.rand(int(num_samples), int(horizon), device=self.device, generator=generator) | |
| idx = torch.searchsorted(levels, u.clamp(levels[0].item(), levels[-1].item())) | |
| idx = idx.clamp(1, nq - 1) | |
| i_lo = idx - 1 | |
| i_hi = idx | |
| qe = q.unsqueeze(0).expand(u.shape[0], -1, -1) # (ns, h, nq) | |
| vl = torch.gather(qe, -1, i_lo.unsqueeze(-1)).squeeze(-1) | |
| vh = torch.gather(qe, -1, i_hi.unsqueeze(-1)).squeeze(-1) | |
| ql = levels[i_lo].double(); qh = levels[i_hi].double() | |
| frac = ((u.double() - ql) / (qh - ql).clamp_min(1e-8)).clamp(0, 1) | |
| out = vl + frac * (vh - vl) # (ns, h) | |
| return out.detach().cpu().numpy().reshape(1, int(num_samples), int(horizon)) | |