Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- README.md +25 -7
- app.py +331 -0
- requirements.txt +6 -0
- timebraid/__init__.py +69 -0
- timebraid/_vendor/__init__.py +1 -0
- timebraid/_vendor/timesfm/__init__.py +20 -0
- timebraid/_vendor/timesfm/configs.py +64 -0
- timebraid/_vendor/timesfm/timesfm_2p5/timesfm_2p5_base.py +74 -0
- timebraid/_vendor/timesfm/timesfm_2p5/timesfm_2p5_torch.py +257 -0
- timebraid/_vendor/timesfm/torch/__init__.py +14 -0
- timebraid/_vendor/timesfm/torch/dense.py +95 -0
- timebraid/_vendor/timesfm/torch/normalization.py +40 -0
- timebraid/_vendor/timesfm/torch/transformer.py +380 -0
- timebraid/_vendor/timesfm/torch/util.py +95 -0
- timebraid/data/__init__.py +1 -0
- timebraid/data/mot_utils.py +612 -0
- timebraid/model/__init__.py +35 -0
- timebraid/model/loading.py +1038 -0
- timebraid/model/mot/__init__.py +5 -0
- timebraid/model/mot/attention.py +447 -0
- timebraid/model/mot/bridge_runtime.py +471 -0
- timebraid/model/mot/bridge_runtime_forecast.py +1317 -0
- timebraid/model/mot/bridge_runtime_mot.py +0 -0
- timebraid/model/mot/config_contract.py +186 -0
- timebraid/model/mot/generation_route.py +223 -0
- timebraid/model/mot/kv_cache.py +1614 -0
- timebraid/model/mot/model.py +0 -0
- timebraid/model/mot/pairing.py +97 -0
- timebraid/model/mot/runtime_state.py +130 -0
- timebraid/model/mot/structures.py +270 -0
- timebraid/model/timebraid.py +1021 -0
- timebraid/processing_timebraid.py +726 -0
README.md
CHANGED
|
@@ -1,13 +1,31 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.29.1
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: TimeBraid
|
| 3 |
+
emoji: 🧵
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.29.1
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Time series + language model for analysis and forecasting
|
| 10 |
+
python_version: "3.12"
|
| 11 |
+
startup_duration_timeout: 30m
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# TimeBraid
|
| 15 |
+
|
| 16 |
+
Interactive demo of **TimeBraid-2.5B** — a unified time-series + language model that
|
| 17 |
+
interleaves a Qwen3-1.7B language backbone with TimesFM 2.5 200M time-series experts
|
| 18 |
+
through Mixture-of-Transformers layers.
|
| 19 |
+
|
| 20 |
+
Paste numeric series (one series per line, values comma- or space-separated) and ask a
|
| 21 |
+
question, or set a forecast horizon to get a numeric point forecast plotted against the
|
| 22 |
+
observed history.
|
| 23 |
+
|
| 24 |
+
- Model: [XinyueWangg/TimeBraid-2.5B](https://huggingface.co/XinyueWangg/TimeBraid-2.5B) (Apache-2.0)
|
| 25 |
+
- Code: [CharonWangg/TimeBraid](https://github.com/CharonWangg/TimeBraid)
|
| 26 |
+
- The `timebraid/` package in this Space is the authors' inference package, shipped
|
| 27 |
+
verbatim so `AutoModelForCausalLM` resolves the custom architecture with
|
| 28 |
+
`trust_remote_code=False`.
|
| 29 |
+
|
| 30 |
+
Runs on ZeroGPU. Example inputs are drawn from the authors'
|
| 31 |
+
`examples/inference_tasks.ipynb` notebook (Apache-2.0).
|
app.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TimeBraid: unified time series + language demo on ZeroGPU."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 6 |
+
|
| 7 |
+
import spaces # MUST come before any torch / CUDA-touching import
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
import gradio as gr
|
| 11 |
+
import matplotlib
|
| 12 |
+
|
| 13 |
+
matplotlib.use("Agg")
|
| 14 |
+
import matplotlib.pyplot as plt
|
| 15 |
+
import torch
|
| 16 |
+
from matplotlib.ticker import MaxNLocator
|
| 17 |
+
from transformers import AutoModelForCausalLM, AutoProcessor
|
| 18 |
+
|
| 19 |
+
from timebraid import TimeBraidProcessor # noqa: F401 - registers HF AutoClasses
|
| 20 |
+
|
| 21 |
+
MODEL_ID = "XinyueWangg/TimeBraid-2.5B"
|
| 22 |
+
|
| 23 |
+
# Chart conventions from the authors' notebook (examples/inference_tasks.ipynb).
|
| 24 |
+
_INPUT_COLOR = "#0072B2"
|
| 25 |
+
_FORECAST_COLOR = "#D55E00"
|
| 26 |
+
_BOUNDARY_COLOR = "#555555"
|
| 27 |
+
_SERIES_COLORS = ("#0072B2", "#009E73", "#CC79A7", "#E69F00")
|
| 28 |
+
|
| 29 |
+
processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=False)
|
| 30 |
+
model = (
|
| 31 |
+
AutoModelForCausalLM.from_pretrained(
|
| 32 |
+
MODEL_ID,
|
| 33 |
+
dtype=torch.bfloat16,
|
| 34 |
+
attn_implementation="flash_attention_2",
|
| 35 |
+
trust_remote_code=False,
|
| 36 |
+
use_safetensors=True,
|
| 37 |
+
)
|
| 38 |
+
.eval()
|
| 39 |
+
.to("cuda")
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def parse_series(text: str) -> list[list[float]]:
|
| 44 |
+
"""Parse one series per line; values comma- or whitespace-separated."""
|
| 45 |
+
if text is None:
|
| 46 |
+
return []
|
| 47 |
+
series: list[list[float]] = []
|
| 48 |
+
for line in text.splitlines():
|
| 49 |
+
line = line.strip().rstrip(",")
|
| 50 |
+
if not line:
|
| 51 |
+
continue
|
| 52 |
+
tokens = line.replace(",", " ").split()
|
| 53 |
+
values = [float(token) for token in tokens]
|
| 54 |
+
if not values:
|
| 55 |
+
raise ValueError("A series line parsed to zero values.")
|
| 56 |
+
series.append(values)
|
| 57 |
+
return series
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def render_plot(
|
| 61 |
+
history: list[list[float]],
|
| 62 |
+
forecast: list[float] | None,
|
| 63 |
+
target_index: int | None,
|
| 64 |
+
) -> matplotlib.figure.Figure:
|
| 65 |
+
"""Draw observed history and, when present, the forecast continuation."""
|
| 66 |
+
n = len(history)
|
| 67 |
+
fig, axes = plt.subplots(
|
| 68 |
+
n, 1, figsize=(9.5, 2.9 * n), constrained_layout=True, squeeze=False
|
| 69 |
+
)
|
| 70 |
+
for index, (ax, values) in enumerate(zip(axes[:, 0], history)):
|
| 71 |
+
color = _INPUT_COLOR if forecast is not None else _SERIES_COLORS[index % 4]
|
| 72 |
+
x = list(range(len(values)))
|
| 73 |
+
ax.plot(
|
| 74 |
+
x,
|
| 75 |
+
values,
|
| 76 |
+
color=color,
|
| 77 |
+
linewidth=2.0,
|
| 78 |
+
marker="o" if len(values) <= 64 else None,
|
| 79 |
+
markersize=4,
|
| 80 |
+
label="Observed history" if forecast is not None else f"Series {index + 1}",
|
| 81 |
+
)
|
| 82 |
+
title = f"INPUT TIMESERIES · Series {index + 1}"
|
| 83 |
+
if forecast is not None and target_index == index:
|
| 84 |
+
future_x = list(range(len(values), len(values) + len(forecast)))
|
| 85 |
+
boundary = len(values) - 0.5
|
| 86 |
+
ax.axvspan(boundary, future_x[-1] + 0.5, color=_FORECAST_COLOR, alpha=0.06)
|
| 87 |
+
ax.plot(
|
| 88 |
+
(len(values) - 1, *future_x),
|
| 89 |
+
(values[-1], *forecast),
|
| 90 |
+
color=_FORECAST_COLOR,
|
| 91 |
+
linewidth=2.0,
|
| 92 |
+
linestyle="--",
|
| 93 |
+
label="Point forecast",
|
| 94 |
+
)
|
| 95 |
+
ax.scatter(future_x, forecast, color=_FORECAST_COLOR, s=24, zorder=3)
|
| 96 |
+
ax.axvline(
|
| 97 |
+
boundary,
|
| 98 |
+
color=_BOUNDARY_COLOR,
|
| 99 |
+
linewidth=1.2,
|
| 100 |
+
linestyle=":",
|
| 101 |
+
label="Forecast boundary",
|
| 102 |
+
)
|
| 103 |
+
ax.text(
|
| 104 |
+
boundary,
|
| 105 |
+
1.01,
|
| 106 |
+
"forecast starts",
|
| 107 |
+
transform=ax.get_xaxis_transform(),
|
| 108 |
+
ha="center",
|
| 109 |
+
va="bottom",
|
| 110 |
+
fontsize=8,
|
| 111 |
+
color=_BOUNDARY_COLOR,
|
| 112 |
+
)
|
| 113 |
+
title = (
|
| 114 |
+
f"INPUT HISTORY + OUTPUT FORECAST · Series {index + 1} (forecast target)"
|
| 115 |
+
)
|
| 116 |
+
ax.set_title(title, loc="left", fontsize=11, fontweight="bold")
|
| 117 |
+
ax.set_xlabel("Observation / forecast index" if forecast is not None else "Observation index")
|
| 118 |
+
ax.set_ylabel("Value (raw scale)")
|
| 119 |
+
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
| 120 |
+
ax.grid(axis="y", color="#D9DCE1", linewidth=0.8, alpha=0.7)
|
| 121 |
+
ax.set_axisbelow(True)
|
| 122 |
+
ax.spines["top"].set_visible(False)
|
| 123 |
+
ax.spines["right"].set_visible(False)
|
| 124 |
+
ax.legend(loc="upper left", frameon=False, ncol=3, fontsize=9)
|
| 125 |
+
return fig
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@spaces.GPU(duration=120)
|
| 129 |
+
def run(
|
| 130 |
+
prompt: str,
|
| 131 |
+
series_text: str,
|
| 132 |
+
horizon: float,
|
| 133 |
+
target_series_index: float,
|
| 134 |
+
max_new_tokens: float,
|
| 135 |
+
progress=gr.Progress(track_tqdm=True),
|
| 136 |
+
):
|
| 137 |
+
"""Analyze or forecast a time series with TimeBraid.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
prompt: Question or instruction about the series (or a plain text question).
|
| 141 |
+
series_text: Numeric series, one per line; values comma- or space-separated.
|
| 142 |
+
horizon: Number of future points to forecast; 0 = no forecast (understanding only).
|
| 143 |
+
target_series_index: 1-based index of the series to forecast (multi-series only).
|
| 144 |
+
max_new_tokens: Maximum generated text tokens.
|
| 145 |
+
Returns:
|
| 146 |
+
(answer text, forecast values text, plot, elapsed seconds text).
|
| 147 |
+
"""
|
| 148 |
+
started = time.perf_counter()
|
| 149 |
+
try:
|
| 150 |
+
series = parse_series(series_text)
|
| 151 |
+
except ValueError as exc:
|
| 152 |
+
raise gr.Error(f"Could not parse the time series: {exc}")
|
| 153 |
+
|
| 154 |
+
horizon_i = int(horizon) if horizon and horizon > 0 else None
|
| 155 |
+
target_i: int | None = None
|
| 156 |
+
if horizon_i is not None:
|
| 157 |
+
if not series:
|
| 158 |
+
raise gr.Error("A forecast horizon needs at least one input series.")
|
| 159 |
+
if len(series) > 1:
|
| 160 |
+
if not target_series_index or target_series_index < 1:
|
| 161 |
+
raise gr.Error(
|
| 162 |
+
"With multiple series, pick which one to forecast (Target series)."
|
| 163 |
+
)
|
| 164 |
+
if int(target_series_index) > len(series):
|
| 165 |
+
raise gr.Error(
|
| 166 |
+
f"Target series is out of range: only {len(series)} series given."
|
| 167 |
+
)
|
| 168 |
+
target_i = int(target_series_index) - 1
|
| 169 |
+
|
| 170 |
+
messages = [{"role": "user", "content": prompt}]
|
| 171 |
+
inputs = processor.apply_chat_template(
|
| 172 |
+
messages,
|
| 173 |
+
timeseries=series if series else None,
|
| 174 |
+
horizon=horizon_i,
|
| 175 |
+
target_series_index=target_i,
|
| 176 |
+
add_generation_prompt=True,
|
| 177 |
+
tokenize=True,
|
| 178 |
+
return_dict=True,
|
| 179 |
+
return_tensors="pt",
|
| 180 |
+
).to(model.device)
|
| 181 |
+
with torch.inference_mode():
|
| 182 |
+
outputs = model.generate(
|
| 183 |
+
**inputs,
|
| 184 |
+
max_new_tokens=int(max_new_tokens),
|
| 185 |
+
do_sample=False,
|
| 186 |
+
num_beams=1,
|
| 187 |
+
num_return_sequences=1,
|
| 188 |
+
)
|
| 189 |
+
result = processor.post_process_generation(outputs, model_inputs=inputs)
|
| 190 |
+
elapsed = time.perf_counter() - started
|
| 191 |
+
|
| 192 |
+
answer = (result.get("content") or "").strip()
|
| 193 |
+
if not answer and horizon_i is None:
|
| 194 |
+
answer = "(empty response)"
|
| 195 |
+
ts = result.get("timeseries")
|
| 196 |
+
forecast_values = None
|
| 197 |
+
forecast_text = "No numeric forecast (understanding route)."
|
| 198 |
+
if ts is not None and ts.get("values"):
|
| 199 |
+
forecast_values = [float(v) for v in ts["values"]]
|
| 200 |
+
forecast_text = ", ".join(f"{v:.4g}" for v in forecast_values)
|
| 201 |
+
|
| 202 |
+
plot = render_plot(series, forecast_values, result.get("target_series_index"))
|
| 203 |
+
stats = (
|
| 204 |
+
f"route: {'forecast' if horizon_i is not None else 'understanding'} · "
|
| 205 |
+
f"prompt tokens: {result.get('prompt_tokens')} · "
|
| 206 |
+
f"completion tokens: {result.get('completion_tokens')} · "
|
| 207 |
+
f"finish: {result.get('finish_reason')} · {elapsed:.1f}s on GPU"
|
| 208 |
+
)
|
| 209 |
+
return answer, forecast_text, plot, stats
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
CSS = """
|
| 213 |
+
#col-container { max-width: 1150px; margin: 0 auto; }
|
| 214 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 215 |
+
"""
|
| 216 |
+
|
| 217 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="TimeBraid") as demo:
|
| 218 |
+
with gr.Column(elem_id="col-container"):
|
| 219 |
+
gr.Markdown(
|
| 220 |
+
"""
|
| 221 |
+
# 🧵 TimeBraid: Unifying Time Series and Language
|
| 222 |
+
Ask questions about numeric series, or request a point forecast.
|
| 223 |
+
One series per line, values comma- or space-separated. Leave the series
|
| 224 |
+
box empty for a pure text question. Set **Forecast horizon** to 0 for
|
| 225 |
+
analysis-only, or a positive number to get numeric forecast values.
|
| 226 |
+
**Model:** [XinyueWangg/TimeBraid-2.5B](https://huggingface.co/XinyueWangg/TimeBraid-2.5B) ·
|
| 227 |
+
**Code:** [GitHub](https://github.com/CharonWangg/TimeBraid) (Apache-2.0)
|
| 228 |
+
"""
|
| 229 |
+
)
|
| 230 |
+
with gr.Row():
|
| 231 |
+
with gr.Column(scale=1):
|
| 232 |
+
prompt = gr.Textbox(
|
| 233 |
+
label="Prompt",
|
| 234 |
+
placeholder="e.g. Describe the dominant trend and turning points, or forecast the next 8 values.",
|
| 235 |
+
lines=3,
|
| 236 |
+
)
|
| 237 |
+
series_box = gr.Textbox(
|
| 238 |
+
label="Time series (one series per line)",
|
| 239 |
+
placeholder="101.2, 101.8, 102.1, 102.5, 103.0, 103.4\n200, 208, 215, ...",
|
| 240 |
+
lines=5,
|
| 241 |
+
)
|
| 242 |
+
horizon = gr.Number(
|
| 243 |
+
label="Forecast horizon (0 = no forecast)",
|
| 244 |
+
value=0,
|
| 245 |
+
precision=0,
|
| 246 |
+
minimum=0,
|
| 247 |
+
)
|
| 248 |
+
run_btn = gr.Button("Run", variant="primary")
|
| 249 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 250 |
+
target_idx = gr.Number(
|
| 251 |
+
label="Target series (1-based; multi-series forecasts)",
|
| 252 |
+
value=1,
|
| 253 |
+
precision=0,
|
| 254 |
+
minimum=1,
|
| 255 |
+
)
|
| 256 |
+
max_new_tokens = gr.Slider(
|
| 257 |
+
label="Max new text tokens",
|
| 258 |
+
minimum=64,
|
| 259 |
+
maximum=1024,
|
| 260 |
+
value=256,
|
| 261 |
+
step=32,
|
| 262 |
+
)
|
| 263 |
+
with gr.Column(scale=1):
|
| 264 |
+
answer_out = gr.Textbox(label="Answer", lines=8, show_copy_button=True)
|
| 265 |
+
forecast_out = gr.Textbox(
|
| 266 |
+
label="Forecast values (raw scale)", show_copy_button=True
|
| 267 |
+
)
|
| 268 |
+
plot_out = gr.Plot(label="Series and forecast")
|
| 269 |
+
stats_out = gr.Markdown()
|
| 270 |
+
gr.Examples(
|
| 271 |
+
examples=[
|
| 272 |
+
[
|
| 273 |
+
"Forecast the next 8 values from the observed seasonal pattern.",
|
| 274 |
+
"100, 102, 105, 107, 103, 101, 99, 100, 103, 106, 108, 104, 102, 100, 101, 104",
|
| 275 |
+
8,
|
| 276 |
+
1,
|
| 277 |
+
256,
|
| 278 |
+
],
|
| 279 |
+
[
|
| 280 |
+
"A promotion starts at the first forecast step and is expected to lift demand above the recent seasonal baseline. Forecast the next 6 values.",
|
| 281 |
+
"200, 208, 215, 205, 198, 210, 218, 207, 201, 212, 220, 209",
|
| 282 |
+
6,
|
| 283 |
+
1,
|
| 284 |
+
256,
|
| 285 |
+
],
|
| 286 |
+
[
|
| 287 |
+
"Describe the dominant trend, major turning points, and whether the series becomes more volatile.",
|
| 288 |
+
"2.0, 2.1, 2.2, 2.4, 2.8, 3.1, 3.0, 2.9, 3.4, 3.8, 4.1, 4.3",
|
| 289 |
+
0,
|
| 290 |
+
1,
|
| 291 |
+
256,
|
| 292 |
+
],
|
| 293 |
+
[
|
| 294 |
+
"Does this series contain an isolated upward spike? Say only whether it occurs near the beginning, middle, or end; do not give a numeric index. Compare it with the neighboring level.",
|
| 295 |
+
"20.1, 20.0, 20.2, 20.1, 20.3, 34.8, 20.2, 20.1, 20.0, 20.2",
|
| 296 |
+
0,
|
| 297 |
+
1,
|
| 298 |
+
256,
|
| 299 |
+
],
|
| 300 |
+
[
|
| 301 |
+
"Series 1 is website visits and Series 2 is conversion rate. Compare their trends, volatility, and co-movement.",
|
| 302 |
+
"100, 104, 107, 111, 116, 120, 125, 129\n3.2, 3.1, 3.3, 3.4, 3.8, 3.7, 4.0, 4.2",
|
| 303 |
+
0,
|
| 304 |
+
1,
|
| 305 |
+
256,
|
| 306 |
+
],
|
| 307 |
+
[
|
| 308 |
+
"Explain in one sentence what a moving average reveals about a time series.",
|
| 309 |
+
"",
|
| 310 |
+
0,
|
| 311 |
+
1,
|
| 312 |
+
256,
|
| 313 |
+
],
|
| 314 |
+
],
|
| 315 |
+
inputs=[prompt, series_box, horizon, target_idx, max_new_tokens],
|
| 316 |
+
outputs=[answer_out, forecast_out, plot_out, stats_out],
|
| 317 |
+
fn=run,
|
| 318 |
+
cache_examples=True,
|
| 319 |
+
cache_mode="lazy",
|
| 320 |
+
label="Examples (from the authors' inference notebook)",
|
| 321 |
+
)
|
| 322 |
+
run_btn.click(
|
| 323 |
+
run,
|
| 324 |
+
inputs=[prompt, series_box, horizon, target_idx, max_new_tokens],
|
| 325 |
+
outputs=[answer_out, forecast_out, plot_out, stats_out],
|
| 326 |
+
api_name="run",
|
| 327 |
+
concurrency_limit=1,
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
if __name__ == "__main__":
|
| 331 |
+
demo.launch(mcp_server=True)
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers==4.57.6
|
| 2 |
+
accelerate==1.11.0
|
| 3 |
+
numpy==2.1.3
|
| 4 |
+
safetensors==0.5.3
|
| 5 |
+
matplotlib
|
| 6 |
+
https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt211-cu130-cp312/flash_attn-2.8.3-cp312-cp312-linux_x86_64.whl
|
timebraid/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public TimeBraid package with lazy Hugging Face runtime exports."""
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
# Runtime types
|
| 5 |
+
"GenerationRoute",
|
| 6 |
+
"TimeBraid",
|
| 7 |
+
"TimeBraidBatchFeature",
|
| 8 |
+
"TimeBraidConfig",
|
| 9 |
+
"TimeBraidGenerateOutput",
|
| 10 |
+
"TimeBraidLossBreakdown",
|
| 11 |
+
"TimeBraidOutput",
|
| 12 |
+
"TimeBraidProcessor",
|
| 13 |
+
# Registration
|
| 14 |
+
"register_timebraid_auto_classes",
|
| 15 |
+
# Loading and validation
|
| 16 |
+
"LoadedTimeBraid",
|
| 17 |
+
"ValidatedTimeBraidCheckpointSource",
|
| 18 |
+
"load_timebraid_checkpoint",
|
| 19 |
+
"load_timebraid_tokenizer",
|
| 20 |
+
"validate_timebraid_canonical_artifact",
|
| 21 |
+
"validate_timebraid_checkpoint_source",
|
| 22 |
+
"validate_timebraid_model",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def __getattr__(name: str):
|
| 27 |
+
if name not in __all__:
|
| 28 |
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
| 29 |
+
from .model import (
|
| 30 |
+
LoadedTimeBraid,
|
| 31 |
+
TimeBraid,
|
| 32 |
+
TimeBraidConfig,
|
| 33 |
+
TimeBraidGenerateOutput,
|
| 34 |
+
TimeBraidLossBreakdown,
|
| 35 |
+
TimeBraidOutput,
|
| 36 |
+
ValidatedTimeBraidCheckpointSource,
|
| 37 |
+
load_timebraid_checkpoint,
|
| 38 |
+
load_timebraid_tokenizer,
|
| 39 |
+
validate_timebraid_canonical_artifact,
|
| 40 |
+
validate_timebraid_checkpoint_source,
|
| 41 |
+
validate_timebraid_model,
|
| 42 |
+
)
|
| 43 |
+
from .model.mot.generation_route import GenerationRoute
|
| 44 |
+
from .model.timebraid import register_timebraid_auto_classes
|
| 45 |
+
from .processing_timebraid import TimeBraidBatchFeature, TimeBraidProcessor
|
| 46 |
+
|
| 47 |
+
# Importing any public name registers the Hugging Face auto classes, so
|
| 48 |
+
# `AutoModelForCausalLM.from_pretrained` resolves without remote code.
|
| 49 |
+
# `register_timebraid_auto_classes` is exported as well, because relying on
|
| 50 |
+
# an import side effect forces callers to suppress an unused-import warning.
|
| 51 |
+
register_timebraid_auto_classes()
|
| 52 |
+
return {
|
| 53 |
+
"GenerationRoute": GenerationRoute,
|
| 54 |
+
"LoadedTimeBraid": LoadedTimeBraid,
|
| 55 |
+
"TimeBraid": TimeBraid,
|
| 56 |
+
"TimeBraidBatchFeature": TimeBraidBatchFeature,
|
| 57 |
+
"TimeBraidConfig": TimeBraidConfig,
|
| 58 |
+
"TimeBraidGenerateOutput": TimeBraidGenerateOutput,
|
| 59 |
+
"TimeBraidLossBreakdown": TimeBraidLossBreakdown,
|
| 60 |
+
"TimeBraidOutput": TimeBraidOutput,
|
| 61 |
+
"TimeBraidProcessor": TimeBraidProcessor,
|
| 62 |
+
"ValidatedTimeBraidCheckpointSource": ValidatedTimeBraidCheckpointSource,
|
| 63 |
+
"load_timebraid_checkpoint": load_timebraid_checkpoint,
|
| 64 |
+
"load_timebraid_tokenizer": load_timebraid_tokenizer,
|
| 65 |
+
"register_timebraid_auto_classes": register_timebraid_auto_classes,
|
| 66 |
+
"validate_timebraid_canonical_artifact": validate_timebraid_canonical_artifact,
|
| 67 |
+
"validate_timebraid_checkpoint_source": validate_timebraid_checkpoint_source,
|
| 68 |
+
"validate_timebraid_model": validate_timebraid_model,
|
| 69 |
+
}[name]
|
timebraid/_vendor/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Third-party runtime code vendored for reproducible TimeBraid inference."""
|
timebraid/_vendor/timesfm/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified by TimeBraid contributors for the vendored PyTorch-only runtime.
|
| 2 |
+
# Copyright 2025 Google LLC
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Vendored TimesFM module surface used by TimeBraid."""
|
| 17 |
+
|
| 18 |
+
from .timesfm_2p5.timesfm_2p5_torch import TimesFM_2p5_200M_torch_module
|
| 19 |
+
|
| 20 |
+
__all__ = ["TimesFM_2p5_200M_torch_module"]
|
timebraid/_vendor/timesfm/configs.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified by TimeBraid contributors for the vendored PyTorch-only runtime.
|
| 2 |
+
# Copyright 2025 Google LLC
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Abstract configs for TimesFM layers."""
|
| 17 |
+
|
| 18 |
+
import dataclasses
|
| 19 |
+
from typing import Literal
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclasses.dataclass(frozen=True)
|
| 23 |
+
class ResidualBlockConfig:
|
| 24 |
+
"""Framework-agnostic config for a residual block."""
|
| 25 |
+
|
| 26 |
+
input_dims: int
|
| 27 |
+
hidden_dims: int
|
| 28 |
+
output_dims: int
|
| 29 |
+
use_bias: bool
|
| 30 |
+
activation: Literal["relu", "swish", "none"]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclasses.dataclass(frozen=True)
|
| 34 |
+
class RandomFourierFeaturesConfig:
|
| 35 |
+
"""Framework-agnostic config for random fourier features."""
|
| 36 |
+
|
| 37 |
+
input_dims: int
|
| 38 |
+
output_dims: int
|
| 39 |
+
projection_stddev: float
|
| 40 |
+
use_bias: bool
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@dataclasses.dataclass(frozen=True)
|
| 44 |
+
class TransformerConfig:
|
| 45 |
+
"""Framework-agnostic config for a transformer."""
|
| 46 |
+
|
| 47 |
+
model_dims: int
|
| 48 |
+
hidden_dims: int
|
| 49 |
+
num_heads: int
|
| 50 |
+
attention_norm: Literal["rms"]
|
| 51 |
+
feedforward_norm: Literal["rms"]
|
| 52 |
+
qk_norm: Literal["rms", "none"]
|
| 53 |
+
use_bias: bool
|
| 54 |
+
use_rotary_position_embeddings: bool
|
| 55 |
+
ff_activation: Literal["relu", "swish", "none"]
|
| 56 |
+
fuse_qkv: bool
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@dataclasses.dataclass(frozen=True)
|
| 60 |
+
class StackedTransformersConfig:
|
| 61 |
+
"""Framework-agnostic config for a stacked transformers."""
|
| 62 |
+
|
| 63 |
+
num_layers: int
|
| 64 |
+
transformer: TransformerConfig
|
timebraid/_vendor/timesfm/timesfm_2p5/timesfm_2p5_base.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified by TimeBraid contributors for the vendored PyTorch-only runtime.
|
| 2 |
+
# Copyright 2025 Google LLC
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""TimesFM 2p5 base implementation."""
|
| 17 |
+
|
| 18 |
+
import dataclasses
|
| 19 |
+
|
| 20 |
+
from .. import configs
|
| 21 |
+
|
| 22 |
+
ResidualBlockConfig = configs.ResidualBlockConfig
|
| 23 |
+
StackedTransformersConfig = configs.StackedTransformersConfig
|
| 24 |
+
TransformerConfig = configs.TransformerConfig
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclasses.dataclass(frozen=True)
|
| 28 |
+
class TimesFM_2p5_200M_Definition:
|
| 29 |
+
"""Framework-agnostic config of TimesFM 2.5."""
|
| 30 |
+
|
| 31 |
+
context_limit = 16384
|
| 32 |
+
input_patch_len: int = 32
|
| 33 |
+
output_patch_len: int = 128
|
| 34 |
+
output_quantile_len: int = 1024
|
| 35 |
+
quantiles: list[float] = dataclasses.field(
|
| 36 |
+
default_factory=lambda: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
| 37 |
+
)
|
| 38 |
+
decode_index: int = 5
|
| 39 |
+
tokenizer: ResidualBlockConfig = ResidualBlockConfig(
|
| 40 |
+
input_dims=64,
|
| 41 |
+
hidden_dims=1280,
|
| 42 |
+
output_dims=1280,
|
| 43 |
+
use_bias=True,
|
| 44 |
+
activation="swish",
|
| 45 |
+
)
|
| 46 |
+
stacked_transformers: StackedTransformersConfig = StackedTransformersConfig(
|
| 47 |
+
num_layers=20,
|
| 48 |
+
transformer=TransformerConfig(
|
| 49 |
+
model_dims=1280,
|
| 50 |
+
hidden_dims=1280,
|
| 51 |
+
num_heads=16,
|
| 52 |
+
attention_norm="rms",
|
| 53 |
+
feedforward_norm="rms",
|
| 54 |
+
qk_norm="rms",
|
| 55 |
+
use_bias=False,
|
| 56 |
+
use_rotary_position_embeddings=True,
|
| 57 |
+
ff_activation="swish",
|
| 58 |
+
fuse_qkv=True,
|
| 59 |
+
),
|
| 60 |
+
)
|
| 61 |
+
output_projection_point: ResidualBlockConfig = ResidualBlockConfig(
|
| 62 |
+
input_dims=1280,
|
| 63 |
+
hidden_dims=1280,
|
| 64 |
+
output_dims=1280,
|
| 65 |
+
use_bias=False,
|
| 66 |
+
activation="swish",
|
| 67 |
+
)
|
| 68 |
+
output_projection_quantiles: ResidualBlockConfig = ResidualBlockConfig(
|
| 69 |
+
input_dims=1280,
|
| 70 |
+
hidden_dims=1280,
|
| 71 |
+
output_dims=10240,
|
| 72 |
+
use_bias=False,
|
| 73 |
+
activation="swish",
|
| 74 |
+
)
|
timebraid/_vendor/timesfm/timesfm_2p5/timesfm_2p5_torch.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified by TimeBraid contributors for the vendored PyTorch-only runtime.
|
| 2 |
+
# Copyright 2025 Google LLC
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
"""TimesFM models."""
|
| 16 |
+
|
| 17 |
+
from typing import Sequence
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
import torch
|
| 21 |
+
from safetensors.torch import load_file
|
| 22 |
+
from torch import nn
|
| 23 |
+
|
| 24 |
+
from ..torch import dense, transformer, util
|
| 25 |
+
from . import timesfm_2p5_base
|
| 26 |
+
|
| 27 |
+
revin = util.revin
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class TimesFM_2p5_200M_torch_module(nn.Module):
|
| 31 |
+
"""TimesFM 2.5 with 200M parameters."""
|
| 32 |
+
|
| 33 |
+
config = timesfm_2p5_base.TimesFM_2p5_200M_Definition()
|
| 34 |
+
|
| 35 |
+
def __init__(self):
|
| 36 |
+
super().__init__()
|
| 37 |
+
|
| 38 |
+
# Names constants.
|
| 39 |
+
self.p = self.config.input_patch_len # 32
|
| 40 |
+
self.o = self.config.output_patch_len # 128
|
| 41 |
+
self.os = self.config.output_quantile_len # 1024
|
| 42 |
+
self.m = self.o // self.p # 4
|
| 43 |
+
self.x = self.config.stacked_transformers.num_layers # 20
|
| 44 |
+
self.h = self.config.stacked_transformers.transformer.num_heads # 16
|
| 45 |
+
self.md = self.config.stacked_transformers.transformer.model_dims # 1280
|
| 46 |
+
self.hd = self.md // self.h # 80
|
| 47 |
+
self.q = len(self.config.quantiles) + 1 # 10
|
| 48 |
+
self.aridx = self.config.decode_index # 5
|
| 49 |
+
|
| 50 |
+
# Layers.
|
| 51 |
+
self.tokenizer = dense.ResidualBlock(self.config.tokenizer)
|
| 52 |
+
self.stacked_xf = nn.ModuleList(
|
| 53 |
+
[
|
| 54 |
+
transformer.Transformer(self.config.stacked_transformers.transformer)
|
| 55 |
+
for _ in range(self.x)
|
| 56 |
+
]
|
| 57 |
+
)
|
| 58 |
+
self.output_projection_point = dense.ResidualBlock(
|
| 59 |
+
self.config.output_projection_point
|
| 60 |
+
)
|
| 61 |
+
self.output_projection_quantiles = dense.ResidualBlock(
|
| 62 |
+
self.config.output_projection_quantiles
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Device.
|
| 66 |
+
if torch.cuda.is_available():
|
| 67 |
+
self.device = torch.device("cuda:0")
|
| 68 |
+
self.device_count = torch.cuda.device_count()
|
| 69 |
+
else:
|
| 70 |
+
self.device = torch.device("cpu")
|
| 71 |
+
self.device_count = 1
|
| 72 |
+
|
| 73 |
+
def load_checkpoint(self, path: str, **kwargs):
|
| 74 |
+
"""Loads a PyTorch TimesFM model from a checkpoint."""
|
| 75 |
+
tensors = load_file(path)
|
| 76 |
+
self.load_state_dict(tensors, strict=True)
|
| 77 |
+
self.to(self.device)
|
| 78 |
+
torch_compile = True
|
| 79 |
+
if "torch_compile" in kwargs:
|
| 80 |
+
torch_compile = kwargs["torch_compile"]
|
| 81 |
+
if torch_compile:
|
| 82 |
+
print("Compiling model...")
|
| 83 |
+
self = torch.compile(self)
|
| 84 |
+
|
| 85 |
+
self.eval()
|
| 86 |
+
|
| 87 |
+
def forward(
|
| 88 |
+
self,
|
| 89 |
+
inputs: torch.Tensor,
|
| 90 |
+
masks: torch.Tensor,
|
| 91 |
+
decode_caches: list[util.DecodeCache] | None = None,
|
| 92 |
+
):
|
| 93 |
+
tokenizer_inputs = torch.cat([inputs, masks.to(inputs.dtype)], dim=-1)
|
| 94 |
+
input_embeddings = self.tokenizer(tokenizer_inputs)
|
| 95 |
+
|
| 96 |
+
if decode_caches is None:
|
| 97 |
+
decode_caches = [None] * self.x
|
| 98 |
+
|
| 99 |
+
output_embeddings = input_embeddings
|
| 100 |
+
new_decode_caches = []
|
| 101 |
+
for i, layer in enumerate(self.stacked_xf):
|
| 102 |
+
output_embeddings, new_cache = layer(
|
| 103 |
+
output_embeddings, masks[..., -1], decode_caches[i]
|
| 104 |
+
)
|
| 105 |
+
new_decode_caches.append(new_cache)
|
| 106 |
+
output_ts = self.output_projection_point(output_embeddings)
|
| 107 |
+
output_quantile_spread = self.output_projection_quantiles(output_embeddings)
|
| 108 |
+
|
| 109 |
+
return (
|
| 110 |
+
input_embeddings,
|
| 111 |
+
output_embeddings,
|
| 112 |
+
output_ts,
|
| 113 |
+
output_quantile_spread,
|
| 114 |
+
), new_decode_caches
|
| 115 |
+
|
| 116 |
+
def decode(self, horizon: int, inputs, masks):
|
| 117 |
+
"""Decodes the time series."""
|
| 118 |
+
|
| 119 |
+
with torch.no_grad():
|
| 120 |
+
batch_size, context = inputs.shape[0], inputs.shape[1]
|
| 121 |
+
num_decode_steps = (horizon - 1) // self.o
|
| 122 |
+
num_input_patches = context // self.p
|
| 123 |
+
decode_cache_size = num_input_patches + num_decode_steps * self.m
|
| 124 |
+
|
| 125 |
+
# Prefill
|
| 126 |
+
patched_inputs = torch.reshape(inputs, (batch_size, -1, self.p))
|
| 127 |
+
patched_masks = torch.reshape(masks, (batch_size, -1, self.p))
|
| 128 |
+
|
| 129 |
+
# running stats
|
| 130 |
+
n = torch.zeros(batch_size, device=inputs.device)
|
| 131 |
+
mu = torch.zeros(batch_size, device=inputs.device)
|
| 132 |
+
sigma = torch.zeros(batch_size, device=inputs.device)
|
| 133 |
+
patch_mu = []
|
| 134 |
+
patch_sigma = []
|
| 135 |
+
for i in range(num_input_patches):
|
| 136 |
+
(n, mu, sigma), _ = util.update_running_stats(
|
| 137 |
+
n, mu, sigma, patched_inputs[:, i], patched_masks[:, i]
|
| 138 |
+
)
|
| 139 |
+
patch_mu.append(mu)
|
| 140 |
+
patch_sigma.append(sigma)
|
| 141 |
+
last_n, last_mu, last_sigma = n, mu, sigma
|
| 142 |
+
context_mu = torch.stack(patch_mu, dim=1)
|
| 143 |
+
context_sigma = torch.stack(patch_sigma, dim=1)
|
| 144 |
+
|
| 145 |
+
decode_caches = [
|
| 146 |
+
util.DecodeCache(
|
| 147 |
+
next_index=torch.zeros(batch_size, dtype=torch.int32, device=inputs.device),
|
| 148 |
+
num_masked=torch.zeros(batch_size, dtype=torch.int32, device=inputs.device),
|
| 149 |
+
key=torch.zeros(
|
| 150 |
+
batch_size,
|
| 151 |
+
decode_cache_size,
|
| 152 |
+
self.h,
|
| 153 |
+
self.hd,
|
| 154 |
+
device=inputs.device,
|
| 155 |
+
),
|
| 156 |
+
value=torch.zeros(
|
| 157 |
+
batch_size,
|
| 158 |
+
decode_cache_size,
|
| 159 |
+
self.h,
|
| 160 |
+
self.hd,
|
| 161 |
+
device=inputs.device,
|
| 162 |
+
),
|
| 163 |
+
)
|
| 164 |
+
for _ in range(self.x)
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
normed_inputs = revin(patched_inputs, context_mu, context_sigma, reverse=False)
|
| 168 |
+
normed_inputs = torch.where(patched_masks, 0.0, normed_inputs)
|
| 169 |
+
(_, _, normed_outputs, normed_quantile_spread), decode_caches = self(
|
| 170 |
+
normed_inputs, patched_masks, decode_caches
|
| 171 |
+
)
|
| 172 |
+
renormed_outputs = torch.reshape(
|
| 173 |
+
revin(normed_outputs, context_mu, context_sigma, reverse=True),
|
| 174 |
+
(batch_size, -1, self.o, self.q),
|
| 175 |
+
)
|
| 176 |
+
renormed_quantile_spread = torch.reshape(
|
| 177 |
+
revin(normed_quantile_spread, context_mu, context_sigma, reverse=True),
|
| 178 |
+
(batch_size, -1, self.os, self.q),
|
| 179 |
+
)[:, -1, ...]
|
| 180 |
+
|
| 181 |
+
# Autogressive decode
|
| 182 |
+
ar_outputs = []
|
| 183 |
+
last_renormed_output = renormed_outputs[:, -1, :, self.aridx]
|
| 184 |
+
|
| 185 |
+
for _ in range(num_decode_steps):
|
| 186 |
+
new_patched_input = torch.reshape(
|
| 187 |
+
last_renormed_output, (batch_size, self.m, self.p)
|
| 188 |
+
)
|
| 189 |
+
new_mask = torch.zeros_like(new_patched_input, dtype=torch.bool)
|
| 190 |
+
|
| 191 |
+
n, mu, sigma = last_n, last_mu, last_sigma
|
| 192 |
+
new_mus, new_sigmas = [], []
|
| 193 |
+
for i in range(self.m):
|
| 194 |
+
(n, mu, sigma), _ = util.update_running_stats(
|
| 195 |
+
n, mu, sigma, new_patched_input[:, i], new_mask[:, i]
|
| 196 |
+
)
|
| 197 |
+
new_mus.append(mu)
|
| 198 |
+
new_sigmas.append(sigma)
|
| 199 |
+
last_n, last_mu, last_sigma = n, mu, sigma
|
| 200 |
+
new_mu = torch.stack(new_mus, dim=1)
|
| 201 |
+
new_sigma = torch.stack(new_sigmas, dim=1)
|
| 202 |
+
|
| 203 |
+
new_normed_input = revin(new_patched_input, new_mu, new_sigma, reverse=False)
|
| 204 |
+
(_, _, new_normed_output, _), decode_caches = self(
|
| 205 |
+
new_normed_input, new_mask, decode_caches
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
new_renormed_output = torch.reshape(
|
| 209 |
+
revin(new_normed_output, new_mu, new_sigma, reverse=True),
|
| 210 |
+
(batch_size, self.m, self.o, self.q),
|
| 211 |
+
)
|
| 212 |
+
ar_outputs.append(new_renormed_output[:, -1, ...])
|
| 213 |
+
last_renormed_output = new_renormed_output[:, -1, :, self.aridx]
|
| 214 |
+
|
| 215 |
+
if num_decode_steps > 0:
|
| 216 |
+
ar_renormed_outputs = torch.stack(ar_outputs, dim=1)
|
| 217 |
+
else:
|
| 218 |
+
ar_renormed_outputs = None
|
| 219 |
+
|
| 220 |
+
return renormed_outputs, renormed_quantile_spread, ar_renormed_outputs
|
| 221 |
+
|
| 222 |
+
def forecast_naive(
|
| 223 |
+
self, horizon: int, inputs: Sequence[np.ndarray]
|
| 224 |
+
) -> list[np.ndarray]:
|
| 225 |
+
"""Forecasts the time series.
|
| 226 |
+
|
| 227 |
+
This is a naive implementation for debugging purposes. No forecasting
|
| 228 |
+
flags are used here. Forecasting quality can be subpar.
|
| 229 |
+
|
| 230 |
+
Args:
|
| 231 |
+
horizon: The number of time points to forecast.
|
| 232 |
+
inputs: A sequence of numpy arrays, each representing a time series to
|
| 233 |
+
query forecast for.
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
A list of numpy arrays of forecasts.
|
| 237 |
+
"""
|
| 238 |
+
outputs = []
|
| 239 |
+
for each_input in inputs:
|
| 240 |
+
input_t = torch.tensor(each_input, dtype=torch.float32)
|
| 241 |
+
mask = torch.zeros_like(input_t, dtype=torch.bool)
|
| 242 |
+
len_front_mask = self.p - (len(each_input) % self.p)
|
| 243 |
+
if len_front_mask < self.p:
|
| 244 |
+
input_t = torch.cat(
|
| 245 |
+
[torch.zeros(len_front_mask, dtype=torch.float32), input_t], dim=0
|
| 246 |
+
)
|
| 247 |
+
mask = torch.cat([torch.ones(len_front_mask, dtype=torch.bool), mask], dim=0)
|
| 248 |
+
input_t = input_t[None, ...]
|
| 249 |
+
mask = mask[None, ...]
|
| 250 |
+
t_pf, _, t_ar = self.decode(horizon, input_t, mask)
|
| 251 |
+
to_concat = [t_pf[:, -1, ...]]
|
| 252 |
+
if t_ar is not None:
|
| 253 |
+
to_concat.append(t_ar.reshape(1, -1, self.q))
|
| 254 |
+
torch_forecast = torch.cat(to_concat, dim=1)[..., :horizon]
|
| 255 |
+
torch_forecast = torch_forecast.squeeze(0)
|
| 256 |
+
outputs.append(torch_forecast.detach().cpu().numpy())
|
| 257 |
+
return outputs
|
timebraid/_vendor/timesfm/torch/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified by TimeBraid contributors for the vendored PyTorch-only runtime.
|
| 2 |
+
# Copyright 2025 Google LLC
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
timebraid/_vendor/timesfm/torch/dense.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified by TimeBraid contributors for the vendored PyTorch-only runtime.
|
| 2 |
+
# Copyright 2025 Google LLC
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Dense layers for TimesFM."""
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
from torch import nn
|
| 20 |
+
|
| 21 |
+
from .. import configs
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ResidualBlock(nn.Module):
|
| 25 |
+
"""Residual block with two linear layers and a linear residual connection."""
|
| 26 |
+
|
| 27 |
+
def __init__(self, config: configs.ResidualBlockConfig):
|
| 28 |
+
super().__init__()
|
| 29 |
+
self.config = config
|
| 30 |
+
self.hidden_layer = nn.Linear(
|
| 31 |
+
in_features=config.input_dims,
|
| 32 |
+
out_features=config.hidden_dims,
|
| 33 |
+
bias=config.use_bias,
|
| 34 |
+
)
|
| 35 |
+
self.output_layer = nn.Linear(
|
| 36 |
+
in_features=config.hidden_dims,
|
| 37 |
+
out_features=config.output_dims,
|
| 38 |
+
bias=config.use_bias,
|
| 39 |
+
)
|
| 40 |
+
self.residual_layer = nn.Linear(
|
| 41 |
+
in_features=config.input_dims,
|
| 42 |
+
out_features=config.output_dims,
|
| 43 |
+
bias=config.use_bias,
|
| 44 |
+
)
|
| 45 |
+
if config.activation == "relu":
|
| 46 |
+
self.activation = nn.ReLU()
|
| 47 |
+
elif config.activation == "swish":
|
| 48 |
+
self.activation = nn.SiLU()
|
| 49 |
+
elif config.activation == "none":
|
| 50 |
+
self.activation = nn.Identity()
|
| 51 |
+
else:
|
| 52 |
+
raise ValueError(f"Activation: {config.activation} not supported.")
|
| 53 |
+
|
| 54 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 55 |
+
return self.output_layer(
|
| 56 |
+
self.activation(self.hidden_layer(x))
|
| 57 |
+
) + self.residual_layer(x)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class RandomFourierFeatures(nn.Module):
|
| 61 |
+
"""Random Fourier features layer."""
|
| 62 |
+
|
| 63 |
+
def __init__(self, config: configs.RandomFourierFeaturesConfig):
|
| 64 |
+
super().__init__()
|
| 65 |
+
self.config = config
|
| 66 |
+
|
| 67 |
+
if config.output_dims % 4 != 0:
|
| 68 |
+
raise ValueError(
|
| 69 |
+
f"Output dims must be a multiple of 4: {config.output_dims} % 4 != 0."
|
| 70 |
+
)
|
| 71 |
+
num_projected_features = config.output_dims // 4
|
| 72 |
+
|
| 73 |
+
self.phase_shifts = nn.Parameter(torch.zeros(2, num_projected_features))
|
| 74 |
+
self.projection_layer = nn.Linear(
|
| 75 |
+
in_features=config.input_dims,
|
| 76 |
+
out_features=num_projected_features,
|
| 77 |
+
bias=config.use_bias,
|
| 78 |
+
)
|
| 79 |
+
self.residual_layer = nn.Linear(
|
| 80 |
+
in_features=config.input_dims,
|
| 81 |
+
out_features=config.output_dims,
|
| 82 |
+
bias=config.use_bias,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 86 |
+
projected = self.projection_layer(x)
|
| 87 |
+
cos_features = torch.cos(projected)
|
| 88 |
+
sin_features = torch.sin(projected)
|
| 89 |
+
sq_wave_1 = torch.sign(torch.sin(projected + self.phase_shifts[0, :]))
|
| 90 |
+
sq_wave_2 = torch.sign(torch.sin(projected + self.phase_shifts[1, :]))
|
| 91 |
+
fourier_features = torch.cat(
|
| 92 |
+
[cos_features, sin_features, sq_wave_1, sq_wave_2], dim=-1
|
| 93 |
+
)
|
| 94 |
+
residual = self.residual_layer(x)
|
| 95 |
+
return fourier_features + residual
|
timebraid/_vendor/timesfm/torch/normalization.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified by TimeBraid contributors for the vendored PyTorch-only runtime.
|
| 2 |
+
# Copyright 2025 Google LLC
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Normalization layers for TimesFM."""
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
from torch import nn
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class RMSNorm(nn.Module):
|
| 23 |
+
"""RMS normalization."""
|
| 24 |
+
|
| 25 |
+
def __init__(
|
| 26 |
+
self,
|
| 27 |
+
num_features: int,
|
| 28 |
+
*,
|
| 29 |
+
epsilon: float = 1e-6,
|
| 30 |
+
):
|
| 31 |
+
super().__init__()
|
| 32 |
+
self.scale = nn.Parameter(torch.zeros(num_features))
|
| 33 |
+
self.num_features = num_features
|
| 34 |
+
self.epsilon = epsilon
|
| 35 |
+
|
| 36 |
+
def forward(self, inputs: torch.Tensor) -> torch.Tensor:
|
| 37 |
+
var = torch.mean(torch.square(inputs), dim=-1, keepdim=True)
|
| 38 |
+
normed_inputs = inputs * torch.rsqrt(var + self.epsilon)
|
| 39 |
+
normed_inputs = normed_inputs * self.scale
|
| 40 |
+
return normed_inputs
|
timebraid/_vendor/timesfm/torch/transformer.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified by TimeBraid contributors for the vendored PyTorch-only runtime.
|
| 2 |
+
# Copyright 2025 Google LLC
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Transformer layers for TimesFM."""
|
| 17 |
+
|
| 18 |
+
import math
|
| 19 |
+
from typing import Callable
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
from torch import nn
|
| 24 |
+
|
| 25 |
+
from .. import configs
|
| 26 |
+
from . import normalization, util
|
| 27 |
+
|
| 28 |
+
LayerNorm = nn.LayerNorm
|
| 29 |
+
RMSNorm = normalization.RMSNorm
|
| 30 |
+
DecodeCache = util.DecodeCache
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def make_attn_mask(
|
| 34 |
+
query_length: int,
|
| 35 |
+
num_all_masked_kv: torch.Tensor,
|
| 36 |
+
query_index_offset: torch.Tensor | None = None,
|
| 37 |
+
kv_length: int = 0,
|
| 38 |
+
) -> torch.Tensor:
|
| 39 |
+
"""Makes attention mask."""
|
| 40 |
+
if kv_length == 0:
|
| 41 |
+
kv_length = query_length
|
| 42 |
+
|
| 43 |
+
q_index = torch.arange(query_length, device=num_all_masked_kv.device)[
|
| 44 |
+
None, None, :, None
|
| 45 |
+
]
|
| 46 |
+
if query_index_offset is not None:
|
| 47 |
+
q_index = q_index + query_index_offset[:, None, None, None]
|
| 48 |
+
kv_index = torch.arange(kv_length, device=num_all_masked_kv.device)[
|
| 49 |
+
None, None, None, :
|
| 50 |
+
]
|
| 51 |
+
return torch.logical_and(
|
| 52 |
+
q_index >= kv_index,
|
| 53 |
+
kv_index >= num_all_masked_kv[:, None, None, None],
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class RotaryPositionalEmbedding(nn.Module):
|
| 58 |
+
"""Rotary positional embedding."""
|
| 59 |
+
|
| 60 |
+
def __init__(
|
| 61 |
+
self,
|
| 62 |
+
embedding_dims: int,
|
| 63 |
+
min_timescale: float = 1.0,
|
| 64 |
+
max_timescale: float = 10000.0,
|
| 65 |
+
):
|
| 66 |
+
super().__init__()
|
| 67 |
+
self.embedding_dims = embedding_dims
|
| 68 |
+
self.min_timescale = min_timescale
|
| 69 |
+
self.max_timescale = max_timescale
|
| 70 |
+
|
| 71 |
+
def forward(
|
| 72 |
+
self,
|
| 73 |
+
inputs: torch.Tensor,
|
| 74 |
+
position: torch.Tensor | None = None,
|
| 75 |
+
):
|
| 76 |
+
"""Generates a JTensor of sinusoids with different frequencies."""
|
| 77 |
+
if self.embedding_dims != inputs.shape[-1]:
|
| 78 |
+
raise ValueError(
|
| 79 |
+
"The embedding dims of the rotary position embedding"
|
| 80 |
+
"must match the hidden dimension of the inputs."
|
| 81 |
+
)
|
| 82 |
+
half_embedding_dim = self.embedding_dims // 2
|
| 83 |
+
|
| 84 |
+
# Keep RoPE numerics stable by building the sinusoid in fp32, but cast the
|
| 85 |
+
# final trigonometric factors back to the input dtype before mixing them
|
| 86 |
+
# with the activations. Without this, bf16 TimesFM activations get promoted
|
| 87 |
+
# to fp32 in the rotary path while V stays bf16, and fused SDPA then fails
|
| 88 |
+
# because Q/K/V no longer share a dtype.
|
| 89 |
+
rope_dtype = torch.float32
|
| 90 |
+
fraction = (
|
| 91 |
+
2
|
| 92 |
+
* torch.arange(0, half_embedding_dim, device=inputs.device, dtype=rope_dtype)
|
| 93 |
+
/ self.embedding_dims
|
| 94 |
+
)
|
| 95 |
+
timescale = (
|
| 96 |
+
self.min_timescale * (self.max_timescale / self.min_timescale) ** fraction
|
| 97 |
+
).to(device=inputs.device, dtype=rope_dtype)
|
| 98 |
+
if position is None:
|
| 99 |
+
seq_length = inputs.shape[1]
|
| 100 |
+
position = torch.arange(seq_length, dtype=rope_dtype, device=inputs.device)[
|
| 101 |
+
None, :
|
| 102 |
+
]
|
| 103 |
+
else:
|
| 104 |
+
position = position.to(device=inputs.device, dtype=rope_dtype)
|
| 105 |
+
|
| 106 |
+
if len(inputs.shape) == 4:
|
| 107 |
+
position = position[..., None, None]
|
| 108 |
+
timescale = timescale[None, None, None, :]
|
| 109 |
+
elif len(inputs.shape) == 3:
|
| 110 |
+
position = position[..., None]
|
| 111 |
+
timescale = timescale[None, None, :]
|
| 112 |
+
else:
|
| 113 |
+
raise ValueError("Inputs must be of rank 3 or 4.")
|
| 114 |
+
|
| 115 |
+
sinusoid_inp = position / timescale
|
| 116 |
+
sin = torch.sin(sinusoid_inp).to(dtype=inputs.dtype)
|
| 117 |
+
cos = torch.cos(sinusoid_inp).to(dtype=inputs.dtype)
|
| 118 |
+
first_half, second_half = torch.chunk(inputs, 2, dim=-1)
|
| 119 |
+
first_part = first_half * cos - second_half * sin
|
| 120 |
+
second_part = second_half * cos + first_half * sin
|
| 121 |
+
return torch.cat([first_part, second_part], dim=-1)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _dot_product_attention(
|
| 125 |
+
query,
|
| 126 |
+
key,
|
| 127 |
+
value,
|
| 128 |
+
mask=None,
|
| 129 |
+
):
|
| 130 |
+
"""Computes dot-product attention given query, key, and value."""
|
| 131 |
+
attn_weights = torch.einsum("...qhd,...khd->...hqk", query, key)
|
| 132 |
+
if mask is not None:
|
| 133 |
+
attn_weights = torch.where(
|
| 134 |
+
mask, attn_weights, -torch.finfo(attn_weights.dtype).max / 2
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
attn_weights = F.softmax(attn_weights, dim=-1)
|
| 138 |
+
|
| 139 |
+
return torch.einsum("...hqk,...khd->...qhd", attn_weights, value)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _torch_dot_product_attention(query, key, value, mask=None):
|
| 143 |
+
"""
|
| 144 |
+
Performs the exact same (unscaled) attention as the above function,
|
| 145 |
+
but using the fast and fused F.scaled_dot_product_attention kernel.
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
# 1. Permute inputs from (B, L, H, D) to the expected (B, H, L, D)
|
| 149 |
+
query = query.permute(0, 2, 1, 3)
|
| 150 |
+
key = key.permute(0, 2, 1, 3)
|
| 151 |
+
value = value.permute(0, 2, 1, 3)
|
| 152 |
+
|
| 153 |
+
# 2. Call the fused attention kernel
|
| 154 |
+
# - Pass the mask to `attn_mask`.
|
| 155 |
+
# - Set `scale=1.0` to disable the default 1/sqrt(d_k) scaling.
|
| 156 |
+
output = F.scaled_dot_product_attention(query, key, value, attn_mask=mask, scale=1.0)
|
| 157 |
+
|
| 158 |
+
# 3. Permute the output back to the original (B, L, H, D) layout
|
| 159 |
+
output = output.permute(0, 2, 1, 3)
|
| 160 |
+
|
| 161 |
+
return output
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
class PerDimScale(nn.Module):
|
| 165 |
+
"""Per-dimension scaling."""
|
| 166 |
+
|
| 167 |
+
def __init__(self, num_dims: int):
|
| 168 |
+
super().__init__()
|
| 169 |
+
self.num_dims = num_dims
|
| 170 |
+
self.per_dim_scale = nn.Parameter(torch.zeros(num_dims))
|
| 171 |
+
|
| 172 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 173 |
+
scale_factor = (
|
| 174 |
+
1.442695041 / math.sqrt(self.num_dims) * F.softplus(self.per_dim_scale)
|
| 175 |
+
)
|
| 176 |
+
return x * scale_factor
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class MultiHeadAttention(nn.Module):
|
| 180 |
+
"""Multi-head attention."""
|
| 181 |
+
|
| 182 |
+
def __init__(
|
| 183 |
+
self,
|
| 184 |
+
num_heads: int,
|
| 185 |
+
in_features: int,
|
| 186 |
+
*,
|
| 187 |
+
use_per_dim_scale: bool = True,
|
| 188 |
+
use_rotary_position_embeddings: bool = True,
|
| 189 |
+
use_bias: bool = False,
|
| 190 |
+
attention_fn: Callable[..., torch.Tensor] = _torch_dot_product_attention,
|
| 191 |
+
qk_norm: str = "rms",
|
| 192 |
+
fuse_qkv: bool = False,
|
| 193 |
+
):
|
| 194 |
+
super().__init__()
|
| 195 |
+
self.num_heads = num_heads
|
| 196 |
+
self.in_features = in_features
|
| 197 |
+
self.head_dim = in_features // num_heads
|
| 198 |
+
self.use_bias = use_bias
|
| 199 |
+
self.attention_fn = attention_fn
|
| 200 |
+
self.qk_norm = qk_norm
|
| 201 |
+
self.fuse_qkv = fuse_qkv
|
| 202 |
+
|
| 203 |
+
if self.in_features % self.num_heads != 0:
|
| 204 |
+
raise ValueError(
|
| 205 |
+
f"Memory dimension ({self.in_features}) must be divisible by "
|
| 206 |
+
f"'num_heads' heads ({self.num_heads})."
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
if self.fuse_qkv:
|
| 210 |
+
self.qkv_proj = nn.Linear(self.in_features, 3 * self.in_features, bias=use_bias)
|
| 211 |
+
else:
|
| 212 |
+
self.query = nn.Linear(self.in_features, self.in_features, bias=use_bias)
|
| 213 |
+
self.key = nn.Linear(self.in_features, self.in_features, bias=use_bias)
|
| 214 |
+
self.value = nn.Linear(self.in_features, self.in_features, bias=use_bias)
|
| 215 |
+
self.out = nn.Linear(self.in_features, self.in_features, bias=use_bias)
|
| 216 |
+
|
| 217 |
+
if self.qk_norm == "rms":
|
| 218 |
+
self.query_ln = RMSNorm(self.head_dim)
|
| 219 |
+
self.key_ln = RMSNorm(self.head_dim)
|
| 220 |
+
else:
|
| 221 |
+
self.query_ln = nn.Identity()
|
| 222 |
+
self.key_ln = nn.Identity()
|
| 223 |
+
|
| 224 |
+
self.use_rotary_position_embeddings = use_rotary_position_embeddings
|
| 225 |
+
if self.use_rotary_position_embeddings:
|
| 226 |
+
self.rotary_position_embedding = RotaryPositionalEmbedding(
|
| 227 |
+
embedding_dims=self.head_dim,
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
self.use_per_dim_scale = use_per_dim_scale
|
| 231 |
+
if use_per_dim_scale:
|
| 232 |
+
self.per_dim_scale = PerDimScale(num_dims=self.head_dim)
|
| 233 |
+
|
| 234 |
+
def forward(
|
| 235 |
+
self,
|
| 236 |
+
inputs_q: torch.Tensor,
|
| 237 |
+
*,
|
| 238 |
+
decode_cache: DecodeCache | None = None,
|
| 239 |
+
patch_mask: torch.Tensor | None = None,
|
| 240 |
+
) -> tuple[torch.Tensor, DecodeCache | None]:
|
| 241 |
+
b, n_patches, _ = inputs_q.shape
|
| 242 |
+
if patch_mask is None:
|
| 243 |
+
patch_mask = torch.zeros(b, n_patches, dtype=torch.bool, device=inputs_q.device)
|
| 244 |
+
|
| 245 |
+
if self.fuse_qkv:
|
| 246 |
+
qkv = self.qkv_proj(inputs_q)
|
| 247 |
+
query, key, value = torch.chunk(qkv, 3, dim=-1)
|
| 248 |
+
query = query.view(b, n_patches, self.num_heads, self.head_dim)
|
| 249 |
+
key = key.view(b, n_patches, self.num_heads, self.head_dim)
|
| 250 |
+
value = value.view(b, n_patches, self.num_heads, self.head_dim)
|
| 251 |
+
else:
|
| 252 |
+
query = self.query(inputs_q).view(b, n_patches, self.num_heads, self.head_dim)
|
| 253 |
+
key = self.key(inputs_q).view(b, n_patches, self.num_heads, self.head_dim)
|
| 254 |
+
value = self.value(inputs_q).view(b, n_patches, self.num_heads, self.head_dim)
|
| 255 |
+
|
| 256 |
+
if decode_cache is None:
|
| 257 |
+
num_masked = torch.sum(patch_mask.to(torch.int32), dim=-1)
|
| 258 |
+
next_index = torch.zeros_like(num_masked, dtype=torch.int32)
|
| 259 |
+
else:
|
| 260 |
+
num_masked = (
|
| 261 |
+
torch.sum(patch_mask.to(torch.int32), dim=-1) + decode_cache.num_masked
|
| 262 |
+
)
|
| 263 |
+
next_index = decode_cache.next_index.clone()
|
| 264 |
+
|
| 265 |
+
if self.use_rotary_position_embeddings:
|
| 266 |
+
position = (
|
| 267 |
+
torch.arange(n_patches, device=inputs_q.device)[None, :]
|
| 268 |
+
+ next_index[:, None]
|
| 269 |
+
- num_masked[:, None]
|
| 270 |
+
)
|
| 271 |
+
query = self.rotary_position_embedding(query, position)
|
| 272 |
+
key = self.rotary_position_embedding(key, position)
|
| 273 |
+
|
| 274 |
+
query = self.query_ln(query)
|
| 275 |
+
key = self.key_ln(key)
|
| 276 |
+
|
| 277 |
+
if self.use_per_dim_scale:
|
| 278 |
+
query = self.per_dim_scale(query)
|
| 279 |
+
|
| 280 |
+
if decode_cache is not None:
|
| 281 |
+
_, decode_cache_size, _, _ = decode_cache.value.shape
|
| 282 |
+
|
| 283 |
+
start = decode_cache.next_index[0]
|
| 284 |
+
end = start + n_patches
|
| 285 |
+
|
| 286 |
+
# Perform a single, vectorized slice assignment for the entire batch.
|
| 287 |
+
# This is vastly more efficient than a Python for-loop.
|
| 288 |
+
|
| 289 |
+
decode_cache.key[:, start:end] = key
|
| 290 |
+
decode_cache.value[:, start:end] = value
|
| 291 |
+
|
| 292 |
+
key = decode_cache.key
|
| 293 |
+
value = decode_cache.value
|
| 294 |
+
decode_cache.next_index += n_patches
|
| 295 |
+
decode_cache.num_masked = num_masked
|
| 296 |
+
attn_mask = make_attn_mask(
|
| 297 |
+
query_length=n_patches,
|
| 298 |
+
num_all_masked_kv=num_masked,
|
| 299 |
+
query_index_offset=next_index,
|
| 300 |
+
kv_length=decode_cache_size,
|
| 301 |
+
)
|
| 302 |
+
else:
|
| 303 |
+
attn_mask = make_attn_mask(query_length=n_patches, num_all_masked_kv=num_masked)
|
| 304 |
+
|
| 305 |
+
x = self.attention_fn(
|
| 306 |
+
query,
|
| 307 |
+
key,
|
| 308 |
+
value,
|
| 309 |
+
mask=attn_mask,
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
x = x.reshape(b, n_patches, self.in_features)
|
| 313 |
+
out = self.out(x)
|
| 314 |
+
return out, decode_cache
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
class Transformer(nn.Module):
|
| 318 |
+
"""Classic Transformer used in TimesFM."""
|
| 319 |
+
|
| 320 |
+
def __init__(self, config: configs.TransformerConfig):
|
| 321 |
+
super().__init__()
|
| 322 |
+
self.config = config
|
| 323 |
+
|
| 324 |
+
if config.attention_norm == "rms":
|
| 325 |
+
self.pre_attn_ln = RMSNorm(num_features=config.model_dims)
|
| 326 |
+
self.post_attn_ln = RMSNorm(num_features=config.model_dims)
|
| 327 |
+
else:
|
| 328 |
+
raise ValueError(f"Layer norm: {config.attention_norm} not supported.")
|
| 329 |
+
|
| 330 |
+
self.attn = MultiHeadAttention(
|
| 331 |
+
num_heads=config.num_heads,
|
| 332 |
+
in_features=config.model_dims,
|
| 333 |
+
use_per_dim_scale=True,
|
| 334 |
+
use_rotary_position_embeddings=config.use_rotary_position_embeddings,
|
| 335 |
+
qk_norm=config.qk_norm,
|
| 336 |
+
fuse_qkv=config.fuse_qkv,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
if config.feedforward_norm == "rms":
|
| 340 |
+
self.pre_ff_ln = RMSNorm(num_features=config.model_dims)
|
| 341 |
+
self.post_ff_ln = RMSNorm(num_features=config.model_dims)
|
| 342 |
+
else:
|
| 343 |
+
raise ValueError(f"Layer norm: {config.feedforward_norm} not supported.")
|
| 344 |
+
|
| 345 |
+
self.ff0 = nn.Linear(
|
| 346 |
+
in_features=config.model_dims,
|
| 347 |
+
out_features=config.hidden_dims,
|
| 348 |
+
bias=config.use_bias,
|
| 349 |
+
)
|
| 350 |
+
self.ff1 = nn.Linear(
|
| 351 |
+
in_features=config.hidden_dims,
|
| 352 |
+
out_features=config.model_dims,
|
| 353 |
+
bias=config.use_bias,
|
| 354 |
+
)
|
| 355 |
+
if config.ff_activation == "relu":
|
| 356 |
+
self.activation = nn.ReLU()
|
| 357 |
+
elif config.ff_activation == "swish":
|
| 358 |
+
self.activation = nn.SiLU()
|
| 359 |
+
elif config.ff_activation == "none":
|
| 360 |
+
self.activation = nn.Identity()
|
| 361 |
+
else:
|
| 362 |
+
raise ValueError(f"Activation: {config.ff_activation} not supported.")
|
| 363 |
+
|
| 364 |
+
def forward(
|
| 365 |
+
self,
|
| 366 |
+
input_embeddings: torch.Tensor,
|
| 367 |
+
patch_mask: torch.Tensor,
|
| 368 |
+
decode_cache: DecodeCache | None = None,
|
| 369 |
+
) -> tuple[torch.Tensor, DecodeCache | None]:
|
| 370 |
+
attn_output, decode_cache = self.attn(
|
| 371 |
+
inputs_q=self.pre_attn_ln(input_embeddings),
|
| 372 |
+
decode_cache=decode_cache,
|
| 373 |
+
patch_mask=patch_mask,
|
| 374 |
+
)
|
| 375 |
+
attn_output = self.post_attn_ln(attn_output) + input_embeddings
|
| 376 |
+
output_embeddings = (
|
| 377 |
+
self.post_ff_ln(self.ff1(self.activation(self.ff0(self.pre_ff_ln(attn_output)))))
|
| 378 |
+
+ attn_output
|
| 379 |
+
)
|
| 380 |
+
return output_embeddings, decode_cache
|
timebraid/_vendor/timesfm/torch/util.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modified by TimeBraid contributors for the vendored PyTorch-only runtime.
|
| 2 |
+
# Copyright 2025 Google LLC
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""PyTorch utility functions for TimesFM layers."""
|
| 17 |
+
|
| 18 |
+
import dataclasses
|
| 19 |
+
import torch
|
| 20 |
+
|
| 21 |
+
_TOLERANCE = 1e-6
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclasses.dataclass(frozen=False)
|
| 25 |
+
class DecodeCache:
|
| 26 |
+
"""Cache for decoding."""
|
| 27 |
+
|
| 28 |
+
next_index: torch.Tensor
|
| 29 |
+
num_masked: torch.Tensor
|
| 30 |
+
key: torch.Tensor
|
| 31 |
+
value: torch.Tensor
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def update_running_stats(
|
| 35 |
+
n: torch.Tensor,
|
| 36 |
+
mu: torch.Tensor,
|
| 37 |
+
sigma: torch.Tensor,
|
| 38 |
+
x: torch.Tensor,
|
| 39 |
+
mask: torch.Tensor,
|
| 40 |
+
) -> tuple[
|
| 41 |
+
tuple[torch.Tensor, torch.Tensor, torch.Tensor],
|
| 42 |
+
tuple[torch.Tensor, torch.Tensor, torch.Tensor],
|
| 43 |
+
]:
|
| 44 |
+
"""Updates the running stats."""
|
| 45 |
+
is_legit = torch.logical_not(mask)
|
| 46 |
+
inc_n = torch.sum(is_legit.to(x.dtype), dim=-1)
|
| 47 |
+
|
| 48 |
+
inc_mu_numerator = torch.sum(x * is_legit, dim=-1)
|
| 49 |
+
inc_n_safe = torch.where(inc_n == 0, 1.0, inc_n)
|
| 50 |
+
inc_mu = inc_mu_numerator / inc_n_safe
|
| 51 |
+
inc_mu = torch.where(inc_n == 0, 0.0, inc_mu)
|
| 52 |
+
|
| 53 |
+
inc_var_numerator = torch.sum(
|
| 54 |
+
((x - inc_mu.unsqueeze(-1)) ** 2) * is_legit, dim=-1
|
| 55 |
+
)
|
| 56 |
+
inc_var = inc_var_numerator / inc_n_safe
|
| 57 |
+
inc_var = torch.where(inc_n == 0, 0.0, inc_var)
|
| 58 |
+
inc_sigma = torch.sqrt(inc_var)
|
| 59 |
+
|
| 60 |
+
new_n = n + inc_n
|
| 61 |
+
new_n_safe = torch.where(new_n == 0, 1.0, new_n)
|
| 62 |
+
|
| 63 |
+
new_mu = (n * mu + inc_mu * inc_n) / new_n_safe
|
| 64 |
+
new_mu = torch.where(new_n == 0, 0.0, new_mu)
|
| 65 |
+
|
| 66 |
+
term1 = n * sigma.pow(2)
|
| 67 |
+
term2 = inc_n * inc_sigma.pow(2)
|
| 68 |
+
term3 = n * (mu - new_mu).pow(2)
|
| 69 |
+
term4 = inc_n * (inc_mu - new_mu).pow(2)
|
| 70 |
+
|
| 71 |
+
new_var = (term1 + term2 + term3 + term4) / new_n_safe
|
| 72 |
+
new_var = torch.where(new_n == 0, 0.0, new_var)
|
| 73 |
+
new_sigma = torch.sqrt(torch.clamp(new_var, min=0.0))
|
| 74 |
+
|
| 75 |
+
return (w := (new_n, new_mu, new_sigma), w)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def revin(
|
| 79 |
+
x: torch.Tensor,
|
| 80 |
+
mu: torch.Tensor,
|
| 81 |
+
sigma: torch.Tensor,
|
| 82 |
+
reverse: bool = False,
|
| 83 |
+
):
|
| 84 |
+
"""Reversible instance normalization."""
|
| 85 |
+
if len(mu.shape) == len(x.shape) - 1:
|
| 86 |
+
mu = mu[..., None]
|
| 87 |
+
sigma = sigma[..., None]
|
| 88 |
+
elif len(mu.shape) == len(x.shape) - 2:
|
| 89 |
+
mu = mu[..., None, None]
|
| 90 |
+
sigma = sigma[..., None, None]
|
| 91 |
+
|
| 92 |
+
if reverse:
|
| 93 |
+
return x * sigma + mu
|
| 94 |
+
else:
|
| 95 |
+
return (x - mu) / torch.where(sigma < _TOLERANCE, 1.0, sigma)
|
timebraid/data/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Inference tensorization internals for the TimeBraid processor."""
|
timebraid/data/mot_utils.py
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 the TimeBraid contributors.
|
| 2 |
+
"""Inference-only validation and tensorization for TimeBraid time-series spans."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import math
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from ..model.mot.structures import ROLE_CONTEXT, ROLE_OBSERVED, ROLE_TARGET
|
| 13 |
+
|
| 14 |
+
# Derived from the model-side constants rather than restated. This module
|
| 15 |
+
# writes `role_id` and the MoT runtime reads it back, so two independent
|
| 16 |
+
# literals would route spans to the wrong expert the moment either side
|
| 17 |
+
# renumbered — with correctly shaped tensors and no error.
|
| 18 |
+
ROLE_TO_ID = {
|
| 19 |
+
"observed": ROLE_OBSERVED,
|
| 20 |
+
"context": ROLE_CONTEXT,
|
| 21 |
+
"target": ROLE_TARGET,
|
| 22 |
+
}
|
| 23 |
+
_GENERIC_OPEN_TAG = "<ts>"
|
| 24 |
+
_GENERIC_CLOSE_TAG = "</ts>"
|
| 25 |
+
_FLOAT32_MAX = float(torch.finfo(torch.float32).max)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass(frozen=True, slots=True)
|
| 29 |
+
class MoTNormalizedSpan:
|
| 30 |
+
"""
|
| 31 |
+
One validated MoT span payload entry.
|
| 32 |
+
|
| 33 |
+
`loss_start` and `loss_roi_mask` remain part of the model payload ABI even
|
| 34 |
+
though the inference processor constructs conditioning-only spans.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
length: int
|
| 38 |
+
role_id: int
|
| 39 |
+
segment_id: int
|
| 40 |
+
values: list[float]
|
| 41 |
+
loss_start: int = 0
|
| 42 |
+
loss_roi_mask: list[float] | None = None
|
| 43 |
+
text_start_token_idx: int | None = None
|
| 44 |
+
text_end_token_idx: int | None = None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _normalize_special_token_entry(token: Any) -> str:
|
| 48 |
+
"""
|
| 49 |
+
Normalize tokenizer special-token declarations to their textual surface.
|
| 50 |
+
|
| 51 |
+
Tokenizers expose special tokens through a mix of:
|
| 52 |
+
- raw strings
|
| 53 |
+
- dicts with `content`
|
| 54 |
+
- AddedToken-like objects with `.content`
|
| 55 |
+
|
| 56 |
+
The processor uses this normalization to derive the tokenizer's exact TS
|
| 57 |
+
delimiter inventory.
|
| 58 |
+
"""
|
| 59 |
+
if isinstance(token, str):
|
| 60 |
+
return token
|
| 61 |
+
if isinstance(token, dict):
|
| 62 |
+
content = token.get("content")
|
| 63 |
+
if isinstance(content, str):
|
| 64 |
+
return content
|
| 65 |
+
raise RuntimeError(
|
| 66 |
+
f"Tokenizer special-token dict is missing string `content`: {token!r}"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
content = getattr(token, "content", None)
|
| 70 |
+
if isinstance(content, str):
|
| 71 |
+
return content
|
| 72 |
+
|
| 73 |
+
raise RuntimeError(
|
| 74 |
+
"Unsupported tokenizer special-token entry while resolving MoT TS delimiters: "
|
| 75 |
+
f"{type(token)} {token!r}"
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _format_ts_tag(length: int | None, *, is_close: bool) -> str:
|
| 80 |
+
if length is not None:
|
| 81 |
+
raise RuntimeError(
|
| 82 |
+
f"Length-tagged TS delimiters are no longer supported: {length}."
|
| 83 |
+
)
|
| 84 |
+
return _GENERIC_CLOSE_TAG if is_close else _GENERIC_OPEN_TAG
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _parse_ts_tag(token: str) -> tuple[int | None, bool]:
|
| 88 |
+
"""
|
| 89 |
+
Parse one textual TS delimiter into `(tagged_len, is_close)`.
|
| 90 |
+
|
| 91 |
+
Canonical generic tags are `<ts>` and `</ts>`.
|
| 92 |
+
"""
|
| 93 |
+
if token == _GENERIC_OPEN_TAG:
|
| 94 |
+
return None, False
|
| 95 |
+
if token == _GENERIC_CLOSE_TAG:
|
| 96 |
+
return None, True
|
| 97 |
+
|
| 98 |
+
raise RuntimeError(f"Unsupported TS tag syntax: {token}")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def resolve_ts_delimiter_id_maps(
|
| 102 |
+
tokenizer: Any,
|
| 103 |
+
) -> tuple[dict[int, int | None], dict[int, int | None]]:
|
| 104 |
+
"""
|
| 105 |
+
Resolve tokenizer-visible TS delimiter ids to their optional tagged length.
|
| 106 |
+
|
| 107 |
+
The only supported tokenizer-visible TS delimiters are `<ts>` and `</ts>`.
|
| 108 |
+
"""
|
| 109 |
+
candidate_sources: list[tuple[str, Any]] = []
|
| 110 |
+
for attr_name in (
|
| 111 |
+
"additional_special_tokens",
|
| 112 |
+
"extra_special_tokens",
|
| 113 |
+
"all_special_tokens",
|
| 114 |
+
):
|
| 115 |
+
declared_tokens = getattr(tokenizer, attr_name, None)
|
| 116 |
+
if declared_tokens is not None:
|
| 117 |
+
candidate_sources.append((f"tokenizer.{attr_name}", declared_tokens))
|
| 118 |
+
|
| 119 |
+
added_tokens_decoder = getattr(tokenizer, "added_tokens_decoder", None)
|
| 120 |
+
if isinstance(added_tokens_decoder, dict) and len(added_tokens_decoder) > 0:
|
| 121 |
+
candidate_sources.append(
|
| 122 |
+
("tokenizer.added_tokens_decoder", list(added_tokens_decoder.values()))
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
open_id_to_len: dict[int, int | None] = {}
|
| 126 |
+
close_id_to_len: dict[int, int | None] = {}
|
| 127 |
+
for source_name, declared_tokens in candidate_sources:
|
| 128 |
+
if isinstance(declared_tokens, dict):
|
| 129 |
+
iterable = declared_tokens.values()
|
| 130 |
+
elif isinstance(declared_tokens, (list, tuple, set)):
|
| 131 |
+
iterable = declared_tokens
|
| 132 |
+
else:
|
| 133 |
+
raise RuntimeError(
|
| 134 |
+
f"Unsupported tokenizer special-token collection while resolving MoT TS delimiters: "
|
| 135 |
+
f"{source_name} -> {type(declared_tokens)}"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
for token in iterable:
|
| 139 |
+
token_str = _normalize_special_token_entry(token)
|
| 140 |
+
try:
|
| 141 |
+
tagged_len, is_close = _parse_ts_tag(token_str)
|
| 142 |
+
except RuntimeError:
|
| 143 |
+
continue
|
| 144 |
+
|
| 145 |
+
token_id = int(tokenizer.convert_tokens_to_ids(token_str))
|
| 146 |
+
target_map = close_id_to_len if is_close else open_id_to_len
|
| 147 |
+
previous = target_map.get(token_id)
|
| 148 |
+
if previous is not None or token_id in target_map:
|
| 149 |
+
if previous != tagged_len:
|
| 150 |
+
raise RuntimeError(
|
| 151 |
+
"Tokenizer resolves one TS delimiter id to multiple tagged lengths: "
|
| 152 |
+
f"token={token_str!r}, token_id={token_id}, existing_len={previous}, new_len={tagged_len}"
|
| 153 |
+
)
|
| 154 |
+
continue
|
| 155 |
+
target_map[token_id] = tagged_len
|
| 156 |
+
|
| 157 |
+
if not open_id_to_len or not close_id_to_len:
|
| 158 |
+
raise RuntimeError(
|
| 159 |
+
"MoT expected tokenizer-visible TS delimiters, but failed to resolve a complete id inventory. "
|
| 160 |
+
f"open_ids={sorted(open_id_to_len)}, close_ids={sorted(close_id_to_len)}."
|
| 161 |
+
)
|
| 162 |
+
return open_id_to_len, close_id_to_len
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def find_tokenized_ts_spans(
|
| 166 |
+
token_ids: list[int],
|
| 167 |
+
*,
|
| 168 |
+
open_id_to_len: dict[int, int | None],
|
| 169 |
+
close_id_to_len: dict[int, int | None],
|
| 170 |
+
) -> list[tuple[int, int, int | None]]:
|
| 171 |
+
"""
|
| 172 |
+
Parse one tokenized text row into ordered `(start_idx, end_idx, tagged_len)` spans.
|
| 173 |
+
|
| 174 |
+
This mirrors the strict delimiter parser the runtime historically performed
|
| 175 |
+
inside every forward pass. Moving it into preprocessing lets us cache
|
| 176 |
+
the result as batch metadata without weakening any structural checks.
|
| 177 |
+
"""
|
| 178 |
+
spans: list[tuple[int, int, int | None]] = []
|
| 179 |
+
stack: list[tuple[int | None, int]] = []
|
| 180 |
+
for idx, raw_tid in enumerate(token_ids):
|
| 181 |
+
tid = int(raw_tid)
|
| 182 |
+
if tid in open_id_to_len:
|
| 183 |
+
if stack:
|
| 184 |
+
raise RuntimeError("Nested TS spans are not supported.")
|
| 185 |
+
stack.append((open_id_to_len[tid], idx))
|
| 186 |
+
continue
|
| 187 |
+
|
| 188 |
+
if tid not in close_id_to_len:
|
| 189 |
+
continue
|
| 190 |
+
|
| 191 |
+
if not stack:
|
| 192 |
+
raise RuntimeError(
|
| 193 |
+
f"Found closing TS token without opening token at position {idx}."
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
tagged_len, start_idx = stack.pop()
|
| 197 |
+
close_len = close_id_to_len[tid]
|
| 198 |
+
if close_len != tagged_len:
|
| 199 |
+
raise RuntimeError(
|
| 200 |
+
"Mismatched TS delimiters: "
|
| 201 |
+
f"open={_format_ts_tag(tagged_len, is_close=False)}, "
|
| 202 |
+
f"close={_format_ts_tag(close_len, is_close=True)}"
|
| 203 |
+
)
|
| 204 |
+
spans.append((start_idx, idx, tagged_len))
|
| 205 |
+
|
| 206 |
+
if stack:
|
| 207 |
+
raise RuntimeError("Unclosed TS tags found in sequence.")
|
| 208 |
+
spans.sort(key=lambda item: item[0])
|
| 209 |
+
return spans
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def _require_json_int(value: Any, *, owner: str, field_name: str) -> int:
|
| 213 |
+
"""Require an actual JSON integer for schema fields that define supervision boundaries."""
|
| 214 |
+
if type(value) is not int:
|
| 215 |
+
raise RuntimeError(
|
| 216 |
+
f"{owner} `{field_name}` must be int, got {value!r} ({type(value)})."
|
| 217 |
+
)
|
| 218 |
+
return int(value)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def normalize_timeseries_spans(
|
| 222 |
+
spans: Any,
|
| 223 |
+
*,
|
| 224 |
+
sample_idx: int,
|
| 225 |
+
require_segment_id: bool,
|
| 226 |
+
default_segment_id: int,
|
| 227 |
+
max_spans_per_sample: int,
|
| 228 |
+
) -> list[MoTNormalizedSpan]:
|
| 229 |
+
"""
|
| 230 |
+
Validate and normalize one sample's `timeseries` payload.
|
| 231 |
+
|
| 232 |
+
Expected schema for each span:
|
| 233 |
+
{
|
| 234 |
+
"len": int > 0,
|
| 235 |
+
"role": "observed" | "context" | "target",
|
| 236 |
+
"values": list[float] with exact `len`,
|
| 237 |
+
"segment_id": int > 0 # optional when require_segment_id=False
|
| 238 |
+
"loss_start": int in [0, len] # required; len means no TS supervision
|
| 239 |
+
"loss_roi_mask": list[float] # optional; raw-point aligned soft ROI in [0, 1]
|
| 240 |
+
}
|
| 241 |
+
"""
|
| 242 |
+
if spans is None:
|
| 243 |
+
return []
|
| 244 |
+
if not isinstance(spans, list):
|
| 245 |
+
raise RuntimeError(
|
| 246 |
+
f"Sample[{sample_idx}] `timeseries` must be list, got {type(spans)}."
|
| 247 |
+
)
|
| 248 |
+
if len(spans) > max_spans_per_sample:
|
| 249 |
+
raise RuntimeError(
|
| 250 |
+
f"Sample[{sample_idx}] spans exceed max limit: "
|
| 251 |
+
f"{len(spans)} > {max_spans_per_sample}."
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
normalized: list[MoTNormalizedSpan] = []
|
| 255 |
+
for span_idx, span in enumerate(spans):
|
| 256 |
+
if not isinstance(span, dict):
|
| 257 |
+
raise RuntimeError(
|
| 258 |
+
f"Sample[{sample_idx}] span[{span_idx}] must be dict, got {type(span)}."
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
for key in ("len", "role", "values", "loss_start"):
|
| 262 |
+
if key not in span:
|
| 263 |
+
raise RuntimeError(
|
| 264 |
+
f"Sample[{sample_idx}] span[{span_idx}] missing key `{key}`."
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
owner = f"Sample[{sample_idx}] span[{span_idx}]"
|
| 268 |
+
length = _require_json_int(span["len"], owner=owner, field_name="len")
|
| 269 |
+
if length <= 0:
|
| 270 |
+
raise RuntimeError(
|
| 271 |
+
f"Sample[{sample_idx}] span[{span_idx}] invalid len={length}."
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
role = str(span["role"])
|
| 275 |
+
if role not in ROLE_TO_ID:
|
| 276 |
+
raise RuntimeError(
|
| 277 |
+
f"Sample[{sample_idx}] span[{span_idx}] invalid role={role}."
|
| 278 |
+
)
|
| 279 |
+
role_id = ROLE_TO_ID[role]
|
| 280 |
+
|
| 281 |
+
values = span["values"]
|
| 282 |
+
if not isinstance(values, list):
|
| 283 |
+
raise RuntimeError(
|
| 284 |
+
f"Sample[{sample_idx}] span[{span_idx}] values must be list, got {type(values)}."
|
| 285 |
+
)
|
| 286 |
+
if len(values) != length:
|
| 287 |
+
raise RuntimeError(
|
| 288 |
+
f"Sample[{sample_idx}] span[{span_idx}] len mismatch: "
|
| 289 |
+
f"len={length}, values={len(values)}."
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
normalized_values: list[float] = []
|
| 293 |
+
for value_idx, value in enumerate(values):
|
| 294 |
+
if isinstance(value, bool):
|
| 295 |
+
raise RuntimeError(
|
| 296 |
+
f"Sample[{sample_idx}] span[{span_idx}] value[{value_idx}] is bool."
|
| 297 |
+
)
|
| 298 |
+
if isinstance(value, (int, float)):
|
| 299 |
+
casted = float(value)
|
| 300 |
+
else:
|
| 301 |
+
raise RuntimeError(
|
| 302 |
+
f"Sample[{sample_idx}] span[{span_idx}] value[{value_idx}] invalid type={type(value)}."
|
| 303 |
+
)
|
| 304 |
+
if not math.isfinite(casted):
|
| 305 |
+
raise RuntimeError(
|
| 306 |
+
f"Sample[{sample_idx}] span[{span_idx}] value[{value_idx}] is not finite: {casted}."
|
| 307 |
+
)
|
| 308 |
+
if abs(casted) > _FLOAT32_MAX:
|
| 309 |
+
raise RuntimeError(
|
| 310 |
+
f"Sample[{sample_idx}] span[{span_idx}] value[{value_idx}] exceeds float32 range: {casted}."
|
| 311 |
+
)
|
| 312 |
+
normalized_values.append(casted)
|
| 313 |
+
|
| 314 |
+
if "segment_id" in span and span["segment_id"] is not None:
|
| 315 |
+
segment_id = _require_json_int(
|
| 316 |
+
span["segment_id"],
|
| 317 |
+
owner=owner,
|
| 318 |
+
field_name="segment_id",
|
| 319 |
+
)
|
| 320 |
+
elif require_segment_id:
|
| 321 |
+
raise RuntimeError(
|
| 322 |
+
f"Sample[{sample_idx}] span[{span_idx}] missing `segment_id`."
|
| 323 |
+
)
|
| 324 |
+
else:
|
| 325 |
+
segment_id = int(default_segment_id)
|
| 326 |
+
|
| 327 |
+
if segment_id <= 0:
|
| 328 |
+
raise RuntimeError(
|
| 329 |
+
f"Sample[{sample_idx}] span[{span_idx}] invalid segment_id={segment_id}."
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
loss_start = _require_json_int(
|
| 333 |
+
span["loss_start"],
|
| 334 |
+
owner=owner,
|
| 335 |
+
field_name="loss_start",
|
| 336 |
+
)
|
| 337 |
+
if loss_start < 0 or loss_start > length:
|
| 338 |
+
raise RuntimeError(
|
| 339 |
+
f"Sample[{sample_idx}] span[{span_idx}] invalid loss_start={loss_start} for len={length}."
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
loss_roi_mask = None
|
| 343 |
+
if "loss_roi_mask" in span and span["loss_roi_mask"] is not None:
|
| 344 |
+
raw_loss_roi_mask = span["loss_roi_mask"]
|
| 345 |
+
if not isinstance(raw_loss_roi_mask, list):
|
| 346 |
+
raise RuntimeError(
|
| 347 |
+
f"Sample[{sample_idx}] span[{span_idx}] loss_roi_mask must be list, "
|
| 348 |
+
f"got {type(raw_loss_roi_mask)}."
|
| 349 |
+
)
|
| 350 |
+
if len(raw_loss_roi_mask) > 0:
|
| 351 |
+
if len(raw_loss_roi_mask) != length:
|
| 352 |
+
raise RuntimeError(
|
| 353 |
+
f"Sample[{sample_idx}] span[{span_idx}] loss_roi_mask length mismatch: "
|
| 354 |
+
f"len={length}, mask={len(raw_loss_roi_mask)}."
|
| 355 |
+
)
|
| 356 |
+
normalized_roi_mask: list[float] = []
|
| 357 |
+
for mask_idx, mask_value in enumerate(raw_loss_roi_mask):
|
| 358 |
+
if isinstance(mask_value, bool) or not isinstance(
|
| 359 |
+
mask_value, (int, float)
|
| 360 |
+
):
|
| 361 |
+
raise RuntimeError(
|
| 362 |
+
f"Sample[{sample_idx}] span[{span_idx}] loss_roi_mask[{mask_idx}] "
|
| 363 |
+
f"invalid type={type(mask_value)}."
|
| 364 |
+
)
|
| 365 |
+
casted_mask = float(mask_value)
|
| 366 |
+
if (
|
| 367 |
+
not math.isfinite(casted_mask)
|
| 368 |
+
or casted_mask < 0.0
|
| 369 |
+
or casted_mask > 1.0
|
| 370 |
+
):
|
| 371 |
+
raise RuntimeError(
|
| 372 |
+
f"Sample[{sample_idx}] span[{span_idx}] loss_roi_mask[{mask_idx}] "
|
| 373 |
+
f"must be finite and in [0, 1], got {casted_mask}."
|
| 374 |
+
)
|
| 375 |
+
normalized_roi_mask.append(casted_mask)
|
| 376 |
+
loss_roi_mask = normalized_roi_mask
|
| 377 |
+
|
| 378 |
+
text_start_token_idx = None
|
| 379 |
+
if "text_start_token_idx" in span and span["text_start_token_idx"] is not None:
|
| 380 |
+
text_start_token_idx = _require_json_int(
|
| 381 |
+
span["text_start_token_idx"],
|
| 382 |
+
owner=owner,
|
| 383 |
+
field_name="text_start_token_idx",
|
| 384 |
+
)
|
| 385 |
+
if text_start_token_idx < 0:
|
| 386 |
+
raise RuntimeError(
|
| 387 |
+
f"{owner} text_start_token_idx must be >= 0, got {text_start_token_idx}."
|
| 388 |
+
)
|
| 389 |
+
text_end_token_idx = None
|
| 390 |
+
if "text_end_token_idx" in span and span["text_end_token_idx"] is not None:
|
| 391 |
+
text_end_token_idx = _require_json_int(
|
| 392 |
+
span["text_end_token_idx"],
|
| 393 |
+
owner=owner,
|
| 394 |
+
field_name="text_end_token_idx",
|
| 395 |
+
)
|
| 396 |
+
if text_end_token_idx < 0:
|
| 397 |
+
raise RuntimeError(
|
| 398 |
+
f"{owner} text_end_token_idx must be >= 0, got {text_end_token_idx}."
|
| 399 |
+
)
|
| 400 |
+
if (text_start_token_idx is None) != (text_end_token_idx is None):
|
| 401 |
+
raise RuntimeError(
|
| 402 |
+
f"{owner} text_start_token_idx/text_end_token_idx must be provided together."
|
| 403 |
+
)
|
| 404 |
+
if (
|
| 405 |
+
text_start_token_idx is not None
|
| 406 |
+
and text_end_token_idx < text_start_token_idx
|
| 407 |
+
):
|
| 408 |
+
raise RuntimeError(
|
| 409 |
+
f"{owner} text token bounds are inverted: start={text_start_token_idx}, end={text_end_token_idx}."
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
normalized.append(
|
| 413 |
+
MoTNormalizedSpan(
|
| 414 |
+
length=length,
|
| 415 |
+
role_id=role_id,
|
| 416 |
+
segment_id=segment_id,
|
| 417 |
+
values=normalized_values,
|
| 418 |
+
loss_start=loss_start,
|
| 419 |
+
loss_roi_mask=loss_roi_mask,
|
| 420 |
+
text_start_token_idx=text_start_token_idx,
|
| 421 |
+
text_end_token_idx=text_end_token_idx,
|
| 422 |
+
)
|
| 423 |
+
)
|
| 424 |
+
return normalized
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
def build_timeseries_tensors(
|
| 428 |
+
spans_by_sample: list[list[MoTNormalizedSpan]],
|
| 429 |
+
) -> dict[str, torch.Tensor]:
|
| 430 |
+
batch_size = len(spans_by_sample)
|
| 431 |
+
max_spans = max((len(spans) for spans in spans_by_sample), default=0)
|
| 432 |
+
max_len = max(
|
| 433 |
+
(span.length for spans in spans_by_sample for span in spans), default=0
|
| 434 |
+
)
|
| 435 |
+
|
| 436 |
+
ts_values = torch.zeros((batch_size, max_spans, max_len), dtype=torch.float32)
|
| 437 |
+
ts_lengths = torch.zeros((batch_size, max_spans), dtype=torch.int32)
|
| 438 |
+
ts_loss_start_idxs = torch.zeros((batch_size, max_spans), dtype=torch.int32)
|
| 439 |
+
ts_loss_roi_masks = torch.zeros(
|
| 440 |
+
(batch_size, max_spans, max_len), dtype=torch.float32
|
| 441 |
+
)
|
| 442 |
+
ts_roles = torch.full((batch_size, max_spans), -1, dtype=torch.int32)
|
| 443 |
+
ts_segment_ids = torch.full((batch_size, max_spans), -1, dtype=torch.int32)
|
| 444 |
+
ts_span_mask = torch.zeros((batch_size, max_spans), dtype=torch.bool)
|
| 445 |
+
|
| 446 |
+
for sample_idx, spans in enumerate(spans_by_sample):
|
| 447 |
+
for span_idx, span in enumerate(spans):
|
| 448 |
+
ts_values[sample_idx, span_idx, : span.length] = torch.tensor(
|
| 449 |
+
span.values, dtype=torch.float32
|
| 450 |
+
)
|
| 451 |
+
ts_lengths[sample_idx, span_idx] = int(span.length)
|
| 452 |
+
ts_loss_start_idxs[sample_idx, span_idx] = int(span.loss_start)
|
| 453 |
+
if span.loss_roi_mask is not None:
|
| 454 |
+
ts_loss_roi_masks[sample_idx, span_idx, : span.length] = torch.tensor(
|
| 455 |
+
span.loss_roi_mask, dtype=torch.float32
|
| 456 |
+
)
|
| 457 |
+
ts_roles[sample_idx, span_idx] = int(span.role_id)
|
| 458 |
+
ts_segment_ids[sample_idx, span_idx] = int(span.segment_id)
|
| 459 |
+
ts_span_mask[sample_idx, span_idx] = True
|
| 460 |
+
|
| 461 |
+
return {
|
| 462 |
+
"ts_values": ts_values,
|
| 463 |
+
"ts_lengths": ts_lengths,
|
| 464 |
+
"ts_loss_start_idxs": ts_loss_start_idxs,
|
| 465 |
+
"ts_loss_roi_masks": ts_loss_roi_masks,
|
| 466 |
+
"ts_roles": ts_roles,
|
| 467 |
+
"ts_segment_ids": ts_segment_ids,
|
| 468 |
+
"ts_span_mask": ts_span_mask,
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
def build_timeseries_text_token_tensors(
|
| 473 |
+
*,
|
| 474 |
+
input_ids_by_sample: list[list[int]],
|
| 475 |
+
spans_by_sample: list[list[MoTNormalizedSpan]],
|
| 476 |
+
max_spans_per_sample: int,
|
| 477 |
+
open_id_to_len: dict[int, int | None],
|
| 478 |
+
close_id_to_len: dict[int, int | None],
|
| 479 |
+
) -> dict[str, torch.Tensor]:
|
| 480 |
+
"""
|
| 481 |
+
Build per-span text-token boundary tensors aligned with the TS payload slots.
|
| 482 |
+
|
| 483 |
+
These tensors are intentionally batch-static:
|
| 484 |
+
- they depend only on tokenizer-visible text and already-normalized
|
| 485 |
+
`timeseries` payload ordering
|
| 486 |
+
- they do *not* depend on model hidden states or current TS layer depth
|
| 487 |
+
|
| 488 |
+
The runtime consumes these exact token boundaries directly instead of
|
| 489 |
+
rescanning every input row for `<ts> ... </ts>` on every forward pass.
|
| 490 |
+
"""
|
| 491 |
+
if len(input_ids_by_sample) != len(spans_by_sample):
|
| 492 |
+
raise RuntimeError(
|
| 493 |
+
"MoT text-span tensor builder expects input_ids and spans to share batch size, "
|
| 494 |
+
f"got input_rows={len(input_ids_by_sample)}, span_rows={len(spans_by_sample)}."
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
batch_size = len(spans_by_sample)
|
| 498 |
+
max_spans = max((len(spans) for spans in spans_by_sample), default=0)
|
| 499 |
+
if max_spans > max_spans_per_sample:
|
| 500 |
+
raise RuntimeError(
|
| 501 |
+
f"Too many TS spans in one sample for processor metadata: {max_spans} > {max_spans_per_sample}."
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
ts_text_start_token_idxs = torch.full(
|
| 505 |
+
(batch_size, max_spans), -1, dtype=torch.int32
|
| 506 |
+
)
|
| 507 |
+
ts_text_end_token_idxs = torch.full((batch_size, max_spans), -1, dtype=torch.int32)
|
| 508 |
+
|
| 509 |
+
for sample_idx, (token_ids, normalized_spans) in enumerate(
|
| 510 |
+
zip(input_ids_by_sample, spans_by_sample)
|
| 511 |
+
):
|
| 512 |
+
if all(
|
| 513 |
+
span.text_start_token_idx is not None
|
| 514 |
+
and span.text_end_token_idx is not None
|
| 515 |
+
for span in normalized_spans
|
| 516 |
+
):
|
| 517 |
+
for span_idx, span in enumerate(normalized_spans):
|
| 518 |
+
start_idx = int(span.text_start_token_idx)
|
| 519 |
+
end_idx = int(span.text_end_token_idx)
|
| 520 |
+
open_target = end_idx < 0 and int(span.role_id) == ROLE_TO_ID["target"]
|
| 521 |
+
if (
|
| 522 |
+
start_idx < 0
|
| 523 |
+
or (not open_target and end_idx < start_idx)
|
| 524 |
+
or (not open_target and end_idx >= len(token_ids))
|
| 525 |
+
or (open_target and start_idx >= len(token_ids))
|
| 526 |
+
):
|
| 527 |
+
raise RuntimeError(
|
| 528 |
+
"Precomputed MoT text bounds are invalid inside the processor: "
|
| 529 |
+
f"sample={sample_idx}, span={span_idx}, start={start_idx}, end={end_idx}, "
|
| 530 |
+
f"tokens={len(token_ids)}."
|
| 531 |
+
)
|
| 532 |
+
ts_text_start_token_idxs[sample_idx, span_idx] = start_idx
|
| 533 |
+
ts_text_end_token_idxs[sample_idx, span_idx] = end_idx
|
| 534 |
+
continue
|
| 535 |
+
|
| 536 |
+
if any(
|
| 537 |
+
span.text_start_token_idx is not None or span.text_end_token_idx is not None
|
| 538 |
+
for span in normalized_spans
|
| 539 |
+
):
|
| 540 |
+
raise RuntimeError(
|
| 541 |
+
f"Sample[{sample_idx}] mixes precomputed and tokenizer-derived TS text bounds."
|
| 542 |
+
)
|
| 543 |
+
parsed_spans = find_tokenized_ts_spans(
|
| 544 |
+
[int(token_id) for token_id in token_ids],
|
| 545 |
+
open_id_to_len=open_id_to_len,
|
| 546 |
+
close_id_to_len=close_id_to_len,
|
| 547 |
+
)
|
| 548 |
+
if len(parsed_spans) != len(normalized_spans):
|
| 549 |
+
raise RuntimeError(
|
| 550 |
+
"Parsed text spans and normalized TS payload spans disagree inside the processor: "
|
| 551 |
+
f"sample={sample_idx}, parsed={len(parsed_spans)}, payload={len(normalized_spans)}."
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
for span_idx, (span, (start_idx, end_idx, tagged_len)) in enumerate(
|
| 555 |
+
zip(normalized_spans, parsed_spans)
|
| 556 |
+
):
|
| 557 |
+
if tagged_len is not None and int(span.length) != int(tagged_len):
|
| 558 |
+
raise RuntimeError(
|
| 559 |
+
"TS payload length mismatch with text delimiter tags in processor input: "
|
| 560 |
+
f"sample={sample_idx}, span={span_idx}, payload_len={span.length}, tag_len={tagged_len}."
|
| 561 |
+
)
|
| 562 |
+
ts_text_start_token_idxs[sample_idx, span_idx] = int(start_idx)
|
| 563 |
+
ts_text_end_token_idxs[sample_idx, span_idx] = int(end_idx)
|
| 564 |
+
|
| 565 |
+
return {
|
| 566 |
+
"ts_text_start_token_idxs": ts_text_start_token_idxs,
|
| 567 |
+
"ts_text_end_token_idxs": ts_text_end_token_idxs,
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def build_mot_batch_timeseries_tensors(
|
| 572 |
+
*,
|
| 573 |
+
tokenizer: Any,
|
| 574 |
+
input_ids_by_sample: list[list[int]],
|
| 575 |
+
spans_by_sample: list[list[MoTNormalizedSpan]],
|
| 576 |
+
max_spans_per_sample: int,
|
| 577 |
+
) -> dict[str, torch.Tensor]:
|
| 578 |
+
"""
|
| 579 |
+
Build the full processor-side TS tensor bundle for one inference request.
|
| 580 |
+
|
| 581 |
+
The model consumes two aligned views:
|
| 582 |
+
- dense span/value tensors for the bridge path
|
| 583 |
+
- text-token boundary tensors for exact `<ts> ... </ts>` alignment
|
| 584 |
+
"""
|
| 585 |
+
|
| 586 |
+
needs_tokenized_delimiter_parse = any(
|
| 587 |
+
any(
|
| 588 |
+
span.text_start_token_idx is None or span.text_end_token_idx is None
|
| 589 |
+
for span in spans
|
| 590 |
+
)
|
| 591 |
+
for spans in spans_by_sample
|
| 592 |
+
)
|
| 593 |
+
if needs_tokenized_delimiter_parse:
|
| 594 |
+
open_id_to_len, close_id_to_len = resolve_ts_delimiter_id_maps(tokenizer)
|
| 595 |
+
else:
|
| 596 |
+
open_id_to_len, close_id_to_len = {}, {}
|
| 597 |
+
batch = build_timeseries_tensors(spans_by_sample)
|
| 598 |
+
text_bounds = build_timeseries_text_token_tensors(
|
| 599 |
+
input_ids_by_sample=input_ids_by_sample,
|
| 600 |
+
spans_by_sample=spans_by_sample,
|
| 601 |
+
max_spans_per_sample=max_spans_per_sample,
|
| 602 |
+
open_id_to_len=open_id_to_len,
|
| 603 |
+
close_id_to_len=close_id_to_len,
|
| 604 |
+
)
|
| 605 |
+
batch.update(text_bounds)
|
| 606 |
+
return batch
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
__all__ = [
|
| 610 |
+
"build_mot_batch_timeseries_tensors",
|
| 611 |
+
"normalize_timeseries_spans",
|
| 612 |
+
]
|
timebraid/model/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TimeBraid model core."""
|
| 2 |
+
|
| 3 |
+
from .loading import (
|
| 4 |
+
LoadedTimeBraid,
|
| 5 |
+
ValidatedTimeBraidCheckpointSource,
|
| 6 |
+
load_timebraid_checkpoint,
|
| 7 |
+
load_timebraid_tokenizer,
|
| 8 |
+
validate_timebraid_canonical_artifact,
|
| 9 |
+
validate_timebraid_checkpoint_source,
|
| 10 |
+
validate_timebraid_model,
|
| 11 |
+
)
|
| 12 |
+
from .mot import TimeBraidGenerateOutput
|
| 13 |
+
from .timebraid import (
|
| 14 |
+
TimeBraid,
|
| 15 |
+
TimeBraidConfig,
|
| 16 |
+
TimeBraidLossBreakdown,
|
| 17 |
+
TimeBraidOutput,
|
| 18 |
+
register_timebraid_auto_classes,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
__all__ = [
|
| 22 |
+
"TimeBraid",
|
| 23 |
+
"TimeBraidConfig",
|
| 24 |
+
"TimeBraidGenerateOutput",
|
| 25 |
+
"TimeBraidLossBreakdown",
|
| 26 |
+
"TimeBraidOutput",
|
| 27 |
+
"LoadedTimeBraid",
|
| 28 |
+
"ValidatedTimeBraidCheckpointSource",
|
| 29 |
+
"register_timebraid_auto_classes",
|
| 30 |
+
"load_timebraid_checkpoint",
|
| 31 |
+
"load_timebraid_tokenizer",
|
| 32 |
+
"validate_timebraid_canonical_artifact",
|
| 33 |
+
"validate_timebraid_model",
|
| 34 |
+
"validate_timebraid_checkpoint_source",
|
| 35 |
+
]
|
timebraid/model/loading.py
ADDED
|
@@ -0,0 +1,1038 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Strict loading for local or Hub TimeBraid checkpoints."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any, Literal
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from huggingface_hub import snapshot_download
|
| 13 |
+
from safetensors import safe_open
|
| 14 |
+
from transformers import (
|
| 15 |
+
AutoConfig,
|
| 16 |
+
AutoProcessor,
|
| 17 |
+
AutoTokenizer,
|
| 18 |
+
PreTrainedModel,
|
| 19 |
+
PreTrainedTokenizerBase,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
from .mot.config_contract import collect_mot_hf_config_contract
|
| 23 |
+
from .timebraid import (
|
| 24 |
+
TimeBraid,
|
| 25 |
+
TimeBraidConfig,
|
| 26 |
+
_validate_finite_model_tensors,
|
| 27 |
+
register_timebraid_auto_classes,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
_MODEL_INDEX_NAME = "model.safetensors.index.json"
|
| 31 |
+
_SINGLE_MODEL_NAME = "model.safetensors"
|
| 32 |
+
# Keep structural preflight bounded independently of untrusted checkpoint
|
| 33 |
+
# metadata; supported runtime payloads use a much smaller configured span cap.
|
| 34 |
+
_MAX_SPANS_PER_SAMPLE = 2048
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass(frozen=True, slots=True)
|
| 38 |
+
class LoadedTimeBraid:
|
| 39 |
+
"""A frozen TimeBraid model, its tokenizer, and explicit load provenance."""
|
| 40 |
+
|
| 41 |
+
model: PreTrainedModel
|
| 42 |
+
tokenizer: PreTrainedTokenizerBase
|
| 43 |
+
source: str
|
| 44 |
+
weight_dtype: str
|
| 45 |
+
compute_dtype: str
|
| 46 |
+
device_map: str | dict[str, str] | None
|
| 47 |
+
physical_tensor_dtypes: tuple[tuple[str, str], ...] = ()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass(frozen=True, slots=True)
|
| 51 |
+
class ValidatedTimeBraidCheckpointSource:
|
| 52 |
+
"""CPU-only structural preflight for one complete TimeBraid checkpoint."""
|
| 53 |
+
|
| 54 |
+
source: str
|
| 55 |
+
config: TimeBraidConfig
|
| 56 |
+
tokenizer: PreTrainedTokenizerBase
|
| 57 |
+
tensor_count: int
|
| 58 |
+
physical_tensor_dtypes: tuple[tuple[str, str], ...]
|
| 59 |
+
mot_ts_open_token_id: int
|
| 60 |
+
mot_ts_close_token_id: int
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _loading_info_key(item: Any) -> str:
|
| 64 |
+
if isinstance(item, (tuple, list)) and item:
|
| 65 |
+
return str(item[0])
|
| 66 |
+
return str(item)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _normalize_source(source: str | Path, *, field_name: str) -> str:
|
| 70 |
+
if not isinstance(source, (str, Path)):
|
| 71 |
+
raise TypeError(
|
| 72 |
+
f"{field_name} must be a local directory path or Hub ID, got {type(source).__name__}."
|
| 73 |
+
)
|
| 74 |
+
normalized = str(source).strip()
|
| 75 |
+
if not normalized:
|
| 76 |
+
raise ValueError(f"{field_name} must be non-empty.")
|
| 77 |
+
return normalized
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _resolve_hf_source(
|
| 81 |
+
source: str,
|
| 82 |
+
*,
|
| 83 |
+
revision: str | None,
|
| 84 |
+
token: str | None,
|
| 85 |
+
cache_dir: str | None,
|
| 86 |
+
local_files_only: bool,
|
| 87 |
+
) -> str:
|
| 88 |
+
"""Resolve an existing directory or one standard Hugging Face Hub repo."""
|
| 89 |
+
path = Path(source).expanduser()
|
| 90 |
+
if path.is_dir():
|
| 91 |
+
return str(path.resolve(strict=True))
|
| 92 |
+
if path.exists():
|
| 93 |
+
raise RuntimeError(f"source must be a directory or Hub ID, got {source!r}.")
|
| 94 |
+
return snapshot_download(
|
| 95 |
+
repo_id=source,
|
| 96 |
+
revision=revision,
|
| 97 |
+
token=token,
|
| 98 |
+
cache_dir=cache_dir,
|
| 99 |
+
local_files_only=local_files_only,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _strict_json_object(path: Path, *, owner: str) -> dict[str, Any]:
|
| 104 |
+
"""Read one strict JSON object without duplicate or non-finite values."""
|
| 105 |
+
|
| 106 |
+
def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
| 107 |
+
result: dict[str, Any] = {}
|
| 108 |
+
for key, value in pairs:
|
| 109 |
+
if key in result:
|
| 110 |
+
raise RuntimeError(f"{owner} contains duplicate key {key!r}: {path}")
|
| 111 |
+
result[key] = value
|
| 112 |
+
return result
|
| 113 |
+
|
| 114 |
+
def reject_constant(value: str) -> None:
|
| 115 |
+
raise RuntimeError(f"{owner} contains non-finite JSON number {value!r}: {path}")
|
| 116 |
+
|
| 117 |
+
def parse_finite_float(value: str) -> float:
|
| 118 |
+
parsed = float(value)
|
| 119 |
+
if not math.isfinite(parsed):
|
| 120 |
+
raise RuntimeError(
|
| 121 |
+
f"{owner} contains overflowing JSON number {value!r}: {path}"
|
| 122 |
+
)
|
| 123 |
+
return parsed
|
| 124 |
+
|
| 125 |
+
value = json.loads(
|
| 126 |
+
path.read_text(encoding="utf-8"),
|
| 127 |
+
object_pairs_hook=reject_duplicates,
|
| 128 |
+
parse_constant=reject_constant,
|
| 129 |
+
parse_float=parse_finite_float,
|
| 130 |
+
)
|
| 131 |
+
if not isinstance(value, dict):
|
| 132 |
+
raise RuntimeError(f"{owner} must contain one JSON object: {path}")
|
| 133 |
+
return value
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _safe_top_level_name(value: object, *, owner: str) -> str:
|
| 137 |
+
if (
|
| 138 |
+
not isinstance(value, str)
|
| 139 |
+
or not value
|
| 140 |
+
or Path(value).name != value
|
| 141 |
+
or "/" in value
|
| 142 |
+
or "\\" in value
|
| 143 |
+
or value in {".", ".."}
|
| 144 |
+
):
|
| 145 |
+
raise RuntimeError(f"{owner} must be a top-level filename, got {value!r}.")
|
| 146 |
+
return value
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _resolved_source_hf_kwargs(*, trust_remote_code: bool) -> dict[str, Any]:
|
| 150 |
+
"""Build reader kwargs after a Hub repo has resolved to one local snapshot."""
|
| 151 |
+
if trust_remote_code is not False:
|
| 152 |
+
raise ValueError(
|
| 153 |
+
"TimeBraid loading requires trust_remote_code=False; install and import "
|
| 154 |
+
"the TimeBraid package to register its Hugging Face classes."
|
| 155 |
+
)
|
| 156 |
+
return {
|
| 157 |
+
"local_files_only": True,
|
| 158 |
+
"trust_remote_code": False,
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _reject_auto_map(value: Any, *, owner: str) -> None:
|
| 163 |
+
"""Reject repository-provided Python entrypoints anywhere in HF metadata.
|
| 164 |
+
|
| 165 |
+
This is deliberately stricter than `trust_remote_code=False`: it refuses an
|
| 166 |
+
artifact that merely *carries* remote-code entrypoints, rather than only
|
| 167 |
+
declining to run them. That makes `load_timebraid_checkpoint` a loader for
|
| 168 |
+
canonical artifacts, which have no `auto_map`.
|
| 169 |
+
|
| 170 |
+
The published Hub artifact is not such an artifact — it ships `auto_map` on
|
| 171 |
+
purpose, so users who have not installed this package can load it with
|
| 172 |
+
`trust_remote_code=True`. Those users, and installed-package users alike,
|
| 173 |
+
go through `AutoModelForCausalLM.from_pretrained`, which works either way.
|
| 174 |
+
"""
|
| 175 |
+
pending = [value]
|
| 176 |
+
while pending:
|
| 177 |
+
current = pending.pop()
|
| 178 |
+
if isinstance(current, dict):
|
| 179 |
+
if "auto_map" in current:
|
| 180 |
+
raise RuntimeError(
|
| 181 |
+
f"{owner} must not contain auto_map; install and import the "
|
| 182 |
+
"TimeBraid package instead."
|
| 183 |
+
)
|
| 184 |
+
pending.extend(current.values())
|
| 185 |
+
elif isinstance(current, list):
|
| 186 |
+
pending.extend(current)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _local_safetensors_tensor_dtypes(root: str) -> dict[str, str]:
|
| 190 |
+
directory = Path(root)
|
| 191 |
+
index_path = directory / _MODEL_INDEX_NAME
|
| 192 |
+
single_path = directory / _SINGLE_MODEL_NAME
|
| 193 |
+
unsafe_weight_files = sorted(
|
| 194 |
+
path.name
|
| 195 |
+
for path in directory.iterdir()
|
| 196 |
+
if path.name == "pytorch_model.bin"
|
| 197 |
+
or path.name == "pytorch_model.bin.index.json"
|
| 198 |
+
or (path.name.startswith("pytorch_model-") and path.name.endswith(".bin"))
|
| 199 |
+
)
|
| 200 |
+
if unsafe_weight_files:
|
| 201 |
+
raise RuntimeError(
|
| 202 |
+
f"Strict loading rejects pickle-backed weights: {unsafe_weight_files}."
|
| 203 |
+
)
|
| 204 |
+
selectors = (index_path.is_file(), single_path.is_file())
|
| 205 |
+
if sum(selectors) != 1:
|
| 206 |
+
raise RuntimeError(
|
| 207 |
+
"Resolved source must contain exactly one safetensors selector: "
|
| 208 |
+
"model.safetensors or model.safetensors.index.json."
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
declared: dict[str, str] | None = None
|
| 212 |
+
if selectors[0]:
|
| 213 |
+
index = _strict_json_object(index_path, owner="Model safetensors index")
|
| 214 |
+
weight_map = index.get("weight_map")
|
| 215 |
+
if not isinstance(weight_map, dict) or not weight_map:
|
| 216 |
+
raise RuntimeError(
|
| 217 |
+
"Model safetensors index must contain a non-empty weight_map."
|
| 218 |
+
)
|
| 219 |
+
declared = {}
|
| 220 |
+
for tensor_name, shard_value in weight_map.items():
|
| 221 |
+
if not isinstance(tensor_name, str) or not tensor_name:
|
| 222 |
+
raise RuntimeError(
|
| 223 |
+
"Model safetensors index tensor names must be non-empty strings."
|
| 224 |
+
)
|
| 225 |
+
shard_name = _safe_top_level_name(
|
| 226 |
+
shard_value, owner=f"Shard for {tensor_name!r}"
|
| 227 |
+
)
|
| 228 |
+
if not shard_name.endswith(".safetensors"):
|
| 229 |
+
raise RuntimeError(
|
| 230 |
+
f"Model shard must use safetensors, got {shard_name!r}."
|
| 231 |
+
)
|
| 232 |
+
declared[tensor_name] = shard_name
|
| 233 |
+
shard_names = sorted(set(declared.values()))
|
| 234 |
+
else:
|
| 235 |
+
shard_names = [_SINGLE_MODEL_NAME]
|
| 236 |
+
|
| 237 |
+
observed: dict[str, tuple[str, str]] = {}
|
| 238 |
+
for shard_name in shard_names:
|
| 239 |
+
shard_path = directory / shard_name
|
| 240 |
+
if not shard_path.is_file():
|
| 241 |
+
raise RuntimeError(
|
| 242 |
+
f"Model shard must resolve to a regular file: {shard_path}"
|
| 243 |
+
)
|
| 244 |
+
try:
|
| 245 |
+
with safe_open(shard_path, framework="pt", device="cpu") as reader:
|
| 246 |
+
shard_keys = list(reader.keys())
|
| 247 |
+
for tensor_name in shard_keys:
|
| 248 |
+
tensor_slice = reader.get_slice(tensor_name)
|
| 249 |
+
if tensor_name in observed:
|
| 250 |
+
raise RuntimeError(
|
| 251 |
+
f"Safetensors tensor appears in multiple shards: {tensor_name!r}."
|
| 252 |
+
)
|
| 253 |
+
observed[tensor_name] = (
|
| 254 |
+
shard_name,
|
| 255 |
+
str(tensor_slice.get_dtype()),
|
| 256 |
+
)
|
| 257 |
+
except RuntimeError:
|
| 258 |
+
raise
|
| 259 |
+
except Exception as exc:
|
| 260 |
+
raise RuntimeError(
|
| 261 |
+
f"Could not validate safetensors shard: {shard_path}"
|
| 262 |
+
) from exc
|
| 263 |
+
if not observed:
|
| 264 |
+
raise RuntimeError("Model safetensors payload contains no tensors.")
|
| 265 |
+
observed_placement = {
|
| 266 |
+
tensor_name: shard_name
|
| 267 |
+
for tensor_name, (shard_name, _dtype) in observed.items()
|
| 268 |
+
}
|
| 269 |
+
if declared is not None and observed_placement != declared:
|
| 270 |
+
missing = sorted(set(declared) - set(observed))
|
| 271 |
+
extra = sorted(set(observed) - set(declared))
|
| 272 |
+
misplaced = {
|
| 273 |
+
name: {"declared": declared[name], "observed": observed_placement[name]}
|
| 274 |
+
for name in sorted(set(declared) & set(observed))
|
| 275 |
+
if declared[name] != observed_placement[name]
|
| 276 |
+
}
|
| 277 |
+
raise RuntimeError(
|
| 278 |
+
"Safetensors index does not match shard tensor placement: "
|
| 279 |
+
f"missing={missing}, extra={extra}, misplaced={misplaced}."
|
| 280 |
+
)
|
| 281 |
+
observed_shards = {
|
| 282 |
+
path.name for path in directory.iterdir() if path.name.endswith(".safetensors")
|
| 283 |
+
}
|
| 284 |
+
if observed_shards != set(shard_names):
|
| 285 |
+
raise RuntimeError(
|
| 286 |
+
"Model source contains undeclared safetensors shards: "
|
| 287 |
+
f"declared={shard_names!r}, observed={sorted(observed_shards)!r}."
|
| 288 |
+
)
|
| 289 |
+
return {
|
| 290 |
+
tensor_name: dtype for tensor_name, (_shard_name, dtype) in observed.items()
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def _reject_noncanonical_tied_aliases(
|
| 295 |
+
config_payload: dict[str, Any], tensor_names: set[str]
|
| 296 |
+
) -> None:
|
| 297 |
+
model_type = config_payload.get("model_type")
|
| 298 |
+
if model_type == TimeBraidConfig.model_type:
|
| 299 |
+
llm_config = config_payload.get("llm_config")
|
| 300 |
+
if not isinstance(llm_config, dict):
|
| 301 |
+
raise RuntimeError(
|
| 302 |
+
"TimeBraid config must contain a nested llm_config object."
|
| 303 |
+
)
|
| 304 |
+
tie_word_embeddings = llm_config.get("tie_word_embeddings", False)
|
| 305 |
+
lm_head_alias = "llm.lm_head.weight"
|
| 306 |
+
else:
|
| 307 |
+
tie_word_embeddings = config_payload.get("tie_word_embeddings", False)
|
| 308 |
+
lm_head_alias = "lm_head.weight"
|
| 309 |
+
if type(tie_word_embeddings) is not bool:
|
| 310 |
+
raise RuntimeError(
|
| 311 |
+
f"Model config tie_word_embeddings must be bool, got {tie_word_embeddings!r}."
|
| 312 |
+
)
|
| 313 |
+
forbidden = set()
|
| 314 |
+
if tie_word_embeddings and lm_head_alias in tensor_names:
|
| 315 |
+
forbidden.add(lm_head_alias)
|
| 316 |
+
if forbidden:
|
| 317 |
+
raise RuntimeError(
|
| 318 |
+
"Model safetensors contains noncanonical tied-weight aliases that HF may silently ignore: "
|
| 319 |
+
f"{sorted(forbidden)!r}."
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def _validate_local_model_payload(root: str) -> dict[str, str]:
|
| 324 |
+
directory = Path(root)
|
| 325 |
+
for forbidden_name in ("adapter_config.json", "additional_chat_templates"):
|
| 326 |
+
forbidden_path = directory / forbidden_name
|
| 327 |
+
try:
|
| 328 |
+
forbidden_path.lstat()
|
| 329 |
+
except FileNotFoundError:
|
| 330 |
+
continue
|
| 331 |
+
raise RuntimeError(
|
| 332 |
+
"Strict full-checkpoint loading rejects loader redirection or nested "
|
| 333 |
+
f"tokenizer templates: {forbidden_path}"
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
config_path = directory / "config.json"
|
| 337 |
+
if not config_path.is_file():
|
| 338 |
+
raise RuntimeError(
|
| 339 |
+
f"Model config must resolve to a regular file: {config_path}"
|
| 340 |
+
)
|
| 341 |
+
config_payload = _strict_json_object(config_path, owner="Model config")
|
| 342 |
+
_reject_auto_map(config_payload, owner="Model config")
|
| 343 |
+
tensor_dtypes = _local_safetensors_tensor_dtypes(root)
|
| 344 |
+
_reject_noncanonical_tied_aliases(config_payload, set(tensor_dtypes))
|
| 345 |
+
return tensor_dtypes
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def _validate_complete_checkpoint_load(
|
| 349 |
+
model: TimeBraid, loading_info: dict[str, Any]
|
| 350 |
+
) -> None:
|
| 351 |
+
"""Reject every tensor mismatch except live tied-weight aliases omitted by HF saves."""
|
| 352 |
+
tied_targets = set(getattr(model, "_tied_weights_keys", {}))
|
| 353 |
+
missing = sorted(
|
| 354 |
+
key
|
| 355 |
+
for item in (loading_info.get("missing_keys") or [])
|
| 356 |
+
if (key := _loading_info_key(item)) not in tied_targets
|
| 357 |
+
)
|
| 358 |
+
unexpected = sorted(
|
| 359 |
+
_loading_info_key(item) for item in (loading_info.get("unexpected_keys") or [])
|
| 360 |
+
)
|
| 361 |
+
mismatched = sorted(
|
| 362 |
+
str(item) for item in (loading_info.get("mismatched_keys") or [])
|
| 363 |
+
)
|
| 364 |
+
error_messages = [str(item) for item in (loading_info.get("error_msgs") or [])]
|
| 365 |
+
if missing or unexpected or mismatched or error_messages:
|
| 366 |
+
raise RuntimeError(
|
| 367 |
+
"TimeBraid checkpoint did not load exactly: "
|
| 368 |
+
f"missing={missing}, unexpected={unexpected}, mismatched={mismatched}, "
|
| 369 |
+
f"errors={error_messages}"
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def _validate_tokenizer_protocol(
|
| 374 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 375 |
+
*,
|
| 376 |
+
token_id_upper_bound: int,
|
| 377 |
+
) -> dict[str, int]:
|
| 378 |
+
if len(tokenizer) > token_id_upper_bound:
|
| 379 |
+
raise RuntimeError(
|
| 380 |
+
"TimeBraid tokenizer vocabulary exceeds the checkpoint embedding table: "
|
| 381 |
+
f"tokenizer={len(tokenizer)}, embeddings={token_id_upper_bound}."
|
| 382 |
+
)
|
| 383 |
+
delimiter_ids: dict[str, int] = {}
|
| 384 |
+
for delimiter in ("<ts>", "</ts>"):
|
| 385 |
+
token_ids = tokenizer(delimiter, add_special_tokens=False)["input_ids"]
|
| 386 |
+
if not isinstance(token_ids, list) or len(token_ids) != 1:
|
| 387 |
+
raise RuntimeError(
|
| 388 |
+
f"TimeBraid tokenizer must encode {delimiter!r} as exactly one token, got {token_ids!r}."
|
| 389 |
+
)
|
| 390 |
+
token_id = int(token_ids[0])
|
| 391 |
+
converted_id = tokenizer.convert_tokens_to_ids(delimiter)
|
| 392 |
+
if converted_id is None or int(converted_id) != token_id:
|
| 393 |
+
raise RuntimeError(
|
| 394 |
+
f"TimeBraid tokenizer has inconsistent ID resolution for {delimiter!r}: "
|
| 395 |
+
f"encode={token_id}, convert={converted_id!r}."
|
| 396 |
+
)
|
| 397 |
+
if tokenizer.unk_token_id is not None and token_id == int(
|
| 398 |
+
tokenizer.unk_token_id
|
| 399 |
+
):
|
| 400 |
+
raise RuntimeError(
|
| 401 |
+
f"TimeBraid tokenizer resolves {delimiter!r} to unk_token_id={token_id}."
|
| 402 |
+
)
|
| 403 |
+
if token_id < 0 or token_id >= token_id_upper_bound:
|
| 404 |
+
raise RuntimeError(
|
| 405 |
+
f"TimeBraid tokenizer encodes {delimiter!r} outside the embedding table: {token_id}."
|
| 406 |
+
)
|
| 407 |
+
delimiter_ids[delimiter] = token_id
|
| 408 |
+
if delimiter_ids["<ts>"] == delimiter_ids["</ts>"]:
|
| 409 |
+
raise RuntimeError(
|
| 410 |
+
"TimeBraid tokenizer maps <ts> and </ts> to the same token ID: "
|
| 411 |
+
f"{delimiter_ids['<ts>']}."
|
| 412 |
+
)
|
| 413 |
+
return delimiter_ids
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _load_exact_tokenizer(
|
| 417 |
+
source: str,
|
| 418 |
+
*,
|
| 419 |
+
use_fast_tokenizer: bool,
|
| 420 |
+
hf_kwargs: dict[str, Any],
|
| 421 |
+
) -> PreTrainedTokenizerBase:
|
| 422 |
+
tokenizer_config_path = Path(source) / "tokenizer_config.json"
|
| 423 |
+
tokenizer_config = _strict_json_object(
|
| 424 |
+
tokenizer_config_path,
|
| 425 |
+
owner="Tokenizer config",
|
| 426 |
+
)
|
| 427 |
+
_reject_auto_map(tokenizer_config, owner="Tokenizer config")
|
| 428 |
+
extra_special_tokens_kwargs: dict[str, Any] = {}
|
| 429 |
+
if "extra_special_tokens" in tokenizer_config:
|
| 430 |
+
extra_special_tokens = tokenizer_config["extra_special_tokens"]
|
| 431 |
+
if isinstance(extra_special_tokens, dict):
|
| 432 |
+
pass
|
| 433 |
+
elif isinstance(extra_special_tokens, list) and not extra_special_tokens:
|
| 434 |
+
# Older Qwen checkpoints serialized the empty model-specific token
|
| 435 |
+
# mapping as []. Transformers 4.57 calls .keys() on this value; an
|
| 436 |
+
# explicit {} preserves the same empty set without changing bytes.
|
| 437 |
+
extra_special_tokens_kwargs["extra_special_tokens"] = {}
|
| 438 |
+
elif isinstance(extra_special_tokens, list):
|
| 439 |
+
raise RuntimeError(
|
| 440 |
+
"tokenizer_config.json extra_special_tokens must be an object or []; "
|
| 441 |
+
f"got list[{len(extra_special_tokens)}]."
|
| 442 |
+
)
|
| 443 |
+
else:
|
| 444 |
+
raise RuntimeError(
|
| 445 |
+
"tokenizer_config.json extra_special_tokens must be an object or []; "
|
| 446 |
+
f"got {type(extra_special_tokens).__name__}."
|
| 447 |
+
)
|
| 448 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 449 |
+
source,
|
| 450 |
+
use_fast=use_fast_tokenizer,
|
| 451 |
+
padding_side="right",
|
| 452 |
+
# Preserve the serialized Qwen regex exactly; an automatic Mistral
|
| 453 |
+
# rewrite changes tokenizer identity and invalidates tokenized caches.
|
| 454 |
+
fix_mistral_regex=False,
|
| 455 |
+
**extra_special_tokens_kwargs,
|
| 456 |
+
**hf_kwargs,
|
| 457 |
+
)
|
| 458 |
+
if tokenizer.padding_side != "right":
|
| 459 |
+
raise RuntimeError(
|
| 460 |
+
f"TimeBraid tokenizer must use right padding, got {tokenizer.padding_side!r}."
|
| 461 |
+
)
|
| 462 |
+
return tokenizer
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def _validate_timebraid_config_surface(
|
| 466 |
+
config: TimeBraidConfig,
|
| 467 |
+
) -> tuple[int, dict[str, Any]]:
|
| 468 |
+
if config.model_type != TimeBraidConfig.model_type:
|
| 469 |
+
raise RuntimeError(
|
| 470 |
+
"Strict TimeBraid checkpoint loading requires model_type='timebraid', "
|
| 471 |
+
f"got {config.model_type!r}."
|
| 472 |
+
)
|
| 473 |
+
mot_contract = collect_mot_hf_config_contract(config)
|
| 474 |
+
if str(getattr(config.llm_config, "model_type", "")) != "qwen3":
|
| 475 |
+
raise RuntimeError(
|
| 476 |
+
"TimeBraid currently requires a nested Qwen3 backbone config, got "
|
| 477 |
+
f"{getattr(config.llm_config, 'model_type', None)!r}."
|
| 478 |
+
)
|
| 479 |
+
config_vocab_size = getattr(config.llm_config, "vocab_size", 0)
|
| 480 |
+
if type(config_vocab_size) is not int or config_vocab_size <= 0:
|
| 481 |
+
raise RuntimeError(
|
| 482 |
+
f"TimeBraid config has invalid vocab_size={config_vocab_size}."
|
| 483 |
+
)
|
| 484 |
+
return config_vocab_size, mot_contract
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def _validate_config_tokenizer_delimiters(
|
| 488 |
+
*, mot_contract: dict[str, Any], tokenizer_ids: dict[str, int]
|
| 489 |
+
) -> None:
|
| 490 |
+
configured = (
|
| 491 |
+
mot_contract["mot_ts_open_token_id"],
|
| 492 |
+
mot_contract["mot_ts_close_token_id"],
|
| 493 |
+
)
|
| 494 |
+
observed = (tokenizer_ids["<ts>"], tokenizer_ids["</ts>"])
|
| 495 |
+
if configured != observed:
|
| 496 |
+
raise RuntimeError(
|
| 497 |
+
"TimeBraid config TS delimiter IDs disagree with the tokenizer: "
|
| 498 |
+
f"config={configured}, tokenizer={observed}."
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
def _load_timebraid_config(
|
| 503 |
+
source: str,
|
| 504 |
+
*,
|
| 505 |
+
hf_kwargs: dict[str, Any],
|
| 506 |
+
) -> TimeBraidConfig:
|
| 507 |
+
"""Load one current TimeBraid config without mutating serialized fields."""
|
| 508 |
+
AutoConfig.register(TimeBraidConfig.model_type, TimeBraidConfig, exist_ok=True)
|
| 509 |
+
config = AutoConfig.from_pretrained(source, **hf_kwargs)
|
| 510 |
+
if not isinstance(config, TimeBraidConfig):
|
| 511 |
+
raise RuntimeError(
|
| 512 |
+
"TimeBraid checkpoint must resolve to TimeBraidConfig, got "
|
| 513 |
+
f"{type(config).__name__}."
|
| 514 |
+
)
|
| 515 |
+
return config
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
def validate_timebraid_checkpoint_source(
|
| 519 |
+
source: str | Path,
|
| 520 |
+
*,
|
| 521 |
+
revision: str | None = None,
|
| 522 |
+
token: str | None = None,
|
| 523 |
+
cache_dir: str | None = None,
|
| 524 |
+
local_files_only: bool = False,
|
| 525 |
+
trust_remote_code: bool = False,
|
| 526 |
+
use_fast_tokenizer: bool = True,
|
| 527 |
+
) -> ValidatedTimeBraidCheckpointSource:
|
| 528 |
+
"""Validate config, tokenizer, and safetensors without loading model tensors."""
|
| 529 |
+
hf_kwargs = _resolved_source_hf_kwargs(trust_remote_code=trust_remote_code)
|
| 530 |
+
source_str = _resolve_hf_source(
|
| 531 |
+
_normalize_source(source, field_name="source"),
|
| 532 |
+
revision=revision,
|
| 533 |
+
token=token,
|
| 534 |
+
cache_dir=cache_dir,
|
| 535 |
+
local_files_only=local_files_only,
|
| 536 |
+
)
|
| 537 |
+
tensor_dtypes = _validate_local_model_payload(source_str)
|
| 538 |
+
tokenizer = _load_exact_tokenizer(
|
| 539 |
+
source_str,
|
| 540 |
+
use_fast_tokenizer=use_fast_tokenizer,
|
| 541 |
+
hf_kwargs=hf_kwargs,
|
| 542 |
+
)
|
| 543 |
+
config = _load_timebraid_config(
|
| 544 |
+
source_str,
|
| 545 |
+
hf_kwargs=hf_kwargs,
|
| 546 |
+
)
|
| 547 |
+
config_vocab_size, mot_contract = _validate_timebraid_config_surface(config)
|
| 548 |
+
token_ids = _validate_tokenizer_protocol(
|
| 549 |
+
tokenizer,
|
| 550 |
+
token_id_upper_bound=config_vocab_size,
|
| 551 |
+
)
|
| 552 |
+
_validate_config_tokenizer_delimiters(
|
| 553 |
+
mot_contract=mot_contract,
|
| 554 |
+
tokenizer_ids=token_ids,
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
return ValidatedTimeBraidCheckpointSource(
|
| 558 |
+
source=source_str,
|
| 559 |
+
config=config,
|
| 560 |
+
tokenizer=tokenizer,
|
| 561 |
+
tensor_count=len(tensor_dtypes),
|
| 562 |
+
physical_tensor_dtypes=tuple(sorted(tensor_dtypes.items())),
|
| 563 |
+
mot_ts_open_token_id=token_ids["<ts>"],
|
| 564 |
+
mot_ts_close_token_id=token_ids["</ts>"],
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def load_timebraid_tokenizer(
|
| 569 |
+
source: str | Path,
|
| 570 |
+
*,
|
| 571 |
+
revision: str | None = None,
|
| 572 |
+
token: str | None = None,
|
| 573 |
+
cache_dir: str | None = None,
|
| 574 |
+
local_files_only: bool = False,
|
| 575 |
+
trust_remote_code: bool = False,
|
| 576 |
+
use_fast_tokenizer: bool = True,
|
| 577 |
+
) -> PreTrainedTokenizerBase:
|
| 578 |
+
"""Load the exact tokenizer from one local directory or Hub repo."""
|
| 579 |
+
hf_kwargs = _resolved_source_hf_kwargs(trust_remote_code=trust_remote_code)
|
| 580 |
+
source_str = _resolve_hf_source(
|
| 581 |
+
_normalize_source(source, field_name="source"),
|
| 582 |
+
revision=revision,
|
| 583 |
+
token=token,
|
| 584 |
+
cache_dir=cache_dir,
|
| 585 |
+
local_files_only=local_files_only,
|
| 586 |
+
)
|
| 587 |
+
tokenizer = _load_exact_tokenizer(
|
| 588 |
+
source_str,
|
| 589 |
+
use_fast_tokenizer=use_fast_tokenizer,
|
| 590 |
+
hf_kwargs=hf_kwargs,
|
| 591 |
+
)
|
| 592 |
+
# This source-only preflight fails before model allocation. The model loader
|
| 593 |
+
# repeats it against the actual embedding rows.
|
| 594 |
+
_validate_tokenizer_protocol(
|
| 595 |
+
tokenizer,
|
| 596 |
+
token_id_upper_bound=len(tokenizer),
|
| 597 |
+
)
|
| 598 |
+
return tokenizer
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
def _validate_tied_weights(model: TimeBraid) -> None:
|
| 602 |
+
for target_name, source_name in getattr(model, "_tied_weights_keys", {}).items():
|
| 603 |
+
try:
|
| 604 |
+
target = model.get_parameter(target_name)
|
| 605 |
+
source = model.get_parameter(source_name)
|
| 606 |
+
except AttributeError as exc:
|
| 607 |
+
raise RuntimeError(
|
| 608 |
+
f"TimeBraid tied-weight path is missing: {target_name!r} -> {source_name!r}."
|
| 609 |
+
) from exc
|
| 610 |
+
if target.data_ptr() != source.data_ptr():
|
| 611 |
+
raise RuntimeError(
|
| 612 |
+
f"TimeBraid tied weights do not share storage: {target_name!r} -> {source_name!r}."
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
def _validate_no_meta_tensors(model: PreTrainedModel, *, owner: str) -> None:
|
| 617 |
+
meta_tensors = sorted(
|
| 618 |
+
name
|
| 619 |
+
for name, tensor in tuple(model.named_parameters())
|
| 620 |
+
+ tuple(model.named_buffers())
|
| 621 |
+
if tensor.is_meta
|
| 622 |
+
)
|
| 623 |
+
if meta_tensors:
|
| 624 |
+
raise RuntimeError(
|
| 625 |
+
f"{owner} left meta tensors after loading: {meta_tensors[:50]}."
|
| 626 |
+
)
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
def _validate_model(
|
| 630 |
+
model: TimeBraid,
|
| 631 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 632 |
+
) -> None:
|
| 633 |
+
input_embeddings = model.get_input_embeddings()
|
| 634 |
+
if input_embeddings is None or not hasattr(input_embeddings, "weight"):
|
| 635 |
+
raise RuntimeError("Loaded TimeBraid model has no input embedding weight.")
|
| 636 |
+
token_ids = _validate_tokenizer_protocol(
|
| 637 |
+
tokenizer,
|
| 638 |
+
token_id_upper_bound=int(input_embeddings.weight.shape[0]),
|
| 639 |
+
)
|
| 640 |
+
_validate_tied_weights(model)
|
| 641 |
+
_validate_no_meta_tensors(model, owner="TimeBraid checkpoint")
|
| 642 |
+
configured = (
|
| 643 |
+
getattr(model, "ts_open_token_id", None),
|
| 644 |
+
getattr(model, "ts_close_token_id", None),
|
| 645 |
+
)
|
| 646 |
+
observed = (token_ids["<ts>"], token_ids["</ts>"])
|
| 647 |
+
if configured != observed:
|
| 648 |
+
raise RuntimeError(
|
| 649 |
+
"TimeBraid TS delimiter IDs disagree with the tokenizer: "
|
| 650 |
+
f"model={configured}, tokenizer={observed}."
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
def validate_timebraid_model(
|
| 655 |
+
model: TimeBraid,
|
| 656 |
+
tokenizer: PreTrainedTokenizerBase,
|
| 657 |
+
) -> None:
|
| 658 |
+
"""Validate the loaded TimeBraid tensor, tokenizer, and component surface."""
|
| 659 |
+
_validate_model(model, tokenizer)
|
| 660 |
+
|
| 661 |
+
|
| 662 |
+
def _resolve_dtype(
|
| 663 |
+
value: torch.dtype | str | None, *, field_name: str
|
| 664 |
+
) -> torch.dtype | Literal["auto"]:
|
| 665 |
+
if value is None or value == "auto":
|
| 666 |
+
return "auto"
|
| 667 |
+
if isinstance(value, torch.dtype):
|
| 668 |
+
return value
|
| 669 |
+
aliases = {
|
| 670 |
+
"bf16": torch.bfloat16,
|
| 671 |
+
"bfloat16": torch.bfloat16,
|
| 672 |
+
"fp16": torch.float16,
|
| 673 |
+
"float16": torch.float16,
|
| 674 |
+
"fp32": torch.float32,
|
| 675 |
+
"float32": torch.float32,
|
| 676 |
+
}
|
| 677 |
+
resolved = aliases.get(str(value).strip().lower())
|
| 678 |
+
if resolved is None:
|
| 679 |
+
raise ValueError(
|
| 680 |
+
f"{field_name} must be auto, bf16, fp16, or fp32, got {value!r}."
|
| 681 |
+
)
|
| 682 |
+
return resolved
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
def _resolve_compute_dtype(
|
| 686 |
+
value: torch.dtype | str | None,
|
| 687 |
+
*,
|
| 688 |
+
model_dtype: torch.dtype | str | None,
|
| 689 |
+
accelerator_is_cuda: bool,
|
| 690 |
+
) -> torch.dtype:
|
| 691 |
+
resolved = _resolve_dtype(value, field_name="compute_dtype")
|
| 692 |
+
if resolved != "auto":
|
| 693 |
+
return resolved
|
| 694 |
+
if not accelerator_is_cuda:
|
| 695 |
+
return torch.float32
|
| 696 |
+
normalized_model_dtype = (
|
| 697 |
+
str(model_dtype).removeprefix("torch.") if model_dtype is not None else None
|
| 698 |
+
)
|
| 699 |
+
if torch.cuda.is_bf16_supported() and normalized_model_dtype in {None, "bfloat16"}:
|
| 700 |
+
return torch.bfloat16
|
| 701 |
+
return torch.float16
|
| 702 |
+
|
| 703 |
+
|
| 704 |
+
def _dtype_name(value: torch.dtype | Literal["auto"]) -> str:
|
| 705 |
+
return value if value == "auto" else str(value).removeprefix("torch.")
|
| 706 |
+
|
| 707 |
+
|
| 708 |
+
def _resolve_device_map(
|
| 709 |
+
device: str | torch.device | None,
|
| 710 |
+
*,
|
| 711 |
+
low_cpu_mem_usage: bool,
|
| 712 |
+
) -> tuple[str | dict[str, str] | None, bool]:
|
| 713 |
+
device_is_auto = isinstance(device, str) and device.strip().lower() == "auto"
|
| 714 |
+
if device_is_auto:
|
| 715 |
+
raise ValueError(
|
| 716 |
+
"device='auto' is unsupported because TimeBraid requires one explicit model device."
|
| 717 |
+
)
|
| 718 |
+
try:
|
| 719 |
+
requested_device = None if device is None else torch.device(device)
|
| 720 |
+
except (RuntimeError, TypeError) as exc:
|
| 721 |
+
raise ValueError(
|
| 722 |
+
f"device must be cpu, cuda, or cuda:N, got {device!r}."
|
| 723 |
+
) from exc
|
| 724 |
+
accelerator_is_cuda = bool(
|
| 725 |
+
requested_device is not None and requested_device.type == "cuda"
|
| 726 |
+
)
|
| 727 |
+
if device is None:
|
| 728 |
+
return None, accelerator_is_cuda
|
| 729 |
+
if not low_cpu_mem_usage:
|
| 730 |
+
raise ValueError("device loading requires low_cpu_mem_usage=true.")
|
| 731 |
+
if requested_device is None:
|
| 732 |
+
raise RuntimeError("Explicit device resolution produced no device.")
|
| 733 |
+
if requested_device.type not in {"cpu", "cuda"}:
|
| 734 |
+
raise ValueError(f"device must be cpu or cuda, got {requested_device.type!r}.")
|
| 735 |
+
if requested_device.type == "cuda" and not torch.cuda.is_available():
|
| 736 |
+
raise RuntimeError(
|
| 737 |
+
f"CUDA device requested but CUDA is unavailable: {requested_device}."
|
| 738 |
+
)
|
| 739 |
+
return {"": str(requested_device)}, accelerator_is_cuda
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
def _model_config_dtype(config: Any) -> torch.dtype | str | None:
|
| 743 |
+
return getattr(config, "dtype", None)
|
| 744 |
+
|
| 745 |
+
|
| 746 |
+
def load_timebraid_checkpoint(
|
| 747 |
+
source: str | Path,
|
| 748 |
+
*,
|
| 749 |
+
compute_dtype: torch.dtype | str | None = "auto",
|
| 750 |
+
weight_dtype: torch.dtype | str | None = "auto",
|
| 751 |
+
device: str | torch.device | None = None,
|
| 752 |
+
attention_implementation: Literal["eager", "sdpa", "flash_attention_2"]
|
| 753 |
+
| None = None,
|
| 754 |
+
revision: str | None = None,
|
| 755 |
+
token: str | None = None,
|
| 756 |
+
cache_dir: str | None = None,
|
| 757 |
+
local_files_only: bool = False,
|
| 758 |
+
trust_remote_code: bool = False,
|
| 759 |
+
use_fast_tokenizer: bool = True,
|
| 760 |
+
low_cpu_mem_usage: bool = True,
|
| 761 |
+
max_spans_per_sample: int = 1024,
|
| 762 |
+
) -> LoadedTimeBraid:
|
| 763 |
+
"""Load one local or Hub safetensors-only TimeBraid checkpoint."""
|
| 764 |
+
_resolved_source_hf_kwargs(trust_remote_code=trust_remote_code)
|
| 765 |
+
resolved_device_map, accelerator_is_cuda = _resolve_device_map(
|
| 766 |
+
device,
|
| 767 |
+
low_cpu_mem_usage=low_cpu_mem_usage,
|
| 768 |
+
)
|
| 769 |
+
validated = validate_timebraid_checkpoint_source(
|
| 770 |
+
source,
|
| 771 |
+
revision=revision,
|
| 772 |
+
token=token,
|
| 773 |
+
cache_dir=cache_dir,
|
| 774 |
+
local_files_only=local_files_only,
|
| 775 |
+
trust_remote_code=trust_remote_code,
|
| 776 |
+
use_fast_tokenizer=use_fast_tokenizer,
|
| 777 |
+
)
|
| 778 |
+
source_str = validated.source
|
| 779 |
+
tokenizer = validated.tokenizer
|
| 780 |
+
config = validated.config
|
| 781 |
+
hf_kwargs = _resolved_source_hf_kwargs(trust_remote_code=trust_remote_code)
|
| 782 |
+
if attention_implementation is not None:
|
| 783 |
+
config.llm_config._attn_implementation = attention_implementation
|
| 784 |
+
|
| 785 |
+
if type(max_spans_per_sample) is not int:
|
| 786 |
+
raise ValueError(
|
| 787 |
+
f"max_spans_per_sample must be an exact integer, got {max_spans_per_sample!r}."
|
| 788 |
+
)
|
| 789 |
+
if not 1 <= max_spans_per_sample <= _MAX_SPANS_PER_SAMPLE:
|
| 790 |
+
raise ValueError(
|
| 791 |
+
"max_spans_per_sample must stay in the maintained runtime range "
|
| 792 |
+
f"[1, {_MAX_SPANS_PER_SAMPLE}], got {max_spans_per_sample}."
|
| 793 |
+
)
|
| 794 |
+
resolved_weight_dtype = _resolve_dtype(weight_dtype, field_name="weight_dtype")
|
| 795 |
+
resolved_compute_dtype = _resolve_compute_dtype(
|
| 796 |
+
compute_dtype,
|
| 797 |
+
model_dtype=_model_config_dtype(config.llm_config),
|
| 798 |
+
accelerator_is_cuda=accelerator_is_cuda,
|
| 799 |
+
)
|
| 800 |
+
runtime_options: dict[str, Any] = {
|
| 801 |
+
"mot_max_spans_per_sample": max_spans_per_sample,
|
| 802 |
+
}
|
| 803 |
+
# Transformers also forwards unknown model kwargs to GenerationConfig.
|
| 804 |
+
# Keep this custom value JSON-safe while the runtime accepts the same
|
| 805 |
+
# canonical dtype spelling.
|
| 806 |
+
runtime_options["mot_compute_dtype"] = _dtype_name(resolved_compute_dtype)
|
| 807 |
+
|
| 808 |
+
model_kwargs: dict[str, Any] = {
|
| 809 |
+
**hf_kwargs,
|
| 810 |
+
"config": config,
|
| 811 |
+
"low_cpu_mem_usage": low_cpu_mem_usage,
|
| 812 |
+
"output_loading_info": True,
|
| 813 |
+
"runtime_options": runtime_options,
|
| 814 |
+
"dtype": resolved_weight_dtype,
|
| 815 |
+
"use_safetensors": True,
|
| 816 |
+
}
|
| 817 |
+
if resolved_device_map is not None:
|
| 818 |
+
model_kwargs["device_map"] = resolved_device_map
|
| 819 |
+
|
| 820 |
+
model, loading_info = TimeBraid.from_pretrained(source_str, **model_kwargs)
|
| 821 |
+
_validate_complete_checkpoint_load(model, loading_info)
|
| 822 |
+
_validate_model(model, tokenizer)
|
| 823 |
+
model.requires_grad_(False)
|
| 824 |
+
model.eval()
|
| 825 |
+
return LoadedTimeBraid(
|
| 826 |
+
model=model,
|
| 827 |
+
tokenizer=tokenizer,
|
| 828 |
+
source=source_str,
|
| 829 |
+
weight_dtype=_dtype_name(resolved_weight_dtype),
|
| 830 |
+
compute_dtype=_dtype_name(resolved_compute_dtype),
|
| 831 |
+
device_map=resolved_device_map,
|
| 832 |
+
physical_tensor_dtypes=validated.physical_tensor_dtypes,
|
| 833 |
+
)
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
def _safetensors_dtype_code(dtype: torch.dtype) -> str:
|
| 837 |
+
codes = {
|
| 838 |
+
torch.bool: "BOOL",
|
| 839 |
+
torch.uint8: "U8",
|
| 840 |
+
torch.int8: "I8",
|
| 841 |
+
torch.int16: "I16",
|
| 842 |
+
torch.int32: "I32",
|
| 843 |
+
torch.int64: "I64",
|
| 844 |
+
torch.float16: "F16",
|
| 845 |
+
torch.bfloat16: "BF16",
|
| 846 |
+
torch.float32: "F32",
|
| 847 |
+
torch.float64: "F64",
|
| 848 |
+
}
|
| 849 |
+
code = codes.get(dtype)
|
| 850 |
+
if code is None:
|
| 851 |
+
raise RuntimeError(f"TimeBraid release tensor has unsupported dtype {dtype}.")
|
| 852 |
+
return code
|
| 853 |
+
|
| 854 |
+
|
| 855 |
+
def _validate_timebraid_processor_artifact(
|
| 856 |
+
source: str,
|
| 857 |
+
*,
|
| 858 |
+
use_fast_tokenizer: bool,
|
| 859 |
+
trust_remote_code: bool,
|
| 860 |
+
) -> None:
|
| 861 |
+
"""Require one self-contained HF processor package at a resolved source."""
|
| 862 |
+
hf_kwargs = _resolved_source_hf_kwargs(trust_remote_code=trust_remote_code)
|
| 863 |
+
root = Path(source)
|
| 864 |
+
processor_config_path = root / "processor_config.json"
|
| 865 |
+
if not processor_config_path.is_file():
|
| 866 |
+
raise RuntimeError(
|
| 867 |
+
"TimeBraid release artifact must contain processor_config.json: "
|
| 868 |
+
f"{processor_config_path}"
|
| 869 |
+
)
|
| 870 |
+
processor_config = _strict_json_object(
|
| 871 |
+
processor_config_path,
|
| 872 |
+
owner="Processor config",
|
| 873 |
+
)
|
| 874 |
+
tokenizer_config = _strict_json_object(
|
| 875 |
+
root / "tokenizer_config.json",
|
| 876 |
+
owner="Tokenizer config",
|
| 877 |
+
)
|
| 878 |
+
_reject_auto_map(processor_config, owner="Processor config")
|
| 879 |
+
_reject_auto_map(tokenizer_config, owner="Tokenizer config")
|
| 880 |
+
if processor_config.get("processor_class") != "TimeBraidProcessor":
|
| 881 |
+
raise RuntimeError(
|
| 882 |
+
"processor_config.json processor_class must be 'TimeBraidProcessor', got "
|
| 883 |
+
f"{processor_config.get('processor_class')!r}."
|
| 884 |
+
)
|
| 885 |
+
max_spans_per_sample = processor_config.get("max_spans_per_sample")
|
| 886 |
+
if (
|
| 887 |
+
type(max_spans_per_sample) is not int
|
| 888 |
+
or not 1 <= max_spans_per_sample <= _MAX_SPANS_PER_SAMPLE
|
| 889 |
+
):
|
| 890 |
+
raise RuntimeError(
|
| 891 |
+
"processor_config.json max_spans_per_sample must be an integer in "
|
| 892 |
+
f"[1, {_MAX_SPANS_PER_SAMPLE}], got {max_spans_per_sample!r}."
|
| 893 |
+
)
|
| 894 |
+
normalization_epsilon = processor_config.get("normalization_epsilon")
|
| 895 |
+
if (
|
| 896 |
+
isinstance(normalization_epsilon, bool)
|
| 897 |
+
or not isinstance(normalization_epsilon, (int, float))
|
| 898 |
+
or not math.isfinite(float(normalization_epsilon))
|
| 899 |
+
or float(normalization_epsilon) <= 0.0
|
| 900 |
+
):
|
| 901 |
+
raise RuntimeError(
|
| 902 |
+
"processor_config.json normalization_epsilon must be finite and positive, "
|
| 903 |
+
f"got {normalization_epsilon!r}."
|
| 904 |
+
)
|
| 905 |
+
if tokenizer_config.get("processor_class") != "TimeBraidProcessor":
|
| 906 |
+
raise RuntimeError(
|
| 907 |
+
"tokenizer_config.json processor_class must be 'TimeBraidProcessor', got "
|
| 908 |
+
f"{tokenizer_config.get('processor_class')!r}."
|
| 909 |
+
)
|
| 910 |
+
extra_special_tokens = tokenizer_config.get("extra_special_tokens")
|
| 911 |
+
if not isinstance(extra_special_tokens, dict):
|
| 912 |
+
raise RuntimeError(
|
| 913 |
+
"tokenizer_config.json extra_special_tokens must be an object in a release "
|
| 914 |
+
f"artifact, got {type(extra_special_tokens).__name__}."
|
| 915 |
+
)
|
| 916 |
+
if tokenizer_config.get("fix_mistral_regex") is not False:
|
| 917 |
+
raise RuntimeError(
|
| 918 |
+
"tokenizer_config.json fix_mistral_regex must be false to preserve the "
|
| 919 |
+
"trained tokenizer identity."
|
| 920 |
+
)
|
| 921 |
+
|
| 922 |
+
# Registration is process-local. The artifact itself stays free of remote
|
| 923 |
+
# auto_map code, while this gate exercises the same bare AutoProcessor call
|
| 924 |
+
# available to an installed TimeBraid package user.
|
| 925 |
+
register_timebraid_auto_classes()
|
| 926 |
+
from ..processing_timebraid import TimeBraidProcessor
|
| 927 |
+
|
| 928 |
+
processor = AutoProcessor.from_pretrained(
|
| 929 |
+
source,
|
| 930 |
+
use_fast=use_fast_tokenizer,
|
| 931 |
+
**hf_kwargs,
|
| 932 |
+
)
|
| 933 |
+
if not isinstance(processor, TimeBraidProcessor):
|
| 934 |
+
raise RuntimeError(
|
| 935 |
+
f"AutoProcessor must load TimeBraidProcessor, got {type(processor).__name__}."
|
| 936 |
+
)
|
| 937 |
+
if processor.max_spans_per_sample != max_spans_per_sample:
|
| 938 |
+
raise RuntimeError(
|
| 939 |
+
"AutoProcessor max_spans_per_sample disagrees with processor_config.json: "
|
| 940 |
+
f"{processor.max_spans_per_sample} != {max_spans_per_sample}."
|
| 941 |
+
)
|
| 942 |
+
if processor.normalization_epsilon != float(normalization_epsilon):
|
| 943 |
+
raise RuntimeError(
|
| 944 |
+
"AutoProcessor normalization_epsilon disagrees with processor_config.json: "
|
| 945 |
+
f"{processor.normalization_epsilon} != {normalization_epsilon}."
|
| 946 |
+
)
|
| 947 |
+
|
| 948 |
+
|
| 949 |
+
def validate_timebraid_canonical_artifact(
|
| 950 |
+
source: str | Path,
|
| 951 |
+
*,
|
| 952 |
+
revision: str | None = None,
|
| 953 |
+
token: str | None = None,
|
| 954 |
+
cache_dir: str | None = None,
|
| 955 |
+
local_files_only: bool = False,
|
| 956 |
+
trust_remote_code: bool = False,
|
| 957 |
+
use_fast_tokenizer: bool = True,
|
| 958 |
+
low_cpu_mem_usage: bool = True,
|
| 959 |
+
) -> LoadedTimeBraid:
|
| 960 |
+
"""Run the one-time exact-load and finite-state release check."""
|
| 961 |
+
_resolved_source_hf_kwargs(trust_remote_code=trust_remote_code)
|
| 962 |
+
resolved_source = _resolve_hf_source(
|
| 963 |
+
_normalize_source(source, field_name="source"),
|
| 964 |
+
revision=revision,
|
| 965 |
+
token=token,
|
| 966 |
+
cache_dir=cache_dir,
|
| 967 |
+
local_files_only=local_files_only,
|
| 968 |
+
)
|
| 969 |
+
_validate_timebraid_processor_artifact(
|
| 970 |
+
resolved_source,
|
| 971 |
+
use_fast_tokenizer=use_fast_tokenizer,
|
| 972 |
+
trust_remote_code=trust_remote_code,
|
| 973 |
+
)
|
| 974 |
+
loaded = load_timebraid_checkpoint(
|
| 975 |
+
resolved_source,
|
| 976 |
+
compute_dtype="auto",
|
| 977 |
+
weight_dtype="auto",
|
| 978 |
+
device=None,
|
| 979 |
+
local_files_only=True,
|
| 980 |
+
trust_remote_code=trust_remote_code,
|
| 981 |
+
use_fast_tokenizer=use_fast_tokenizer,
|
| 982 |
+
low_cpu_mem_usage=low_cpu_mem_usage,
|
| 983 |
+
)
|
| 984 |
+
nested_config = getattr(getattr(loaded.model, "config", None), "llm_config", None)
|
| 985 |
+
declared_dtype = str(getattr(nested_config, "dtype", None)).removeprefix("torch.")
|
| 986 |
+
if declared_dtype != "float32":
|
| 987 |
+
raise RuntimeError(
|
| 988 |
+
"TimeBraid release artifact must declare llm_config.dtype='float32', "
|
| 989 |
+
f"got {declared_dtype!r}."
|
| 990 |
+
)
|
| 991 |
+
non_f32_tensors = {
|
| 992 |
+
tensor_name: physical_dtype
|
| 993 |
+
for tensor_name, physical_dtype in loaded.physical_tensor_dtypes
|
| 994 |
+
if physical_dtype != "F32"
|
| 995 |
+
}
|
| 996 |
+
if non_f32_tensors:
|
| 997 |
+
sample = dict(list(sorted(non_f32_tensors.items()))[:20])
|
| 998 |
+
raise RuntimeError(
|
| 999 |
+
"TimeBraid release safetensors must physically contain only F32 tensors; "
|
| 1000 |
+
f"non_f32={sample}."
|
| 1001 |
+
)
|
| 1002 |
+
state = loaded.model.state_dict()
|
| 1003 |
+
dtype_mismatches = {}
|
| 1004 |
+
for tensor_name, physical_dtype in loaded.physical_tensor_dtypes:
|
| 1005 |
+
tensor = state.get(tensor_name)
|
| 1006 |
+
if tensor is None:
|
| 1007 |
+
raise RuntimeError(
|
| 1008 |
+
f"TimeBraid release tensor is absent after exact load: {tensor_name!r}."
|
| 1009 |
+
)
|
| 1010 |
+
loaded_dtype = _safetensors_dtype_code(tensor.dtype)
|
| 1011 |
+
if physical_dtype != loaded_dtype:
|
| 1012 |
+
dtype_mismatches[tensor_name] = {
|
| 1013 |
+
"physical": physical_dtype,
|
| 1014 |
+
"loaded": loaded_dtype,
|
| 1015 |
+
}
|
| 1016 |
+
if dtype_mismatches:
|
| 1017 |
+
sample = dict(list(sorted(dtype_mismatches.items()))[:20])
|
| 1018 |
+
raise RuntimeError(
|
| 1019 |
+
"TimeBraid release safetensors must physically match their loaded dtypes; "
|
| 1020 |
+
f"mismatches={sample}. Validate the final F32 artifact without "
|
| 1021 |
+
"load-time dtype conversion."
|
| 1022 |
+
)
|
| 1023 |
+
_validate_finite_model_tensors(
|
| 1024 |
+
loaded.model,
|
| 1025 |
+
owner="TimeBraid release artifact",
|
| 1026 |
+
)
|
| 1027 |
+
return loaded
|
| 1028 |
+
|
| 1029 |
+
|
| 1030 |
+
__all__ = [
|
| 1031 |
+
"LoadedTimeBraid",
|
| 1032 |
+
"ValidatedTimeBraidCheckpointSource",
|
| 1033 |
+
"load_timebraid_checkpoint",
|
| 1034 |
+
"load_timebraid_tokenizer",
|
| 1035 |
+
"validate_timebraid_canonical_artifact",
|
| 1036 |
+
"validate_timebraid_model",
|
| 1037 |
+
"validate_timebraid_checkpoint_source",
|
| 1038 |
+
]
|
timebraid/model/mot/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public output type for native TimeBraid generation."""
|
| 2 |
+
|
| 3 |
+
from .model import TimeBraidGenerateOutput
|
| 4 |
+
|
| 5 |
+
__all__ = ["TimeBraidGenerateOutput"]
|
timebraid/model/mot/attention.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Mixed language and time-series attention for TimeBraid."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class _GlobalCausalJointSelfAttention(nn.Module):
|
| 12 |
+
"""Projection-free packed-attention kernel shared by native and residual paths."""
|
| 13 |
+
|
| 14 |
+
def __init__(
|
| 15 |
+
self,
|
| 16 |
+
q_heads: int,
|
| 17 |
+
q_head_dim: int,
|
| 18 |
+
*,
|
| 19 |
+
lang_hidden_size: Optional[int] = None,
|
| 20 |
+
):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.q_heads = q_heads
|
| 23 |
+
self.q_head_dim = q_head_dim
|
| 24 |
+
self.q_attention_hidden_size = self.q_heads * self.q_head_dim
|
| 25 |
+
# Residual fusion projects language, generation-TS and understanding-TS
|
| 26 |
+
# into one shared Qwen head geometry before packing, so its softmax scale
|
| 27 |
+
# is tied to `q_head_dim`. Native language attention keeps Qwen's
|
| 28 |
+
# `self_attn.scaling`; native TimesFM replay uses its own head dimension
|
| 29 |
+
# for the fixed attention-kernel scale.
|
| 30 |
+
self.scale = float(self.q_head_dim) ** -0.5
|
| 31 |
+
self.lang_hidden_size = int(lang_hidden_size or self.q_attention_hidden_size)
|
| 32 |
+
if self.lang_hidden_size <= 0:
|
| 33 |
+
raise ValueError(
|
| 34 |
+
f"`lang_hidden_size` must be positive, got {self.lang_hidden_size}."
|
| 35 |
+
)
|
| 36 |
+
self._flash_attn_varlen_func = None
|
| 37 |
+
self._fa_peft_integration_check = None
|
| 38 |
+
|
| 39 |
+
def _get_flash_attn_varlen_func(self):
|
| 40 |
+
if self._flash_attn_varlen_func is not None:
|
| 41 |
+
return self._flash_attn_varlen_func
|
| 42 |
+
try:
|
| 43 |
+
from flash_attn import flash_attn_varlen_func
|
| 44 |
+
except Exception as exc:
|
| 45 |
+
raise RuntimeError(
|
| 46 |
+
"MoT mixed attention is FA2-only but `flash_attn` could not be imported. "
|
| 47 |
+
"Install flash-attn in the active TimeBraid Python environment."
|
| 48 |
+
) from exc
|
| 49 |
+
self._flash_attn_varlen_func = flash_attn_varlen_func
|
| 50 |
+
return self._flash_attn_varlen_func
|
| 51 |
+
|
| 52 |
+
def _get_fa_peft_integration_check(self):
|
| 53 |
+
if self._fa_peft_integration_check is not None:
|
| 54 |
+
return self._fa_peft_integration_check
|
| 55 |
+
try:
|
| 56 |
+
from transformers.modeling_flash_attention_utils import (
|
| 57 |
+
fa_peft_integration_check,
|
| 58 |
+
)
|
| 59 |
+
except Exception as exc:
|
| 60 |
+
raise RuntimeError(
|
| 61 |
+
"Failed to import transformers.modeling_flash_attention_utils.fa_peft_integration_check. "
|
| 62 |
+
"This runtime patch follows HF FA2 dtype reconciliation logic and requires this symbol."
|
| 63 |
+
) from exc
|
| 64 |
+
self._fa_peft_integration_check = fa_peft_integration_check
|
| 65 |
+
return self._fa_peft_integration_check
|
| 66 |
+
|
| 67 |
+
def _infer_fa2_target_dtype_like_hf(self, q: torch.Tensor) -> Optional[torch.dtype]:
|
| 68 |
+
"""Mirror HF FA2 target-dtype inference for packed-attention calls."""
|
| 69 |
+
target_dtype: Optional[torch.dtype] = None
|
| 70 |
+
if q.dtype == torch.float32:
|
| 71 |
+
if torch.is_autocast_enabled():
|
| 72 |
+
target_dtype = torch.get_autocast_gpu_dtype()
|
| 73 |
+
elif hasattr(self, "config") and hasattr(
|
| 74 |
+
self.config, "_pre_quantization_dtype"
|
| 75 |
+
):
|
| 76 |
+
target_dtype = self.config._pre_quantization_dtype
|
| 77 |
+
else:
|
| 78 |
+
first_linear = next(
|
| 79 |
+
(layer for layer in self.modules() if isinstance(layer, nn.Linear)),
|
| 80 |
+
None,
|
| 81 |
+
)
|
| 82 |
+
if first_linear is None:
|
| 83 |
+
raise RuntimeError(
|
| 84 |
+
"Cannot infer FA2 target dtype because no nn.Linear module was found in bridge layer."
|
| 85 |
+
)
|
| 86 |
+
target_dtype = first_linear.weight.dtype
|
| 87 |
+
return target_dtype
|
| 88 |
+
|
| 89 |
+
def _reconcile_fa2_qkv(
|
| 90 |
+
self,
|
| 91 |
+
*,
|
| 92 |
+
q: torch.Tensor,
|
| 93 |
+
k: torch.Tensor,
|
| 94 |
+
v: torch.Tensor,
|
| 95 |
+
mode_label: str,
|
| 96 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 97 |
+
target_dtype = self._infer_fa2_target_dtype_like_hf(q)
|
| 98 |
+
fa_peft_integration_check = self._get_fa_peft_integration_check()
|
| 99 |
+
q_fa, k_fa, v_fa = fa_peft_integration_check(q, k, v, target_dtype)
|
| 100 |
+
if q_fa.dtype not in {torch.float16, torch.bfloat16}:
|
| 101 |
+
raise RuntimeError(
|
| 102 |
+
f"MoT mixed FA2 {mode_label} requires fp16/bf16 Q/K/V after HF-style dtype reconciliation, "
|
| 103 |
+
f"but got dtype={q_fa.dtype}, original_dtype={q.dtype}, "
|
| 104 |
+
f"target_dtype={target_dtype}, autocast_enabled={torch.is_autocast_enabled()}."
|
| 105 |
+
)
|
| 106 |
+
return q_fa, k_fa, v_fa
|
| 107 |
+
|
| 108 |
+
def _reconcile_fast_qkv(
|
| 109 |
+
self,
|
| 110 |
+
*,
|
| 111 |
+
q: torch.Tensor,
|
| 112 |
+
k: torch.Tensor,
|
| 113 |
+
v: torch.Tensor,
|
| 114 |
+
mode_label: str,
|
| 115 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 116 |
+
"""Normalize Q/K/V for the shared FA2 packed-attention kernel."""
|
| 117 |
+
return self._reconcile_fa2_qkv(q=q, k=k, v=v, mode_label=mode_label)
|
| 118 |
+
|
| 119 |
+
def _cu_seqlens(self, *, seqlens: list[int], device: torch.device) -> torch.Tensor:
|
| 120 |
+
cu = [0]
|
| 121 |
+
for seq_len in seqlens:
|
| 122 |
+
cu.append(cu[-1] + int(seq_len))
|
| 123 |
+
return torch.tensor(cu, device=device, dtype=torch.int32)
|
| 124 |
+
|
| 125 |
+
def _seqlens_from_cu_seqlens(self, *, cu_seqlens: torch.Tensor) -> list[int]:
|
| 126 |
+
if cu_seqlens.ndim != 1 or cu_seqlens.shape[0] == 0:
|
| 127 |
+
raise RuntimeError(
|
| 128 |
+
"Expected a non-empty 1D cu_seqlens tensor, got "
|
| 129 |
+
f"shape={tuple(cu_seqlens.shape)}."
|
| 130 |
+
)
|
| 131 |
+
cu_values = cu_seqlens.detach().to(device="cpu", dtype=torch.long).tolist()
|
| 132 |
+
return [
|
| 133 |
+
int(end) - int(start) for start, end in zip(cu_values[:-1], cu_values[1:])
|
| 134 |
+
]
|
| 135 |
+
|
| 136 |
+
def _run_varlen_flash_attention(
|
| 137 |
+
self,
|
| 138 |
+
*,
|
| 139 |
+
q_sorted: torch.Tensor,
|
| 140 |
+
k_sorted: torch.Tensor,
|
| 141 |
+
v_sorted: torch.Tensor,
|
| 142 |
+
q_positions_sorted: torch.Tensor,
|
| 143 |
+
k_positions_sorted: torch.Tensor,
|
| 144 |
+
softmax_scale: float,
|
| 145 |
+
dropout_p: float,
|
| 146 |
+
mode_label: str,
|
| 147 |
+
q_seqlens: Optional[list[int]] = None,
|
| 148 |
+
k_seqlens: Optional[list[int]] = None,
|
| 149 |
+
cu_seqlens_q: Optional[torch.Tensor] = None,
|
| 150 |
+
cu_seqlens_k: Optional[torch.Tensor] = None,
|
| 151 |
+
max_seqlen_q: Optional[int] = None,
|
| 152 |
+
max_seqlen_k: Optional[int] = None,
|
| 153 |
+
) -> torch.Tensor:
|
| 154 |
+
if q_sorted.ndim != 3 or k_sorted.ndim != 3 or v_sorted.ndim != 3:
|
| 155 |
+
raise RuntimeError(
|
| 156 |
+
"Fast MoT attention expects packed [N,H,D] Q/K/V tensors, got "
|
| 157 |
+
f"q={tuple(q_sorted.shape)}, k={tuple(k_sorted.shape)}, v={tuple(v_sorted.shape)}."
|
| 158 |
+
)
|
| 159 |
+
if k_sorted.shape != v_sorted.shape:
|
| 160 |
+
raise RuntimeError(
|
| 161 |
+
"Fast MoT attention expects aligned K/V shapes, got "
|
| 162 |
+
f"k={tuple(k_sorted.shape)}, v={tuple(v_sorted.shape)}."
|
| 163 |
+
)
|
| 164 |
+
if (
|
| 165 |
+
q_positions_sorted.ndim != 1
|
| 166 |
+
or q_positions_sorted.shape[0] != q_sorted.shape[0]
|
| 167 |
+
):
|
| 168 |
+
raise RuntimeError(
|
| 169 |
+
"Fast MoT attention expects 1D query positions aligned to Q, got "
|
| 170 |
+
f"positions={tuple(q_positions_sorted.shape)}, q_tokens={q_sorted.shape[0]}."
|
| 171 |
+
)
|
| 172 |
+
if (
|
| 173 |
+
k_positions_sorted.ndim != 1
|
| 174 |
+
or k_positions_sorted.shape[0] != k_sorted.shape[0]
|
| 175 |
+
):
|
| 176 |
+
raise RuntimeError(
|
| 177 |
+
"Fast MoT attention expects 1D key positions aligned to K, got "
|
| 178 |
+
f"positions={tuple(k_positions_sorted.shape)}, k_tokens={k_sorted.shape[0]}."
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
if q_seqlens is None:
|
| 182 |
+
if cu_seqlens_q is None:
|
| 183 |
+
raise RuntimeError(
|
| 184 |
+
"Fast MoT attention requires either q_seqlens or cu_seqlens_q."
|
| 185 |
+
)
|
| 186 |
+
q_seqlens = self._seqlens_from_cu_seqlens(cu_seqlens=cu_seqlens_q)
|
| 187 |
+
if k_seqlens is None:
|
| 188 |
+
if cu_seqlens_k is None:
|
| 189 |
+
raise RuntimeError(
|
| 190 |
+
"Fast MoT attention requires either k_seqlens or cu_seqlens_k."
|
| 191 |
+
)
|
| 192 |
+
k_seqlens = self._seqlens_from_cu_seqlens(cu_seqlens=cu_seqlens_k)
|
| 193 |
+
if max_seqlen_q is None:
|
| 194 |
+
max_seqlen_q = max(q_seqlens) if q_seqlens else 0
|
| 195 |
+
if max_seqlen_k is None:
|
| 196 |
+
max_seqlen_k = max(k_seqlens) if k_seqlens else 0
|
| 197 |
+
|
| 198 |
+
if q_sorted.device.type != "cuda":
|
| 199 |
+
raise RuntimeError(
|
| 200 |
+
f"MoT FA2 {mode_label} requires CUDA device, got {q_sorted.device.type}."
|
| 201 |
+
)
|
| 202 |
+
q_fast, k_fast, v_fast = self._reconcile_fast_qkv(
|
| 203 |
+
q=q_sorted,
|
| 204 |
+
k=k_sorted,
|
| 205 |
+
v=v_sorted,
|
| 206 |
+
mode_label=mode_label,
|
| 207 |
+
)
|
| 208 |
+
if cu_seqlens_q is None:
|
| 209 |
+
cu_seqlens_q = self._cu_seqlens(seqlens=q_seqlens, device=q_sorted.device)
|
| 210 |
+
if cu_seqlens_k is None:
|
| 211 |
+
cu_seqlens_k = self._cu_seqlens(seqlens=k_seqlens, device=q_sorted.device)
|
| 212 |
+
return self._get_flash_attn_varlen_func()(
|
| 213 |
+
q_fast,
|
| 214 |
+
k_fast,
|
| 215 |
+
v_fast,
|
| 216 |
+
cu_seqlens_q,
|
| 217 |
+
cu_seqlens_k,
|
| 218 |
+
max_seqlen_q,
|
| 219 |
+
max_seqlen_k,
|
| 220 |
+
dropout_p=dropout_p,
|
| 221 |
+
softmax_scale=softmax_scale,
|
| 222 |
+
causal=True,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
def run_packed_self_attention(
|
| 226 |
+
self,
|
| 227 |
+
*,
|
| 228 |
+
q_sorted: torch.Tensor,
|
| 229 |
+
k_sorted: torch.Tensor,
|
| 230 |
+
v_sorted: torch.Tensor,
|
| 231 |
+
cu_seqlens: torch.Tensor,
|
| 232 |
+
max_seqlen: int,
|
| 233 |
+
rope_positions_sorted: torch.Tensor,
|
| 234 |
+
softmax_scale: float,
|
| 235 |
+
dropout_p: float,
|
| 236 |
+
mode_label: str,
|
| 237 |
+
) -> torch.Tensor:
|
| 238 |
+
"""Shared FA2 packed-attention entrypoint for native and residual callers."""
|
| 239 |
+
return self._run_varlen_flash_attention(
|
| 240 |
+
q_sorted=q_sorted,
|
| 241 |
+
k_sorted=k_sorted,
|
| 242 |
+
v_sorted=v_sorted,
|
| 243 |
+
q_positions_sorted=rope_positions_sorted,
|
| 244 |
+
k_positions_sorted=rope_positions_sorted,
|
| 245 |
+
softmax_scale=softmax_scale,
|
| 246 |
+
dropout_p=dropout_p,
|
| 247 |
+
mode_label=mode_label,
|
| 248 |
+
cu_seqlens_q=cu_seqlens,
|
| 249 |
+
cu_seqlens_k=cu_seqlens,
|
| 250 |
+
max_seqlen_q=max_seqlen,
|
| 251 |
+
max_seqlen_k=max_seqlen,
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
class ResidualStreamProjection(nn.Module):
|
| 256 |
+
"""Project one stream into and out of the shared residual-attention heads."""
|
| 257 |
+
|
| 258 |
+
def __init__(
|
| 259 |
+
self,
|
| 260 |
+
*,
|
| 261 |
+
input_size: int,
|
| 262 |
+
attention_heads: int,
|
| 263 |
+
head_dim: int,
|
| 264 |
+
) -> None:
|
| 265 |
+
super().__init__()
|
| 266 |
+
if input_size <= 0:
|
| 267 |
+
raise ValueError(f"input_size must be positive, got {input_size}.")
|
| 268 |
+
if attention_heads <= 0:
|
| 269 |
+
raise ValueError(
|
| 270 |
+
f"attention_heads must be positive, got {attention_heads}."
|
| 271 |
+
)
|
| 272 |
+
if head_dim <= 0:
|
| 273 |
+
raise ValueError(f"head_dim must be positive, got {head_dim}.")
|
| 274 |
+
|
| 275 |
+
self.input_size = int(input_size)
|
| 276 |
+
self.attention_heads = int(attention_heads)
|
| 277 |
+
self.head_dim = int(head_dim)
|
| 278 |
+
attention_size = self.attention_heads * self.head_dim
|
| 279 |
+
|
| 280 |
+
# Keep registration order identical across all streams. Q/K use the
|
| 281 |
+
# Qwen-style post-projection head norm; V remains the raw projection.
|
| 282 |
+
#
|
| 283 |
+
# The epsilon is fixed at 1e-6 on purpose rather than mirrored from the
|
| 284 |
+
# host LM's `rms_norm_eps`. These norms carry trained weights, so the
|
| 285 |
+
# value they were trained under is part of this module's topology, the
|
| 286 |
+
# same as its head dimension; reading it from whatever backbone happens
|
| 287 |
+
# to be loaded would change a trained module's numerics. It matches
|
| 288 |
+
# Qwen3's default, so the two agree in practice today. Persisting it in
|
| 289 |
+
# the MoT restore contract is the proper fix and is deferred to the
|
| 290 |
+
# change that can update the training tree and the published configs
|
| 291 |
+
# together.
|
| 292 |
+
self.input_norm = nn.RMSNorm(
|
| 293 |
+
self.input_size, eps=1.0e-6, elementwise_affine=True
|
| 294 |
+
)
|
| 295 |
+
self.q_proj = nn.Linear(self.input_size, attention_size, bias=False)
|
| 296 |
+
self.q_norm = nn.RMSNorm(self.head_dim, eps=1.0e-6, elementwise_affine=True)
|
| 297 |
+
self.k_proj = nn.Linear(self.input_size, attention_size, bias=False)
|
| 298 |
+
self.k_norm = nn.RMSNorm(self.head_dim, eps=1.0e-6, elementwise_affine=True)
|
| 299 |
+
self.v_proj = nn.Linear(self.input_size, attention_size, bias=False)
|
| 300 |
+
self.out_proj = nn.Linear(attention_size, self.input_size, bias=False)
|
| 301 |
+
nn.init.zeros_(self.out_proj.weight)
|
| 302 |
+
|
| 303 |
+
@staticmethod
|
| 304 |
+
def _apply_head_norm(states: torch.Tensor, norm: nn.Module) -> torch.Tensor:
|
| 305 |
+
normalized = norm(states.to(dtype=norm.weight.dtype))
|
| 306 |
+
return normalized.to(dtype=states.dtype)
|
| 307 |
+
|
| 308 |
+
def project_qkv(
|
| 309 |
+
self, hidden: torch.Tensor
|
| 310 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 311 |
+
if hidden.ndim != 2 or int(hidden.shape[-1]) != self.input_size:
|
| 312 |
+
raise RuntimeError(
|
| 313 |
+
"Residual stream projection expects [N,H], got "
|
| 314 |
+
f"shape={tuple(hidden.shape)}, expected_hidden={self.input_size}."
|
| 315 |
+
)
|
| 316 |
+
if int(hidden.shape[0]) == 0:
|
| 317 |
+
empty = hidden.new_empty((0, self.attention_heads, self.head_dim))
|
| 318 |
+
return empty, empty, empty
|
| 319 |
+
|
| 320 |
+
normalized = self.input_norm(hidden.to(dtype=self.input_norm.weight.dtype)).to(
|
| 321 |
+
dtype=self.q_proj.weight.dtype
|
| 322 |
+
)
|
| 323 |
+
token_count = int(normalized.shape[0])
|
| 324 |
+
head_shape = (token_count, self.attention_heads, self.head_dim)
|
| 325 |
+
q = self.q_proj(normalized).view(head_shape)
|
| 326 |
+
k = self.k_proj(normalized).view(head_shape)
|
| 327 |
+
v = self.v_proj(normalized).view(head_shape)
|
| 328 |
+
q = self._apply_head_norm(q, self.q_norm)
|
| 329 |
+
k = self._apply_head_norm(k, self.k_norm)
|
| 330 |
+
return q.contiguous(), k.contiguous(), v.contiguous()
|
| 331 |
+
|
| 332 |
+
def project_output(
|
| 333 |
+
self, out_heads: torch.Tensor, *, output_dtype: torch.dtype
|
| 334 |
+
) -> torch.Tensor:
|
| 335 |
+
expected_head_shape = (self.attention_heads, self.head_dim)
|
| 336 |
+
if out_heads.ndim != 3 or tuple(out_heads.shape[1:]) != expected_head_shape:
|
| 337 |
+
raise RuntimeError(
|
| 338 |
+
"Residual stream output expects [N,H,D], got "
|
| 339 |
+
f"shape={tuple(out_heads.shape)}, expected_heads={expected_head_shape}."
|
| 340 |
+
)
|
| 341 |
+
flat = out_heads.reshape(out_heads.shape[0], -1)
|
| 342 |
+
delta = self.out_proj(flat.to(dtype=self.out_proj.weight.dtype))
|
| 343 |
+
return delta.to(dtype=output_dtype)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
class GlobalResidualAttention(_GlobalCausalJointSelfAttention):
|
| 347 |
+
"""One joint residual-attention block over language and paired TS streams."""
|
| 348 |
+
|
| 349 |
+
def __init__(
|
| 350 |
+
self,
|
| 351 |
+
*,
|
| 352 |
+
language_hidden_size: int,
|
| 353 |
+
generation_hidden_size: Optional[int],
|
| 354 |
+
understanding_hidden_size: Optional[int],
|
| 355 |
+
attention_heads: int,
|
| 356 |
+
head_dim: int,
|
| 357 |
+
) -> None:
|
| 358 |
+
if generation_hidden_size is None and understanding_hidden_size is None:
|
| 359 |
+
raise ValueError(
|
| 360 |
+
"GlobalResidualAttention requires generation or understanding."
|
| 361 |
+
)
|
| 362 |
+
super().__init__(
|
| 363 |
+
q_heads=attention_heads,
|
| 364 |
+
q_head_dim=head_dim,
|
| 365 |
+
lang_hidden_size=language_hidden_size,
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
# Module registration order is part of the checkpoint schema.
|
| 369 |
+
self.language = ResidualStreamProjection(
|
| 370 |
+
input_size=language_hidden_size,
|
| 371 |
+
attention_heads=attention_heads,
|
| 372 |
+
head_dim=head_dim,
|
| 373 |
+
)
|
| 374 |
+
self.generation = (
|
| 375 |
+
ResidualStreamProjection(
|
| 376 |
+
input_size=generation_hidden_size,
|
| 377 |
+
attention_heads=attention_heads,
|
| 378 |
+
head_dim=head_dim,
|
| 379 |
+
)
|
| 380 |
+
if generation_hidden_size is not None
|
| 381 |
+
else None
|
| 382 |
+
)
|
| 383 |
+
self.understanding = (
|
| 384 |
+
ResidualStreamProjection(
|
| 385 |
+
input_size=understanding_hidden_size,
|
| 386 |
+
attention_heads=attention_heads,
|
| 387 |
+
head_dim=head_dim,
|
| 388 |
+
)
|
| 389 |
+
if understanding_hidden_size is not None
|
| 390 |
+
else None
|
| 391 |
+
)
|
| 392 |
+
self.residual_attention_mode = True
|
| 393 |
+
|
| 394 |
+
@staticmethod
|
| 395 |
+
def _require_stream(
|
| 396 |
+
stream: Optional[ResidualStreamProjection], *, name: str
|
| 397 |
+
) -> ResidualStreamProjection:
|
| 398 |
+
if stream is None:
|
| 399 |
+
raise RuntimeError(
|
| 400 |
+
f"GlobalResidualAttention has no {name} stream on this layer."
|
| 401 |
+
)
|
| 402 |
+
return stream
|
| 403 |
+
|
| 404 |
+
def project_language_qkv(
|
| 405 |
+
self, hidden: torch.Tensor
|
| 406 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 407 |
+
return self.language.project_qkv(hidden)
|
| 408 |
+
|
| 409 |
+
def project_generation_qkv(
|
| 410 |
+
self, hidden: torch.Tensor
|
| 411 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 412 |
+
return self._require_stream(self.generation, name="generation").project_qkv(
|
| 413 |
+
hidden
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
def project_understanding_qkv(
|
| 417 |
+
self, hidden: torch.Tensor
|
| 418 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 419 |
+
return self._require_stream(
|
| 420 |
+
self.understanding, name="understanding"
|
| 421 |
+
).project_qkv(hidden)
|
| 422 |
+
|
| 423 |
+
def project_language_delta(
|
| 424 |
+
self, out_heads: torch.Tensor, *, output_dtype: torch.dtype
|
| 425 |
+
) -> torch.Tensor:
|
| 426 |
+
return self.language.project_output(out_heads, output_dtype=output_dtype)
|
| 427 |
+
|
| 428 |
+
def project_generation_delta(
|
| 429 |
+
self, out_heads: torch.Tensor, *, output_dtype: torch.dtype
|
| 430 |
+
) -> torch.Tensor:
|
| 431 |
+
return self._require_stream(self.generation, name="generation").project_output(
|
| 432 |
+
out_heads, output_dtype=output_dtype
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
def project_understanding_delta(
|
| 436 |
+
self, out_heads: torch.Tensor, *, output_dtype: torch.dtype
|
| 437 |
+
) -> torch.Tensor:
|
| 438 |
+
return self._require_stream(
|
| 439 |
+
self.understanding, name="understanding"
|
| 440 |
+
).project_output(out_heads, output_dtype=output_dtype)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
__all__ = [
|
| 444 |
+
"GlobalResidualAttention",
|
| 445 |
+
"ResidualStreamProjection",
|
| 446 |
+
"_GlobalCausalJointSelfAttention",
|
| 447 |
+
]
|
timebraid/model/mot/bridge_runtime.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Initialize TimeBraid's registered TimesFM and global-attention components."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import copy
|
| 6 |
+
import math
|
| 7 |
+
from typing import List, Optional
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn as nn
|
| 11 |
+
|
| 12 |
+
from .attention import GlobalResidualAttention, _GlobalCausalJointSelfAttention
|
| 13 |
+
from .pairing import (
|
| 14 |
+
_build_pair_map,
|
| 15 |
+
_resolve_pairable_q_layer_indices,
|
| 16 |
+
)
|
| 17 |
+
from .runtime_state import MoTRuntimeOptions
|
| 18 |
+
from .structures import build_timebraid_layer_plan
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _build_upsampled_tsfm_layer_plan(source_depth: int, target_depth: int) -> List[int]:
|
| 22 |
+
"""Build a concrete source-layer copy plan for an aligned TS stack."""
|
| 23 |
+
if source_depth <= 0:
|
| 24 |
+
raise ValueError(f"source_depth must be positive, got {source_depth}")
|
| 25 |
+
if target_depth <= 0:
|
| 26 |
+
raise ValueError(f"target_depth must be positive, got {target_depth}")
|
| 27 |
+
if target_depth < source_depth:
|
| 28 |
+
raise ValueError(
|
| 29 |
+
"TS layer upsampling expects target_depth >= source_depth, got "
|
| 30 |
+
f"source_depth={source_depth}, target_depth={target_depth}"
|
| 31 |
+
)
|
| 32 |
+
if target_depth == source_depth:
|
| 33 |
+
return list(range(source_depth))
|
| 34 |
+
if source_depth == 1:
|
| 35 |
+
return [0] * target_depth
|
| 36 |
+
|
| 37 |
+
source_last = source_depth - 1
|
| 38 |
+
target_last = target_depth - 1
|
| 39 |
+
plan: List[int] = []
|
| 40 |
+
for runtime_idx in range(target_depth):
|
| 41 |
+
source_idx = round(runtime_idx * source_last / target_last)
|
| 42 |
+
plan.append(int(source_idx))
|
| 43 |
+
return plan
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _build_dsfp_tsfm_layer_plan(
|
| 47 |
+
source_depth: int, target_depth: int
|
| 48 |
+
) -> tuple[List[int], List[int]]:
|
| 49 |
+
"""Return the copied source layer and original-layer positions for DSFP."""
|
| 50 |
+
if source_depth <= 0:
|
| 51 |
+
raise ValueError(f"source_depth must be positive, got {source_depth}")
|
| 52 |
+
if target_depth < source_depth:
|
| 53 |
+
raise ValueError(
|
| 54 |
+
"DSFP expects target_depth >= source_depth, got "
|
| 55 |
+
f"source_depth={source_depth}, target_depth={target_depth}."
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
source_pair_map = _build_pair_map(
|
| 59 |
+
num_q_layers=target_depth,
|
| 60 |
+
num_t_layers=source_depth,
|
| 61 |
+
pairing_mode="interleaved",
|
| 62 |
+
)
|
| 63 |
+
source_positions = sorted(source_pair_map)
|
| 64 |
+
copy_plan = _build_upsampled_tsfm_layer_plan(
|
| 65 |
+
source_depth=source_depth, target_depth=target_depth
|
| 66 |
+
)
|
| 67 |
+
for runtime_idx, source_idx in source_pair_map.items():
|
| 68 |
+
if copy_plan[runtime_idx] != source_idx:
|
| 69 |
+
raise RuntimeError(
|
| 70 |
+
"DSFP source placement disagrees with the deterministic copy plan: "
|
| 71 |
+
f"runtime_idx={runtime_idx}, source_idx={source_idx}, "
|
| 72 |
+
f"copy_source_idx={copy_plan[runtime_idx]}."
|
| 73 |
+
)
|
| 74 |
+
return copy_plan, source_positions
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _build_dsfp_tsfm_layers(
|
| 78 |
+
source_layers: List[nn.Module],
|
| 79 |
+
*,
|
| 80 |
+
target_depth: int,
|
| 81 |
+
) -> nn.ModuleList:
|
| 82 |
+
"""Keep source blocks once and fill every gap with a direct full-block copy."""
|
| 83 |
+
copy_plan, source_positions = _build_dsfp_tsfm_layer_plan(
|
| 84 |
+
source_depth=len(source_layers),
|
| 85 |
+
target_depth=target_depth,
|
| 86 |
+
)
|
| 87 |
+
source_position_set = set(source_positions)
|
| 88 |
+
layers: List[nn.Module] = []
|
| 89 |
+
for runtime_idx, source_idx in enumerate(copy_plan):
|
| 90 |
+
if runtime_idx in source_position_set:
|
| 91 |
+
layer = source_layers[source_idx]
|
| 92 |
+
else:
|
| 93 |
+
layer = copy.deepcopy(source_layers[source_idx])
|
| 94 |
+
layers.append(layer)
|
| 95 |
+
|
| 96 |
+
runtime_layers = nn.ModuleList(layers)
|
| 97 |
+
for param in runtime_layers.parameters():
|
| 98 |
+
param.requires_grad = False
|
| 99 |
+
return runtime_layers
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def initialize_timebraid_components(
|
| 103 |
+
owner: nn.Module,
|
| 104 |
+
llm_config,
|
| 105 |
+
runtime_options=None,
|
| 106 |
+
) -> None:
|
| 107 |
+
"""Register TimeBraid's canonical components directly on its model owner."""
|
| 108 |
+
if not isinstance(owner, nn.Module):
|
| 109 |
+
raise TypeError(
|
| 110 |
+
f"owner must be an initialized nn.Module, got {type(owner).__name__}."
|
| 111 |
+
)
|
| 112 |
+
if not hasattr(owner, "_modules"):
|
| 113 |
+
raise RuntimeError(
|
| 114 |
+
"owner nn.Module.__init__ must run before component initialization."
|
| 115 |
+
)
|
| 116 |
+
self = owner
|
| 117 |
+
config = llm_config
|
| 118 |
+
options = MoTRuntimeOptions(runtime_options)
|
| 119 |
+
# Host (Qwen) architecture info.
|
| 120 |
+
self.hidden_size = int(config.hidden_size)
|
| 121 |
+
self.num_heads = int(config.num_attention_heads)
|
| 122 |
+
self.num_q_layers = int(config.num_hidden_layers)
|
| 123 |
+
# Some Qwen configs expose only a subset of pairable full-attention layers.
|
| 124 |
+
self.q_pairable_layer_indices = _resolve_pairable_q_layer_indices(
|
| 125 |
+
config, self.num_q_layers
|
| 126 |
+
)
|
| 127 |
+
self.q_head_dim = int(
|
| 128 |
+
getattr(config, "head_dim", self.hidden_size // self.num_heads)
|
| 129 |
+
)
|
| 130 |
+
self.num_kv_heads = int(getattr(config, "num_key_value_heads", self.num_heads))
|
| 131 |
+
if self.num_heads % self.num_kv_heads != 0:
|
| 132 |
+
raise ValueError(
|
| 133 |
+
f"Qwen heads must be divisible by kv heads, got {self.num_heads}/{self.num_kv_heads}"
|
| 134 |
+
)
|
| 135 |
+
self.num_kv_groups = self.num_heads // self.num_kv_heads
|
| 136 |
+
|
| 137 |
+
self.pairing_mode = options.value("mot_pairing_mode").strip().lower()
|
| 138 |
+
if self.pairing_mode not in {"interleaved", "dsfp"}:
|
| 139 |
+
raise ValueError(
|
| 140 |
+
"`mot_pairing_mode` must be 'interleaved' or 'dsfp', got "
|
| 141 |
+
f"{self.pairing_mode!r}."
|
| 142 |
+
)
|
| 143 |
+
self.source_num_t_layers = options.value("mot_t_layers")
|
| 144 |
+
self.ts_gradient_checkpointing = options.value("mot_ts_gradient_checkpointing")
|
| 145 |
+
self.ts_gradient_checkpointing_use_reentrant = options.value(
|
| 146 |
+
"mot_ts_gradient_checkpointing_use_reentrant"
|
| 147 |
+
)
|
| 148 |
+
self.patch_size = options.value("mot_patch_size")
|
| 149 |
+
if self.patch_size <= 0:
|
| 150 |
+
raise ValueError(f"`mot_patch_size` must be positive, got {self.patch_size}.")
|
| 151 |
+
self.dsfp = self.pairing_mode == "dsfp"
|
| 152 |
+
# Optional understanding separation keeps the existing generation TS
|
| 153 |
+
# path intact, but adds a second TS tower for user/context-side spans.
|
| 154 |
+
# The depth selects the source TimesFM prefix. Both towers use the same
|
| 155 |
+
# pairing mode so one topology field owns every Qwen/TimesFM schedule.
|
| 156 |
+
self.understanding_pair_depth = options.value("mot_understanding_pair_depth")
|
| 157 |
+
if self.understanding_pair_depth < 0:
|
| 158 |
+
raise ValueError(
|
| 159 |
+
"`mot_understanding_pair_depth` must be >= 0, got "
|
| 160 |
+
f"{self.understanding_pair_depth}"
|
| 161 |
+
)
|
| 162 |
+
self.understanding_separation = self.understanding_pair_depth > 0
|
| 163 |
+
if self.understanding_separation:
|
| 164 |
+
if self.understanding_pair_depth > self.source_num_t_layers:
|
| 165 |
+
raise ValueError(
|
| 166 |
+
"`mot_understanding_pair_depth` cannot exceed `mot_t_layers`, got "
|
| 167 |
+
f"understanding_pair_depth={self.understanding_pair_depth}, "
|
| 168 |
+
f"source_t_layers={self.source_num_t_layers}"
|
| 169 |
+
)
|
| 170 |
+
if self.understanding_pair_depth > self.num_q_layers:
|
| 171 |
+
raise ValueError(
|
| 172 |
+
"Understanding TS pairing expects the Qwen depth to be at least the selected "
|
| 173 |
+
"source prefix depth, got "
|
| 174 |
+
f"understanding_pair_depth={self.understanding_pair_depth}, q_layers={self.num_q_layers}"
|
| 175 |
+
)
|
| 176 |
+
self.num_t_layers = int(self.source_num_t_layers)
|
| 177 |
+
if self.dsfp:
|
| 178 |
+
if len(self.q_pairable_layer_indices) != self.num_q_layers:
|
| 179 |
+
raise ValueError(
|
| 180 |
+
"`mot_pairing_mode=dsfp` requires every Qwen layer to be pairable, but "
|
| 181 |
+
"some layers are excluded by `config.layer_types`: "
|
| 182 |
+
f"pairable={len(self.q_pairable_layer_indices)}, total={self.num_q_layers}."
|
| 183 |
+
)
|
| 184 |
+
if self.source_num_t_layers != 20:
|
| 185 |
+
raise ValueError(
|
| 186 |
+
"`mot_pairing_mode=dsfp` requires the standard 20-layer TimesFM source stack, got "
|
| 187 |
+
f"mot_t_layers={self.source_num_t_layers}."
|
| 188 |
+
)
|
| 189 |
+
if self.source_num_t_layers > self.num_q_layers:
|
| 190 |
+
raise ValueError(
|
| 191 |
+
"`mot_pairing_mode=dsfp` expects the source TimesFM stack to fit inside Qwen depth, got "
|
| 192 |
+
f"source_t_layers={self.source_num_t_layers}, q_layers={self.num_q_layers}."
|
| 193 |
+
)
|
| 194 |
+
self.num_t_layers = int(self.num_q_layers)
|
| 195 |
+
self.num_understanding_t_layers = (
|
| 196 |
+
self.num_q_layers
|
| 197 |
+
if self.understanding_separation and self.dsfp
|
| 198 |
+
else self.understanding_pair_depth
|
| 199 |
+
)
|
| 200 |
+
# Packed rows can contain many short TS spans from multiple original samples.
|
| 201 |
+
self.max_spans_per_sample = options.value("mot_max_spans_per_sample")
|
| 202 |
+
if self.max_spans_per_sample <= 0:
|
| 203 |
+
raise ValueError(
|
| 204 |
+
f"max_spans_per_sample must be positive, got {self.max_spans_per_sample}."
|
| 205 |
+
)
|
| 206 |
+
# TimesFM structural defaults (2.5-200M).
|
| 207 |
+
self.tsfm_num_heads = options.value("mot_tsfm_num_heads")
|
| 208 |
+
self.tsfm_head_dim = options.value("mot_tsfm_head_dim")
|
| 209 |
+
self.tsfm_hidden_size = options.value("mot_tsfm_hidden_size")
|
| 210 |
+
if self.tsfm_hidden_size != self.tsfm_num_heads * self.tsfm_head_dim:
|
| 211 |
+
raise ValueError(
|
| 212 |
+
"TimesFM hidden/head mismatch: "
|
| 213 |
+
f"hidden={self.tsfm_hidden_size}, heads={self.tsfm_num_heads}, head_dim={self.tsfm_head_dim}"
|
| 214 |
+
)
|
| 215 |
+
self.understanding_tsfm_num_heads = self.tsfm_num_heads
|
| 216 |
+
self.understanding_tsfm_head_dim = self.tsfm_head_dim
|
| 217 |
+
self.understanding_tsfm_hidden_size = self.tsfm_hidden_size
|
| 218 |
+
# Mixed/Qwen positions are configurable at the span boundary.
|
| 219 |
+
# `patch_slot` reserves one mixed slot per realized TS patch;
|
| 220 |
+
# `span_slot` is the historical coarse lattice where one TS span
|
| 221 |
+
# advances later text by one slot and all patches share anchor+1.
|
| 222 |
+
# Native TS rotary state resets to local 0..N-1 inside each span either
|
| 223 |
+
# way. The restore contract always supplies this key, so the default
|
| 224 |
+
# only covers hand-built option maps and must match the trained lattice.
|
| 225 |
+
self.mixed_position_mode = options.value("mot_mixed_position_mode").strip().lower()
|
| 226 |
+
if self.mixed_position_mode not in {"span_slot", "patch_slot"}:
|
| 227 |
+
raise ValueError(
|
| 228 |
+
"`mot_mixed_position_mode` must be one of {'span_slot', 'patch_slot'}, got "
|
| 229 |
+
f"{self.mixed_position_mode!r}."
|
| 230 |
+
)
|
| 231 |
+
self.pair_map = _build_pair_map(
|
| 232 |
+
num_q_layers=self.num_q_layers,
|
| 233 |
+
num_t_layers=self.num_t_layers,
|
| 234 |
+
pairing_mode=self.pairing_mode,
|
| 235 |
+
q_layer_indices=self.q_pairable_layer_indices,
|
| 236 |
+
)
|
| 237 |
+
if self.understanding_separation:
|
| 238 |
+
self.understanding_pair_map = _build_pair_map(
|
| 239 |
+
num_q_layers=self.num_q_layers,
|
| 240 |
+
num_t_layers=self.num_understanding_t_layers,
|
| 241 |
+
pairing_mode=self.pairing_mode,
|
| 242 |
+
q_layer_indices=self.q_pairable_layer_indices,
|
| 243 |
+
)
|
| 244 |
+
else:
|
| 245 |
+
self.understanding_pair_map = {}
|
| 246 |
+
self.layer_plan = build_timebraid_layer_plan(
|
| 247 |
+
num_llm_layers=self.num_q_layers,
|
| 248 |
+
generation_tsfm_by_llm_layer=self.pair_map,
|
| 249 |
+
understanding_tsfm_by_llm_layer=self.understanding_pair_map,
|
| 250 |
+
)
|
| 251 |
+
# The delimiter ABI is persisted in config; no tokenizer object is
|
| 252 |
+
# required by the numeric runtime.
|
| 253 |
+
self._timesfm_util = None
|
| 254 |
+
self.ts_open_token_id = options.value("mot_ts_open_token_id")
|
| 255 |
+
self.ts_close_token_id = options.value("mot_ts_close_token_id")
|
| 256 |
+
if self.ts_open_token_id < 0 or self.ts_close_token_id < 0:
|
| 257 |
+
raise ValueError("TimeBraid runtime requires persisted TS delimiter token IDs.")
|
| 258 |
+
if self.ts_open_token_id == self.ts_close_token_id:
|
| 259 |
+
raise ValueError(
|
| 260 |
+
"TimeBraid runtime TS delimiter token IDs must be distinct, got "
|
| 261 |
+
f"{self.ts_open_token_id}."
|
| 262 |
+
)
|
| 263 |
+
# Complete TimeBraid checkpoints restore the registered TimesFM towers through
|
| 264 |
+
# HF loading; TimeBraid inference never resolves a second expert checkpoint.
|
| 265 |
+
self._timesfm_target_dtype = options.value("mot_compute_dtype")
|
| 266 |
+
self.ts_loss_weight = options.value("mot_ts_loss_weight")
|
| 267 |
+
if self.ts_loss_weight < 0:
|
| 268 |
+
raise ValueError(f"`mot_ts_loss_weight` must be >=0, got {self.ts_loss_weight}")
|
| 269 |
+
self.lm_loss_weight = options.value("mot_lm_loss_weight")
|
| 270 |
+
if self.lm_loss_weight < 0:
|
| 271 |
+
raise ValueError(f"`mot_lm_loss_weight` must be >=0, got {self.lm_loss_weight}")
|
| 272 |
+
self.ts_roi_mse_alpha = options.value("mot_ts_roi_mse_alpha")
|
| 273 |
+
if self.ts_roi_mse_alpha < 0:
|
| 274 |
+
raise ValueError(
|
| 275 |
+
f"`mot_ts_roi_mse_alpha` must be >= 0, got {self.ts_roi_mse_alpha}"
|
| 276 |
+
)
|
| 277 |
+
self.ts_understanding_loss_weight = options.value(
|
| 278 |
+
"mot_ts_understanding_loss_weight"
|
| 279 |
+
)
|
| 280 |
+
self.has_understanding_head = options.value("has_understanding_head")
|
| 281 |
+
if (
|
| 282 |
+
not math.isfinite(self.ts_understanding_loss_weight)
|
| 283 |
+
or self.ts_understanding_loss_weight < 0
|
| 284 |
+
):
|
| 285 |
+
raise ValueError(
|
| 286 |
+
"`mot_ts_understanding_loss_weight` must be null or a finite value >= 0, got "
|
| 287 |
+
f"{self.ts_understanding_loss_weight}."
|
| 288 |
+
)
|
| 289 |
+
if self.has_understanding_head and not self.understanding_separation:
|
| 290 |
+
raise ValueError(
|
| 291 |
+
"has_understanding_head requires an understanding TimesFM tower."
|
| 292 |
+
)
|
| 293 |
+
if self.ts_understanding_loss_weight > 0.0 and not self.has_understanding_head:
|
| 294 |
+
raise ValueError(
|
| 295 |
+
"ts_understanding_loss_weight > 0 requires has_understanding_head=true."
|
| 296 |
+
)
|
| 297 |
+
self.tsfm_output_patch_len = 0
|
| 298 |
+
self.tsfm_q_channels = 0
|
| 299 |
+
self.tsfm_decode_index = 0
|
| 300 |
+
self.tsfm_quantile_taus: List[float] = []
|
| 301 |
+
self.generation_tsfm: Optional[nn.Module] = None
|
| 302 |
+
self.understanding_tsfm: Optional[nn.Module] = None
|
| 303 |
+
_init_timesfm_eager(self)
|
| 304 |
+
if self.generation_tsfm is not None:
|
| 305 |
+
native_patch_size = int(self.generation_tsfm.p)
|
| 306 |
+
if native_patch_size <= 0:
|
| 307 |
+
raise RuntimeError(
|
| 308 |
+
f"TimesFM must expose a positive input patch size, got p={native_patch_size}."
|
| 309 |
+
)
|
| 310 |
+
if self.patch_size != native_patch_size:
|
| 311 |
+
raise RuntimeError(
|
| 312 |
+
"`mot_patch_size` must match the loaded TimesFM input patch size, got "
|
| 313 |
+
f"mot_patch_size={self.patch_size}, timesfm.p={native_patch_size}."
|
| 314 |
+
)
|
| 315 |
+
if self.understanding_tsfm is not None:
|
| 316 |
+
understanding_patch_size = int(self.understanding_tsfm.p)
|
| 317 |
+
if understanding_patch_size != native_patch_size:
|
| 318 |
+
raise RuntimeError(
|
| 319 |
+
"Generation and understanding TimesFM input patch sizes must match, got "
|
| 320 |
+
f"generation={native_patch_size}, understanding={understanding_patch_size}."
|
| 321 |
+
)
|
| 322 |
+
self.global_residual_attention = nn.ModuleDict(
|
| 323 |
+
{
|
| 324 |
+
str(step.llm_layer): GlobalResidualAttention(
|
| 325 |
+
language_hidden_size=self.hidden_size,
|
| 326 |
+
generation_hidden_size=(
|
| 327 |
+
self.tsfm_hidden_size
|
| 328 |
+
if step.generation_tsfm_layer is not None
|
| 329 |
+
else None
|
| 330 |
+
),
|
| 331 |
+
understanding_hidden_size=(
|
| 332 |
+
self.understanding_tsfm_hidden_size
|
| 333 |
+
if step.understanding_tsfm_layer is not None
|
| 334 |
+
else None
|
| 335 |
+
),
|
| 336 |
+
attention_heads=self.num_heads,
|
| 337 |
+
head_dim=self.q_head_dim,
|
| 338 |
+
)
|
| 339 |
+
for step in self.layer_plan
|
| 340 |
+
if step.generation_tsfm_layer is not None
|
| 341 |
+
or step.understanding_tsfm_layer is not None
|
| 342 |
+
}
|
| 343 |
+
)
|
| 344 |
+
self.packed_attention = _GlobalCausalJointSelfAttention(
|
| 345 |
+
q_heads=self.num_heads,
|
| 346 |
+
q_head_dim=self.q_head_dim,
|
| 347 |
+
lang_hidden_size=self.hidden_size,
|
| 348 |
+
)
|
| 349 |
+
self.understanding_head: Optional[nn.Module] = None
|
| 350 |
+
if self.has_understanding_head:
|
| 351 |
+
if self.generation_tsfm is None:
|
| 352 |
+
raise RuntimeError(
|
| 353 |
+
"TS understanding reconstruction head requires initialized TimesFM."
|
| 354 |
+
)
|
| 355 |
+
# `has_understanding_head` persists this fixed-shape topology in the HF
|
| 356 |
+
# config. The TSFM-hidden MLP reconstructs one native TimesFM input patch;
|
| 357 |
+
# `ts_understanding_loss_weight` separately weights its fixed 2:8
|
| 358 |
+
# time-domain/FFT objective in the routed-loss helper.
|
| 359 |
+
self.understanding_head = nn.Sequential(
|
| 360 |
+
nn.Linear(int(self.tsfm_hidden_size), int(self.tsfm_hidden_size)),
|
| 361 |
+
nn.GELU(),
|
| 362 |
+
nn.Linear(int(self.tsfm_hidden_size), native_patch_size),
|
| 363 |
+
)
|
| 364 |
+
if not any(param.is_meta for param in self.understanding_head.parameters()):
|
| 365 |
+
self.understanding_head.to(
|
| 366 |
+
device=torch.device("cpu"), dtype=self._timesfm_target_dtype
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def _build_timesfm2p5_model():
|
| 371 |
+
from ..._vendor.timesfm.timesfm_2p5.timesfm_2p5_torch import (
|
| 372 |
+
TimesFM_2p5_200M_torch_module,
|
| 373 |
+
)
|
| 374 |
+
from ..._vendor.timesfm.torch import util as timesfm_torch_util
|
| 375 |
+
|
| 376 |
+
# TimeBraid checkpoints already contain this module's complete state. Construct
|
| 377 |
+
# the registered tower directly; the removed upstream Hub wrapper would
|
| 378 |
+
# allocate a second class-level 200M model and expose an unrelated download
|
| 379 |
+
# path before Transformers restores the embedded weights.
|
| 380 |
+
timesfm_model = TimesFM_2p5_200M_torch_module()
|
| 381 |
+
|
| 382 |
+
for param in timesfm_model.parameters():
|
| 383 |
+
param.requires_grad = False
|
| 384 |
+
timesfm_model.eval()
|
| 385 |
+
if not any(param.is_meta for param in timesfm_model.parameters()):
|
| 386 |
+
timesfm_model.to("cpu")
|
| 387 |
+
return timesfm_model, timesfm_torch_util
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
def _init_timesfm_eager(self) -> None:
|
| 391 |
+
# Build TimesFM immediately so the complete registered component surface exists.
|
| 392 |
+
if self.generation_tsfm is not None:
|
| 393 |
+
return
|
| 394 |
+
|
| 395 |
+
self.generation_tsfm, self._timesfm_util = _build_timesfm2p5_model()
|
| 396 |
+
|
| 397 |
+
# Validate expected source structure before optional TS-depth upsampling
|
| 398 |
+
# replaces the registered TimesFM layer stack.
|
| 399 |
+
if len(self.generation_tsfm.stacked_xf) < self.source_num_t_layers:
|
| 400 |
+
raise RuntimeError(
|
| 401 |
+
"TimesFM has fewer source layers than requested by the active MoT runtime: "
|
| 402 |
+
f"got {len(self.generation_tsfm.stacked_xf)}, required {self.source_num_t_layers}"
|
| 403 |
+
)
|
| 404 |
+
source_layers = list(self.generation_tsfm.stacked_xf[: self.source_num_t_layers])
|
| 405 |
+
if self.understanding_separation:
|
| 406 |
+
# Deep-copy the complete expert first, then take the understanding
|
| 407 |
+
# prefix from that independent object graph. DSFP deliberately keeps
|
| 408 |
+
# its source-position modules rather than copying them, so passing
|
| 409 |
+
# generation modules into the expansion helper would otherwise make
|
| 410 |
+
# the two registered towers share parameters and tensor storage.
|
| 411 |
+
understanding_model = copy.deepcopy(self.generation_tsfm)
|
| 412 |
+
understanding_source_layers = list(
|
| 413 |
+
understanding_model.stacked_xf[: self.understanding_pair_depth]
|
| 414 |
+
)
|
| 415 |
+
if self.dsfp:
|
| 416 |
+
understanding_layers = _build_dsfp_tsfm_layers(
|
| 417 |
+
understanding_source_layers,
|
| 418 |
+
target_depth=self.num_q_layers,
|
| 419 |
+
)
|
| 420 |
+
else:
|
| 421 |
+
understanding_layers = nn.ModuleList(understanding_source_layers)
|
| 422 |
+
for param in understanding_model.parameters():
|
| 423 |
+
param.requires_grad = False
|
| 424 |
+
for param in understanding_layers.parameters():
|
| 425 |
+
param.requires_grad = False
|
| 426 |
+
understanding_layers.eval()
|
| 427 |
+
if not any(param.is_meta for param in understanding_layers.parameters()):
|
| 428 |
+
understanding_layers.to(
|
| 429 |
+
device=torch.device("cpu"), dtype=self._timesfm_target_dtype
|
| 430 |
+
)
|
| 431 |
+
understanding_model.stacked_xf = understanding_layers
|
| 432 |
+
understanding_model.x = self.num_understanding_t_layers
|
| 433 |
+
understanding_model.eval()
|
| 434 |
+
if not any(param.is_meta for param in understanding_model.parameters()):
|
| 435 |
+
understanding_model.to("cpu")
|
| 436 |
+
self.understanding_tsfm = understanding_model
|
| 437 |
+
if self.dsfp:
|
| 438 |
+
dsfp_layers = _build_dsfp_tsfm_layers(
|
| 439 |
+
source_layers,
|
| 440 |
+
target_depth=self.num_q_layers,
|
| 441 |
+
)
|
| 442 |
+
dsfp_layers.eval()
|
| 443 |
+
if not any(param.is_meta for param in dsfp_layers.parameters()):
|
| 444 |
+
dsfp_layers.to(device=torch.device("cpu"), dtype=self._timesfm_target_dtype)
|
| 445 |
+
self.generation_tsfm.stacked_xf = dsfp_layers
|
| 446 |
+
self.generation_tsfm.x = self.num_t_layers
|
| 447 |
+
|
| 448 |
+
self.tsfm_output_patch_len = int(self.generation_tsfm.o)
|
| 449 |
+
self.tsfm_q_channels = int(self.generation_tsfm.q)
|
| 450 |
+
self.tsfm_decode_index = int(self.generation_tsfm.aridx)
|
| 451 |
+
quantiles = list(self.generation_tsfm.config.quantiles)
|
| 452 |
+
if len(quantiles) + 1 != self.tsfm_q_channels:
|
| 453 |
+
raise RuntimeError(
|
| 454 |
+
f"TimesFM quantile channel mismatch: q_channels={self.tsfm_q_channels}, quantiles={len(quantiles)}"
|
| 455 |
+
)
|
| 456 |
+
if not (0 <= self.tsfm_decode_index < self.tsfm_q_channels):
|
| 457 |
+
raise RuntimeError(
|
| 458 |
+
f"TimesFM decode index out of range: decode_idx={self.tsfm_decode_index}, q={self.tsfm_q_channels}"
|
| 459 |
+
)
|
| 460 |
+
# Build channel->tau mapping, reserving decode channel for point forecast.
|
| 461 |
+
quantile_iter = iter(quantiles)
|
| 462 |
+
channel_taus: List[float] = []
|
| 463 |
+
for channel_idx in range(self.tsfm_q_channels):
|
| 464 |
+
if channel_idx == self.tsfm_decode_index:
|
| 465 |
+
channel_taus.append(-1.0)
|
| 466 |
+
else:
|
| 467 |
+
channel_taus.append(float(next(quantile_iter)))
|
| 468 |
+
self.tsfm_quantile_taus = channel_taus
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
__all__ = ["initialize_timebraid_components"]
|
timebraid/model/mot/bridge_runtime_forecast.py
ADDED
|
@@ -0,0 +1,1317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Forecast materialization and loss helpers for TimeBraid."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from .structures import (
|
| 11 |
+
ROLE_TARGET,
|
| 12 |
+
TS_ROUTE_GENERATION,
|
| 13 |
+
TS_ROUTE_UNDERSTANDING,
|
| 14 |
+
build_mot_forecast_loss_accounting,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
if TYPE_CHECKING:
|
| 18 |
+
from .bridge_runtime_mot import _MoTForecastTarget, _MoTRuntime
|
| 19 |
+
|
| 20 |
+
_FP32_SINH_LIMIT = math.asinh(torch.finfo(torch.float32).max)
|
| 21 |
+
_MOT_TS_GLOBAL_LOSS_DETACHED_CAP = 2.0
|
| 22 |
+
_MOT_TS_ROI_LOSS_DETACHED_CAP = 2.0
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class _FlatForecastLossParts(NamedTuple):
|
| 26 |
+
point: torch.Tensor
|
| 27 |
+
quantile: torch.Tensor
|
| 28 |
+
global_point: torch.Tensor
|
| 29 |
+
global_quantile: torch.Tensor
|
| 30 |
+
roi_point: torch.Tensor
|
| 31 |
+
roi_weighted_point: torch.Tensor
|
| 32 |
+
global_uncapped: torch.Tensor
|
| 33 |
+
global_cap_scale: torch.Tensor
|
| 34 |
+
roi_uncapped: torch.Tensor
|
| 35 |
+
roi_weighted_uncapped: torch.Tensor
|
| 36 |
+
roi_cap_scale: torch.Tensor
|
| 37 |
+
roi_supervised_points: torch.Tensor
|
| 38 |
+
roi_supervised_targets: torch.Tensor
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _finite_stats(tensor: torch.Tensor) -> str:
|
| 42 |
+
"""Return compact finite-value stats for hard forecast diagnostics."""
|
| 43 |
+
detached = tensor.detach()
|
| 44 |
+
finite = detached[torch.isfinite(detached)]
|
| 45 |
+
if finite.numel() == 0:
|
| 46 |
+
return f"shape={tuple(detached.shape)}, dtype={detached.dtype}, finite=0"
|
| 47 |
+
finite_f32 = finite.to(dtype=torch.float32)
|
| 48 |
+
return (
|
| 49 |
+
f"shape={tuple(detached.shape)}, dtype={detached.dtype}, "
|
| 50 |
+
f"finite={int(finite.numel())}/{int(detached.numel())}, "
|
| 51 |
+
f"min={float(finite_f32.min().item()):.6g}, "
|
| 52 |
+
f"max={float(finite_f32.max().item()):.6g}, "
|
| 53 |
+
f"absmax={float(finite_f32.abs().max().item()):.6g}"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _first_bad_index(mask: torch.Tensor) -> tuple[int, ...]:
|
| 58 |
+
bad = torch.nonzero(mask, as_tuple=False)
|
| 59 |
+
if int(bad.numel()) == 0:
|
| 60 |
+
return ()
|
| 61 |
+
return tuple(int(value) for value in bad[0].detach().cpu().tolist())
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _target_debug_record(target: Any) -> str:
|
| 65 |
+
return (
|
| 66 |
+
f"sample={int(target.sample_idx)}, slot={int(target.slot_idx)}, "
|
| 67 |
+
f"role={int(target.role_id)}, segment={int(target.segment_id)}, "
|
| 68 |
+
f"ts_range=[{int(target.ts_start)},{int(target.ts_end)}), "
|
| 69 |
+
f"loss_start={int(target.loss_start_idx)}, "
|
| 70 |
+
f"generation_start={int(target.generation_start_index)}"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _require_finite_tensor(
|
| 75 |
+
tensor: torch.Tensor, *, label: str, targets: List[Any]
|
| 76 |
+
) -> None:
|
| 77 |
+
if bool(torch.all(torch.isfinite(tensor))):
|
| 78 |
+
return
|
| 79 |
+
bad_index = _first_bad_index(torch.logical_not(torch.isfinite(tensor)))
|
| 80 |
+
target_text = ""
|
| 81 |
+
if bad_index:
|
| 82 |
+
target_axis = int(bad_index[0]) if len(bad_index) >= 1 else -1
|
| 83 |
+
if 0 <= target_axis < len(targets):
|
| 84 |
+
target_text = f", target=({_target_debug_record(targets[target_axis])})"
|
| 85 |
+
raise RuntimeError(
|
| 86 |
+
f"Forecast tensor became non-finite at {label}: "
|
| 87 |
+
f"bad_index={bad_index}, {_finite_stats(tensor)}{target_text}."
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _require_sinh_safe(
|
| 92 |
+
tensor: torch.Tensor,
|
| 93 |
+
*,
|
| 94 |
+
targets: List[Any],
|
| 95 |
+
backend_label: str,
|
| 96 |
+
target_axis: int = 0,
|
| 97 |
+
) -> None:
|
| 98 |
+
"""Raise loudly when normalized-space head outputs would overflow fp32 sinh."""
|
| 99 |
+
unsafe = tensor.detach().to(dtype=torch.float32).abs() > _FP32_SINH_LIMIT
|
| 100 |
+
if not bool(torch.any(unsafe)):
|
| 101 |
+
return
|
| 102 |
+
bad_index = _first_bad_index(unsafe)
|
| 103 |
+
target_text = ""
|
| 104 |
+
if bad_index and 0 <= int(target_axis) < len(bad_index):
|
| 105 |
+
bad_target = int(bad_index[int(target_axis)])
|
| 106 |
+
if 0 <= bad_target < len(targets):
|
| 107 |
+
target_text = f", target=({_target_debug_record(targets[bad_target])})"
|
| 108 |
+
raise RuntimeError(
|
| 109 |
+
f"{backend_label} head output would overflow fp32 sinh during forecast "
|
| 110 |
+
f"inverse transform: bad_index={bad_index}, "
|
| 111 |
+
f"limit={_FP32_SINH_LIMIT:.6g}, "
|
| 112 |
+
f"value={float(tensor[bad_index].detach().to(torch.float32).item()):.6g}, "
|
| 113 |
+
f"{_finite_stats(tensor)}{target_text}."
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _point_forecast_loss_values(
|
| 118 |
+
pred_point: torch.Tensor, target: torch.Tensor
|
| 119 |
+
) -> torch.Tensor:
|
| 120 |
+
"""Compute elementwise MSE on the TimesFM decode channel."""
|
| 121 |
+
pred_point = pred_point.to(dtype=torch.float32)
|
| 122 |
+
target = target.to(device=pred_point.device, dtype=torch.float32)
|
| 123 |
+
return (pred_point - target).square()
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _detached_per_item_loss_cap_scale(
|
| 127 |
+
losses: torch.Tensor, cap_value: float
|
| 128 |
+
) -> torch.Tensor:
|
| 129 |
+
"""Return detached per-item cap scales for already-normalized record losses."""
|
| 130 |
+
if losses.ndim != 1:
|
| 131 |
+
raise RuntimeError(
|
| 132 |
+
f"Per-item loss cap expects a rank-1 tensor, got {tuple(losses.shape)}."
|
| 133 |
+
)
|
| 134 |
+
losses = losses.to(dtype=torch.float32)
|
| 135 |
+
if float(cap_value) <= 0.0:
|
| 136 |
+
return torch.ones_like(losses)
|
| 137 |
+
return torch.clamp(
|
| 138 |
+
losses.new_tensor(float(cap_value)) / losses.detach().clamp_min(1e-12), max=1.0
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _apply_timesfm_value_transform(values: torch.Tensor) -> torch.Tensor:
|
| 143 |
+
"""Apply the fixed post-scale asinh transform before TimesFM consumes values."""
|
| 144 |
+
return torch.asinh(values)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _inverse_timesfm_value_transform(
|
| 148 |
+
values: torch.Tensor,
|
| 149 |
+
*,
|
| 150 |
+
targets: Optional[List[Any]] = None,
|
| 151 |
+
backend_label: str = "TS value transform",
|
| 152 |
+
target_axis: int = 0,
|
| 153 |
+
) -> torch.Tensor:
|
| 154 |
+
"""Invert the fixed asinh transform and fail before fp32 sinh overflow."""
|
| 155 |
+
_require_sinh_safe(
|
| 156 |
+
values,
|
| 157 |
+
targets=[] if targets is None else targets,
|
| 158 |
+
backend_label=backend_label,
|
| 159 |
+
target_axis=target_axis,
|
| 160 |
+
)
|
| 161 |
+
return values.to(dtype=torch.float32).sinh()
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _transform_forecast_targets_for_loss(
|
| 165 |
+
targets: torch.Tensor,
|
| 166 |
+
*,
|
| 167 |
+
owner_mu: torch.Tensor,
|
| 168 |
+
owner_sigma: torch.Tensor,
|
| 169 |
+
) -> torch.Tensor:
|
| 170 |
+
"""Move raw forecast targets into the same scaled value frame as tower predictions."""
|
| 171 |
+
scaled = (
|
| 172 |
+
targets.to(dtype=torch.float32) - owner_mu.unsqueeze(1)
|
| 173 |
+
) / owner_sigma.unsqueeze(1).clamp_min(1e-6)
|
| 174 |
+
return _apply_timesfm_value_transform(scaled)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _global_count_tensor(
|
| 178 |
+
global_count: Optional[object],
|
| 179 |
+
*,
|
| 180 |
+
reference: torch.Tensor,
|
| 181 |
+
label: str,
|
| 182 |
+
) -> Optional[torch.Tensor]:
|
| 183 |
+
if global_count is None:
|
| 184 |
+
return None
|
| 185 |
+
count = torch.as_tensor(
|
| 186 |
+
global_count, device=reference.device, dtype=reference.dtype
|
| 187 |
+
)
|
| 188 |
+
if int(count.numel()) != 1:
|
| 189 |
+
raise RuntimeError(
|
| 190 |
+
f"{label} denominator must be scalar, got shape={tuple(count.shape)}."
|
| 191 |
+
)
|
| 192 |
+
count = count.reshape(())
|
| 193 |
+
if bool(count <= 0):
|
| 194 |
+
raise RuntimeError(
|
| 195 |
+
f"{label} denominator must be positive, got {float(count.detach().cpu().item())}."
|
| 196 |
+
)
|
| 197 |
+
return count
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def _reduce_per_record_loss(
|
| 201 |
+
per_record: torch.Tensor,
|
| 202 |
+
*,
|
| 203 |
+
global_count: Optional[object],
|
| 204 |
+
label: str,
|
| 205 |
+
) -> torch.Tensor:
|
| 206 |
+
if per_record.ndim != 1:
|
| 207 |
+
raise RuntimeError(
|
| 208 |
+
f"{label} reduction expects a rank-1 per-record tensor, got {tuple(per_record.shape)}."
|
| 209 |
+
)
|
| 210 |
+
if int(per_record.numel()) == 0:
|
| 211 |
+
raise RuntimeError(f"{label} reduction requires at least one local record.")
|
| 212 |
+
per_record = per_record.to(dtype=torch.float32)
|
| 213 |
+
count = _global_count_tensor(global_count, reference=per_record, label=label)
|
| 214 |
+
if count is None:
|
| 215 |
+
return per_record.mean()
|
| 216 |
+
return per_record.sum() / count
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def _global_count_for_horizon(
|
| 220 |
+
counts_by_horizon: Optional[object], horizon: int
|
| 221 |
+
) -> Optional[object]:
|
| 222 |
+
if counts_by_horizon is None:
|
| 223 |
+
return None
|
| 224 |
+
if isinstance(counts_by_horizon, dict):
|
| 225 |
+
if int(horizon) not in counts_by_horizon:
|
| 226 |
+
raise RuntimeError(
|
| 227 |
+
"TS global denominator mapping is missing a forecast horizon: "
|
| 228 |
+
f"horizon={int(horizon)}, available={sorted(int(key) for key in counts_by_horizon.keys())}."
|
| 229 |
+
)
|
| 230 |
+
return counts_by_horizon[int(horizon)]
|
| 231 |
+
return counts_by_horizon
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _roi_loss_from_value_losses(
|
| 235 |
+
*,
|
| 236 |
+
value_losses: torch.Tensor,
|
| 237 |
+
roi_records: torch.Tensor,
|
| 238 |
+
keep_mask: torch.Tensor,
|
| 239 |
+
record_target_idx: torch.Tensor,
|
| 240 |
+
target_count: int,
|
| 241 |
+
global_target_count: Optional[object] = None,
|
| 242 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 243 |
+
"""Reduce a per-value loss over ROI points, normalized inside each target span."""
|
| 244 |
+
if value_losses.ndim != 2:
|
| 245 |
+
raise RuntimeError(
|
| 246 |
+
f"ROI loss expects per-value losses [record,horizon], got {tuple(value_losses.shape)}."
|
| 247 |
+
)
|
| 248 |
+
if roi_records.shape != value_losses.shape or keep_mask.shape != value_losses.shape:
|
| 249 |
+
raise RuntimeError(
|
| 250 |
+
"ROI loss expects mask/value shapes to match: "
|
| 251 |
+
f"loss={tuple(value_losses.shape)}, roi={tuple(roi_records.shape)}, keep={tuple(keep_mask.shape)}."
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
value_losses = value_losses.to(dtype=torch.float32)
|
| 255 |
+
|
| 256 |
+
# ROI is a sparse data-side reweighting over already-supervised points. We
|
| 257 |
+
# normalize within each target before averaging target spans so `alpha`
|
| 258 |
+
# means "add one ROI objective" rather than "scale with the number of ROI
|
| 259 |
+
# points or the forecast horizon".
|
| 260 |
+
roi_mask_float = (roi_records * keep_mask).to(dtype=torch.float32)
|
| 261 |
+
roi_den_by_target = torch.zeros(
|
| 262 |
+
(target_count,), device=value_losses.device, dtype=torch.float32
|
| 263 |
+
)
|
| 264 |
+
roi_num_by_target = torch.zeros(
|
| 265 |
+
(target_count,), device=value_losses.device, dtype=torch.float32
|
| 266 |
+
)
|
| 267 |
+
roi_den_by_target.scatter_add_(0, record_target_idx, roi_mask_float.sum(dim=1))
|
| 268 |
+
if not bool(torch.any(roi_den_by_target > 0.0)):
|
| 269 |
+
zero = value_losses.new_zeros(())
|
| 270 |
+
return zero, value_losses.new_zeros((0,))
|
| 271 |
+
|
| 272 |
+
roi_num_by_target.scatter_add_(
|
| 273 |
+
0,
|
| 274 |
+
record_target_idx,
|
| 275 |
+
(value_losses.to(dtype=torch.float32) * roi_mask_float).sum(dim=1),
|
| 276 |
+
)
|
| 277 |
+
active_roi_targets = roi_den_by_target > 0.0
|
| 278 |
+
roi_per_target = (
|
| 279 |
+
roi_num_by_target[active_roi_targets] / roi_den_by_target[active_roi_targets]
|
| 280 |
+
)
|
| 281 |
+
reduced_roi_loss = _reduce_per_record_loss(
|
| 282 |
+
roi_per_target,
|
| 283 |
+
global_count=global_target_count,
|
| 284 |
+
label="ROI target",
|
| 285 |
+
)
|
| 286 |
+
return reduced_roi_loss, roi_per_target
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def _roi_supervision_counts(
|
| 290 |
+
*,
|
| 291 |
+
roi_records: torch.Tensor,
|
| 292 |
+
keep_mask: torch.Tensor,
|
| 293 |
+
record_target_idx: torch.Tensor,
|
| 294 |
+
target_count: int,
|
| 295 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 296 |
+
"""Count ROI supervision after intersecting data masks with active loss positions."""
|
| 297 |
+
if roi_records.ndim != 2:
|
| 298 |
+
raise RuntimeError(
|
| 299 |
+
f"ROI supervision count expects [record,horizon] ROI masks, got {tuple(roi_records.shape)}."
|
| 300 |
+
)
|
| 301 |
+
if keep_mask.shape != roi_records.shape:
|
| 302 |
+
raise RuntimeError(
|
| 303 |
+
"ROI supervision count expects keep/ROI shapes to match: "
|
| 304 |
+
f"roi={tuple(roi_records.shape)}, keep={tuple(keep_mask.shape)}."
|
| 305 |
+
)
|
| 306 |
+
if record_target_idx.ndim != 1 or int(record_target_idx.shape[0]) != int(
|
| 307 |
+
roi_records.shape[0]
|
| 308 |
+
):
|
| 309 |
+
raise RuntimeError(
|
| 310 |
+
"ROI supervision count expects one target index per record, got "
|
| 311 |
+
f"targets={tuple(record_target_idx.shape)}, roi_records={tuple(roi_records.shape)}."
|
| 312 |
+
)
|
| 313 |
+
if target_count < 0:
|
| 314 |
+
raise RuntimeError(
|
| 315 |
+
f"ROI supervision count requires non-negative target_count, got {target_count}."
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
roi_keep = torch.logical_and(roi_records > 0.0, keep_mask.to(dtype=torch.bool))
|
| 319 |
+
roi_points = roi_keep.to(dtype=torch.float32).sum()
|
| 320 |
+
if target_count == 0:
|
| 321 |
+
return roi_points, roi_records.new_zeros((), dtype=torch.float32)
|
| 322 |
+
roi_den_by_target = torch.zeros(
|
| 323 |
+
(target_count,), device=roi_records.device, dtype=torch.float32
|
| 324 |
+
)
|
| 325 |
+
roi_den_by_target.scatter_add_(
|
| 326 |
+
0, record_target_idx, roi_keep.to(dtype=torch.float32).sum(dim=1)
|
| 327 |
+
)
|
| 328 |
+
roi_targets = (roi_den_by_target > 0.0).to(dtype=torch.float32).sum()
|
| 329 |
+
return roi_points, roi_targets
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def _roi_value_losses_from_predictions(
|
| 333 |
+
*,
|
| 334 |
+
pred_records: torch.Tensor,
|
| 335 |
+
gt_records: torch.Tensor,
|
| 336 |
+
decode_index: int,
|
| 337 |
+
) -> torch.Tensor:
|
| 338 |
+
"""Build per-value ROI MSE on the TimesFM decode channel."""
|
| 339 |
+
pred_records = pred_records.to(dtype=torch.float32)
|
| 340 |
+
gt_records = gt_records.to(device=pred_records.device, dtype=torch.float32)
|
| 341 |
+
decode_index = int(decode_index)
|
| 342 |
+
if decode_index < 0 or decode_index >= int(pred_records.shape[-1]):
|
| 343 |
+
raise RuntimeError(
|
| 344 |
+
"ROI point loss decode index is outside the forecast channel axis: "
|
| 345 |
+
f"decode_index={decode_index}, q_channels={int(pred_records.shape[-1])}."
|
| 346 |
+
)
|
| 347 |
+
return _point_forecast_loss_values(
|
| 348 |
+
pred_records[:, :, decode_index],
|
| 349 |
+
gt_records,
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def _require_known_output_space(output_space: str) -> str:
|
| 354 |
+
output_space = str(output_space).strip().lower()
|
| 355 |
+
if output_space not in {"real", "normalized"}:
|
| 356 |
+
raise RuntimeError(
|
| 357 |
+
f"Forecast output space must be 'real' or 'normalized', got {output_space!r}."
|
| 358 |
+
)
|
| 359 |
+
return output_space
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def _predict_mot_span_quantiles_batched(
|
| 363 |
+
self,
|
| 364 |
+
*,
|
| 365 |
+
mot_runtime: "_MoTRuntime",
|
| 366 |
+
targets: List["_MoTForecastTarget"],
|
| 367 |
+
output_space: str = "real",
|
| 368 |
+
) -> tuple[torch.Tensor, List[int]]:
|
| 369 |
+
"""Run one numeric-head projection per non-empty TS-route sub-batch.
|
| 370 |
+
|
| 371 |
+
`output_space="real"` applies the full inverse transform (sinh, then RevIN
|
| 372 |
+
reverse) for generation/eval; `"normalized"` returns the tower-space head
|
| 373 |
+
output for normalized-space training losses.
|
| 374 |
+
"""
|
| 375 |
+
output_space = _require_known_output_space(output_space)
|
| 376 |
+
if self.generation_tsfm is None or self._timesfm_util is None:
|
| 377 |
+
raise RuntimeError("TimesFM model/util is not initialized.")
|
| 378 |
+
|
| 379 |
+
output_patch_len = int(self.tsfm_output_patch_len)
|
| 380 |
+
q_channels = int(self.tsfm_q_channels)
|
| 381 |
+
if not targets:
|
| 382 |
+
return mot_runtime.ts_hidden.new_empty((0, 0, output_patch_len, q_channels)), []
|
| 383 |
+
|
| 384 |
+
# Group targets by explicit TS route so each uses its matching registered
|
| 385 |
+
# numeric head, then restore caller order for downstream loss or decoding.
|
| 386 |
+
predictions_by_target: list[Optional[torch.Tensor]] = [None] * len(targets)
|
| 387 |
+
mu_by_target: list[Optional[torch.Tensor]] = [None] * len(targets)
|
| 388 |
+
sigma_by_target: list[Optional[torch.Tensor]] = [None] * len(targets)
|
| 389 |
+
patch_counts = [0] * len(targets)
|
| 390 |
+
grouped_indices: dict[int, list[int]] = {
|
| 391 |
+
int(TS_ROUTE_GENERATION): [],
|
| 392 |
+
int(TS_ROUTE_UNDERSTANDING): [],
|
| 393 |
+
}
|
| 394 |
+
for target_idx, target in enumerate(targets):
|
| 395 |
+
route_id = int(target.ts_route_id)
|
| 396 |
+
if route_id not in grouped_indices:
|
| 397 |
+
raise RuntimeError(
|
| 398 |
+
f"Packed forecast head cannot read from TS route id {route_id}."
|
| 399 |
+
)
|
| 400 |
+
grouped_indices[route_id].append(target_idx)
|
| 401 |
+
|
| 402 |
+
for route_id, target_indices in grouped_indices.items():
|
| 403 |
+
if not target_indices:
|
| 404 |
+
continue
|
| 405 |
+
route_targets = [targets[index] for index in target_indices]
|
| 406 |
+
batched_hidden, context_mu, context_sigma, route_patch_counts = (
|
| 407 |
+
_collect_batched_forecast_head_inputs(
|
| 408 |
+
self,
|
| 409 |
+
mot_runtime=mot_runtime,
|
| 410 |
+
targets=route_targets,
|
| 411 |
+
)
|
| 412 |
+
)
|
| 413 |
+
route_timesfm = (
|
| 414 |
+
self.generation_tsfm
|
| 415 |
+
if route_id == int(TS_ROUTE_GENERATION)
|
| 416 |
+
else self.understanding_tsfm
|
| 417 |
+
)
|
| 418 |
+
if route_timesfm is None:
|
| 419 |
+
route_name = (
|
| 420 |
+
"generation"
|
| 421 |
+
if route_id == int(TS_ROUTE_GENERATION)
|
| 422 |
+
else "understanding"
|
| 423 |
+
)
|
| 424 |
+
raise RuntimeError(
|
| 425 |
+
f"{route_name.capitalize()} routed forecast target requires its TimesFM expert."
|
| 426 |
+
)
|
| 427 |
+
out_proj = route_timesfm.output_projection_point
|
| 428 |
+
out_dtype = next(out_proj.parameters()).dtype
|
| 429 |
+
route_outputs = out_proj(batched_hidden.to(dtype=out_dtype)).reshape(
|
| 430 |
+
len(route_targets),
|
| 431 |
+
-1,
|
| 432 |
+
output_patch_len,
|
| 433 |
+
q_channels,
|
| 434 |
+
)
|
| 435 |
+
if int(route_outputs.shape[1]) != max(route_patch_counts):
|
| 436 |
+
raise RuntimeError(
|
| 437 |
+
"Batched TimesFM forecast head produced an unexpected patch axis: "
|
| 438 |
+
f"route={route_id}, output_patches={route_outputs.shape[1]}, "
|
| 439 |
+
f"expected={max(route_patch_counts)}."
|
| 440 |
+
)
|
| 441 |
+
for route_row, target_idx in enumerate(target_indices):
|
| 442 |
+
patch_count = int(route_patch_counts[route_row])
|
| 443 |
+
predictions_by_target[target_idx] = route_outputs[route_row, :patch_count]
|
| 444 |
+
mu_by_target[target_idx] = context_mu[route_row, :patch_count]
|
| 445 |
+
sigma_by_target[target_idx] = context_sigma[route_row, :patch_count]
|
| 446 |
+
patch_counts[target_idx] = patch_count
|
| 447 |
+
|
| 448 |
+
if any(prediction is None for prediction in predictions_by_target):
|
| 449 |
+
raise RuntimeError("Batched TimesFM forecast head left an unprojected target.")
|
| 450 |
+
normed_batched = torch.nn.utils.rnn.pad_sequence(
|
| 451 |
+
[prediction for prediction in predictions_by_target if prediction is not None],
|
| 452 |
+
batch_first=True,
|
| 453 |
+
)
|
| 454 |
+
if output_space == "normalized":
|
| 455 |
+
return normed_batched, patch_counts
|
| 456 |
+
else:
|
| 457 |
+
# The TimesFM tower consumes asinh(RevIN(x)), so sinh must run before
|
| 458 |
+
# the RevIN reverse transform.
|
| 459 |
+
normed_outputs = normed_batched.reshape(
|
| 460 |
+
len(targets), -1, output_patch_len * q_channels
|
| 461 |
+
)
|
| 462 |
+
real_outputs = _inverse_timesfm_value_transform(
|
| 463 |
+
normed_outputs,
|
| 464 |
+
targets=targets,
|
| 465 |
+
backend_label="TimesFM",
|
| 466 |
+
)
|
| 467 |
+
_require_finite_tensor(
|
| 468 |
+
real_outputs,
|
| 469 |
+
label="inverse_timesfm_value_transform(timesfm_head)",
|
| 470 |
+
targets=targets,
|
| 471 |
+
)
|
| 472 |
+
context_mu = torch.nn.utils.rnn.pad_sequence(
|
| 473 |
+
[value for value in mu_by_target if value is not None],
|
| 474 |
+
batch_first=True,
|
| 475 |
+
)
|
| 476 |
+
context_sigma = torch.nn.utils.rnn.pad_sequence(
|
| 477 |
+
[value for value in sigma_by_target if value is not None],
|
| 478 |
+
batch_first=True,
|
| 479 |
+
padding_value=1.0,
|
| 480 |
+
)
|
| 481 |
+
head_outputs = self._timesfm_util.revin(
|
| 482 |
+
real_outputs,
|
| 483 |
+
context_mu.to(device=real_outputs.device, dtype=real_outputs.dtype),
|
| 484 |
+
context_sigma.to(device=real_outputs.device, dtype=real_outputs.dtype),
|
| 485 |
+
reverse=True,
|
| 486 |
+
)
|
| 487 |
+
forecast_batched = head_outputs.reshape(
|
| 488 |
+
len(targets),
|
| 489 |
+
-1,
|
| 490 |
+
output_patch_len,
|
| 491 |
+
q_channels,
|
| 492 |
+
)
|
| 493 |
+
if int(forecast_batched.shape[1]) != max(patch_counts):
|
| 494 |
+
raise RuntimeError(
|
| 495 |
+
"Batched TimesFM forecast head produced an unexpected patch axis: "
|
| 496 |
+
f"output_patches={forecast_batched.shape[1]}, expected={max(patch_counts)}"
|
| 497 |
+
)
|
| 498 |
+
return forecast_batched, patch_counts
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def _collect_batched_forecast_head_inputs(
|
| 502 |
+
self,
|
| 503 |
+
*,
|
| 504 |
+
mot_runtime: "_MoTRuntime",
|
| 505 |
+
targets: List["_MoTForecastTarget"],
|
| 506 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, List[int]]:
|
| 507 |
+
"""Pad TS hidden states and RevIN stats for a batched forecast head call."""
|
| 508 |
+
hidden_slices: List[torch.Tensor] = []
|
| 509 |
+
context_mu_parts: List[torch.Tensor] = []
|
| 510 |
+
context_sigma_parts: List[torch.Tensor] = []
|
| 511 |
+
patch_counts: List[int] = []
|
| 512 |
+
for target_idx, target in enumerate(targets):
|
| 513 |
+
if target.ts_end < target.ts_start:
|
| 514 |
+
raise RuntimeError(
|
| 515 |
+
"Packed forecast target has negative TS slice: "
|
| 516 |
+
f"target_idx={target_idx}, start={target.ts_start}, end={target.ts_end}"
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
patch_count = int(target.ts_end) - int(target.ts_start)
|
| 520 |
+
if patch_count <= 0:
|
| 521 |
+
raise RuntimeError(
|
| 522 |
+
"Batched packed forecast head requires every supervised target to have at least "
|
| 523 |
+
f"one TS patch, got target_idx={target_idx}, patches={patch_count}."
|
| 524 |
+
)
|
| 525 |
+
if patch_count != int(target.patch_valid_lengths.shape[0]):
|
| 526 |
+
raise RuntimeError(
|
| 527 |
+
"Packed TimesFM patch count mismatch before batched forecast head: "
|
| 528 |
+
f"target_idx={target_idx}, hidden_patches={patch_count}, "
|
| 529 |
+
f"valid_len_patches={target.patch_valid_lengths.shape[0]}"
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
context_mu = target.context_mu.reshape(-1)
|
| 533 |
+
context_sigma = target.context_sigma.reshape(-1)
|
| 534 |
+
if (
|
| 535 |
+
int(context_mu.shape[0]) != patch_count
|
| 536 |
+
or int(context_sigma.shape[0]) != patch_count
|
| 537 |
+
):
|
| 538 |
+
raise RuntimeError(
|
| 539 |
+
"Packed TimesFM RevIN stats must align one value per target patch: "
|
| 540 |
+
f"target_idx={target_idx}, patches={patch_count}, "
|
| 541 |
+
f"mu={tuple(target.context_mu.shape)}, sigma={tuple(target.context_sigma.shape)}"
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
route_id = int(target.ts_route_id)
|
| 545 |
+
if route_id == int(TS_ROUTE_UNDERSTANDING):
|
| 546 |
+
source_hidden = mot_runtime.understanding_hidden
|
| 547 |
+
if source_hidden is None:
|
| 548 |
+
raise RuntimeError(
|
| 549 |
+
"Understanding routed forecast target requires `mot_runtime.understanding_hidden`."
|
| 550 |
+
)
|
| 551 |
+
elif route_id == int(TS_ROUTE_GENERATION):
|
| 552 |
+
source_hidden = mot_runtime.ts_hidden
|
| 553 |
+
else:
|
| 554 |
+
raise RuntimeError(
|
| 555 |
+
f"Packed forecast head cannot read from TS route id {route_id}."
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
hidden_slices.append(source_hidden[target.ts_start : target.ts_end])
|
| 559 |
+
context_mu_parts.append(context_mu.to(device=source_hidden.device))
|
| 560 |
+
context_sigma_parts.append(context_sigma.to(device=source_hidden.device))
|
| 561 |
+
patch_counts.append(patch_count)
|
| 562 |
+
|
| 563 |
+
batched_hidden = torch.nn.utils.rnn.pad_sequence(hidden_slices, batch_first=True)
|
| 564 |
+
context_mu = torch.nn.utils.rnn.pad_sequence(context_mu_parts, batch_first=True)
|
| 565 |
+
context_sigma = torch.nn.utils.rnn.pad_sequence(
|
| 566 |
+
context_sigma_parts,
|
| 567 |
+
batch_first=True,
|
| 568 |
+
padding_value=1.0,
|
| 569 |
+
)
|
| 570 |
+
return batched_hidden, context_mu, context_sigma, patch_counts
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def _compute_flat_forecast_losses_from_predictions(
|
| 574 |
+
self,
|
| 575 |
+
*,
|
| 576 |
+
pred_quantiles_batched: torch.Tensor,
|
| 577 |
+
targets: List[Any],
|
| 578 |
+
patch_counts: List[int],
|
| 579 |
+
mot_ts_num_records_in_batch: Optional[object] = None,
|
| 580 |
+
mot_ts_num_roi_targets_in_batch: Optional[object] = None,
|
| 581 |
+
loss_space: str = "real",
|
| 582 |
+
) -> Optional[_FlatForecastLossParts]:
|
| 583 |
+
"""
|
| 584 |
+
Reduce all patch forecasts in one masked tensor while preserving patch-level weighting.
|
| 585 |
+
|
| 586 |
+
The previous path launched point/quantile loss kernels once per patch. This
|
| 587 |
+
helper builds one flat `[num_patch_forecasts, horizon]` mask and performs the
|
| 588 |
+
loss reductions in batched tensor ops. Each record is owned by patch i and
|
| 589 |
+
aligns it to the raw suffix immediately after that patch; detached per-record
|
| 590 |
+
caps may rescale its final objective contribution.
|
| 591 |
+
|
| 592 |
+
Predictions must be in the tower-normalized space (producer called with
|
| 593 |
+
`output_space="normalized"`); targets are scored against
|
| 594 |
+
`asinh((gt - mu) / sigma)` built from each record's owner-patch causal stats.
|
| 595 |
+
`loss_space` accepts only `"normalized"`; the `"real"` default exists so a
|
| 596 |
+
caller that forgets to state the space fails loudly instead of silently
|
| 597 |
+
picking a value frame.
|
| 598 |
+
"""
|
| 599 |
+
loss_space = _require_known_output_space(loss_space)
|
| 600 |
+
if loss_space != "normalized":
|
| 601 |
+
raise RuntimeError("Routed TS losses must use loss_space='normalized'.")
|
| 602 |
+
if pred_quantiles_batched.ndim != 4:
|
| 603 |
+
raise RuntimeError(
|
| 604 |
+
"Flat forecast loss expects predictions shaped [target, patch, horizon, channel], got "
|
| 605 |
+
f"{tuple(pred_quantiles_batched.shape)}"
|
| 606 |
+
)
|
| 607 |
+
if int(pred_quantiles_batched.shape[0]) != len(targets):
|
| 608 |
+
raise RuntimeError(
|
| 609 |
+
"Flat forecast loss target count mismatch: "
|
| 610 |
+
f"pred_targets={pred_quantiles_batched.shape[0]}, targets={len(targets)}"
|
| 611 |
+
)
|
| 612 |
+
if len(patch_counts) != len(targets):
|
| 613 |
+
raise RuntimeError(
|
| 614 |
+
"Flat forecast loss patch-count metadata mismatch: "
|
| 615 |
+
f"patch_counts={len(patch_counts)}, targets={len(targets)}"
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
device = pred_quantiles_batched.device
|
| 619 |
+
dtype = torch.float32
|
| 620 |
+
max_pred_patches = int(pred_quantiles_batched.shape[1])
|
| 621 |
+
forecast_horizon = int(pred_quantiles_batched.shape[2])
|
| 622 |
+
q_channels = int(pred_quantiles_batched.shape[3])
|
| 623 |
+
|
| 624 |
+
values_by_target: List[torch.Tensor] = []
|
| 625 |
+
roi_masks_by_target: List[torch.Tensor] = []
|
| 626 |
+
patch_length_parts: List[torch.Tensor] = []
|
| 627 |
+
context_mu_parts: List[torch.Tensor] = []
|
| 628 |
+
context_sigma_parts: List[torch.Tensor] = []
|
| 629 |
+
value_lengths: List[int] = []
|
| 630 |
+
loss_start_idxs: List[int] = []
|
| 631 |
+
|
| 632 |
+
for target_idx, target in enumerate(targets):
|
| 633 |
+
pred_patch_count = int(patch_counts[target_idx])
|
| 634 |
+
if pred_patch_count < 0 or pred_patch_count > max_pred_patches:
|
| 635 |
+
raise RuntimeError(
|
| 636 |
+
"Flat forecast loss received invalid per-target patch count: "
|
| 637 |
+
f"target_idx={target_idx}, patches={pred_patch_count}, max_patches={max_pred_patches}"
|
| 638 |
+
)
|
| 639 |
+
context_mu = target.context_mu.reshape(-1).to(
|
| 640 |
+
device=device, dtype=torch.float32
|
| 641 |
+
)
|
| 642 |
+
context_sigma = target.context_sigma.reshape(-1).to(
|
| 643 |
+
device=device, dtype=torch.float32
|
| 644 |
+
)
|
| 645 |
+
if (
|
| 646 |
+
int(context_mu.shape[0]) != pred_patch_count
|
| 647 |
+
or int(context_sigma.shape[0]) != pred_patch_count
|
| 648 |
+
):
|
| 649 |
+
raise RuntimeError(
|
| 650 |
+
"Normalized-space forecast loss stats must align one value per owner patch: "
|
| 651 |
+
f"target_idx={target_idx}, patches={pred_patch_count}, "
|
| 652 |
+
f"mu={tuple(target.context_mu.shape)}, sigma={tuple(target.context_sigma.shape)}."
|
| 653 |
+
)
|
| 654 |
+
context_mu_parts.append(context_mu)
|
| 655 |
+
context_sigma_parts.append(context_sigma)
|
| 656 |
+
|
| 657 |
+
if target.values.ndim != 1:
|
| 658 |
+
raise RuntimeError(
|
| 659 |
+
"Flat forecast loss expects raw TS values to be one-dimensional, got "
|
| 660 |
+
f"target_idx={target_idx}, values_shape={tuple(target.values.shape)}"
|
| 661 |
+
)
|
| 662 |
+
values_len = int(target.values.shape[0])
|
| 663 |
+
if pred_patch_count != int(target.patch_valid_lengths.shape[0]):
|
| 664 |
+
raise RuntimeError(
|
| 665 |
+
"Flat forecast loss patch count mismatch with valid lengths: "
|
| 666 |
+
f"target_idx={target_idx}, pred_patches={pred_patch_count}, "
|
| 667 |
+
f"valid_len_patches={target.patch_valid_lengths.shape[0]}"
|
| 668 |
+
)
|
| 669 |
+
if pred_patch_count == 0:
|
| 670 |
+
raise RuntimeError(
|
| 671 |
+
"Routed TS supervision requires at least one real TS patch per span."
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
# Raw targets must stay FP32 until after causal normalization. Casting
|
| 675 |
+
# large-offset series to BF16 here can erase the complete future delta.
|
| 676 |
+
values_by_target.append(target.values.to(device=device, dtype=torch.float32))
|
| 677 |
+
target_roi_mask = target.loss_roi_mask
|
| 678 |
+
if target_roi_mask is None:
|
| 679 |
+
target_roi_mask = torch.zeros_like(target.values, dtype=torch.float32)
|
| 680 |
+
target_roi_mask = target_roi_mask.to(device=device, dtype=torch.float32)
|
| 681 |
+
if target_roi_mask.ndim != 1 or int(target_roi_mask.shape[0]) != values_len:
|
| 682 |
+
raise RuntimeError(
|
| 683 |
+
"Flat forecast loss expects loss_roi_mask to align with raw TS values, got "
|
| 684 |
+
f"target_idx={target_idx}, mask_shape={tuple(target_roi_mask.shape)}, values_len={values_len}."
|
| 685 |
+
)
|
| 686 |
+
if bool(
|
| 687 |
+
torch.any(torch.logical_or(target_roi_mask < 0.0, target_roi_mask > 1.0))
|
| 688 |
+
):
|
| 689 |
+
raise RuntimeError(
|
| 690 |
+
"Flat forecast loss expects loss_roi_mask values in [0, 1], got "
|
| 691 |
+
f"target_idx={target_idx}."
|
| 692 |
+
)
|
| 693 |
+
roi_masks_by_target.append(target_roi_mask)
|
| 694 |
+
patch_length_parts.append(
|
| 695 |
+
target.patch_valid_lengths.to(device=device, dtype=torch.long)
|
| 696 |
+
)
|
| 697 |
+
value_lengths.append(values_len)
|
| 698 |
+
loss_start_idx = int(target.loss_start_idx)
|
| 699 |
+
loss_start_idxs.append(loss_start_idx)
|
| 700 |
+
|
| 701 |
+
if not values_by_target:
|
| 702 |
+
return None
|
| 703 |
+
|
| 704 |
+
values_padded = torch.nn.utils.rnn.pad_sequence(values_by_target, batch_first=True)
|
| 705 |
+
roi_masks_padded = torch.nn.utils.rnn.pad_sequence(
|
| 706 |
+
roi_masks_by_target, batch_first=True
|
| 707 |
+
)
|
| 708 |
+
patch_lengths = torch.nn.utils.rnn.pad_sequence(
|
| 709 |
+
patch_length_parts, batch_first=True
|
| 710 |
+
)
|
| 711 |
+
context_mu_padded = torch.nn.utils.rnn.pad_sequence(
|
| 712 |
+
context_mu_parts, batch_first=True
|
| 713 |
+
)
|
| 714 |
+
context_sigma_padded = torch.nn.utils.rnn.pad_sequence(
|
| 715 |
+
context_sigma_parts, batch_first=True, padding_value=1.0
|
| 716 |
+
)
|
| 717 |
+
max_values_len = int(values_padded.shape[1])
|
| 718 |
+
if max_values_len <= 0:
|
| 719 |
+
raise RuntimeError(
|
| 720 |
+
"Flat forecast loss built supervision records without any raw target values."
|
| 721 |
+
)
|
| 722 |
+
if int(patch_lengths.shape[1]) != max_pred_patches:
|
| 723 |
+
raise RuntimeError(
|
| 724 |
+
"Flat forecast loss patch-length padding axis does not match predictions: "
|
| 725 |
+
f"patch_lengths={patch_lengths.shape[1]}, pred_patches={max_pred_patches}"
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
patch_counts_tensor = torch.tensor(patch_counts, device=device, dtype=torch.long)
|
| 729 |
+
values_lengths_tensor = torch.tensor(value_lengths, device=device, dtype=torch.long)
|
| 730 |
+
loss_start_idxs_tensor = torch.tensor(
|
| 731 |
+
loss_start_idxs, device=device, dtype=torch.long
|
| 732 |
+
)
|
| 733 |
+
prefix_ends = torch.cumsum(patch_lengths, dim=1)
|
| 734 |
+
last_patch_indices = torch.clamp(patch_counts_tensor - 1, min=0)
|
| 735 |
+
covered_lengths = prefix_ends.gather(1, last_patch_indices[:, None]).squeeze(1)
|
| 736 |
+
patch_axis = torch.arange(
|
| 737 |
+
max_pred_patches, device=device, dtype=torch.long
|
| 738 |
+
).unsqueeze(0)
|
| 739 |
+
successor_patch_mask = patch_axis < (patch_counts_tensor - 1).unsqueeze(1)
|
| 740 |
+
bad_contract = torch.logical_or(patch_counts_tensor <= 0, patch_lengths[:, 0] <= 0)
|
| 741 |
+
bad_contract = torch.logical_or(
|
| 742 |
+
bad_contract, covered_lengths != values_lengths_tensor
|
| 743 |
+
)
|
| 744 |
+
if max_pred_patches > 1:
|
| 745 |
+
bad_successors = torch.any(
|
| 746 |
+
torch.logical_and(patch_lengths[:, 1:] <= 0, successor_patch_mask[:, :-1]),
|
| 747 |
+
dim=1,
|
| 748 |
+
)
|
| 749 |
+
bad_contract = torch.logical_or(bad_contract, bad_successors)
|
| 750 |
+
if bool(torch.any(bad_contract)):
|
| 751 |
+
bad_idx = int(torch.nonzero(bad_contract, as_tuple=False)[0].item())
|
| 752 |
+
raise RuntimeError(
|
| 753 |
+
"Packed routed TS supervision received an invalid patch/value contract: "
|
| 754 |
+
f"target_idx={bad_idx}, patches={int(patch_counts[bad_idx])}, "
|
| 755 |
+
f"covered_raw_points={int(covered_lengths[bad_idx].item())}, "
|
| 756 |
+
f"raw_values={int(values_lengths_tensor[bad_idx].item())}."
|
| 757 |
+
)
|
| 758 |
+
|
| 759 |
+
decode_index = int(self.tsfm_decode_index)
|
| 760 |
+
if decode_index < 0 or decode_index >= q_channels:
|
| 761 |
+
raise RuntimeError(
|
| 762 |
+
"TS forecast decode index is outside the forecast channel axis: "
|
| 763 |
+
f"decode_index={decode_index}, q_channels={q_channels}"
|
| 764 |
+
)
|
| 765 |
+
accounting = build_mot_forecast_loss_accounting(
|
| 766 |
+
patch_lengths=patch_lengths,
|
| 767 |
+
patch_counts=patch_counts_tensor,
|
| 768 |
+
value_lengths=values_lengths_tensor,
|
| 769 |
+
loss_start_idxs=loss_start_idxs_tensor,
|
| 770 |
+
forecast_horizon=forecast_horizon,
|
| 771 |
+
)
|
| 772 |
+
if accounting is None:
|
| 773 |
+
return None
|
| 774 |
+
|
| 775 |
+
keep_mask = accounting.keep_mask
|
| 776 |
+
record_target_idx = accounting.record_target_idx
|
| 777 |
+
record_patch_idx = accounting.record_patch_idx
|
| 778 |
+
raw_indices = accounting.raw_indices
|
| 779 |
+
valid_counts = accounting.valid_counts.to(dtype=dtype)
|
| 780 |
+
if bool(torch.any(valid_counts <= 0)):
|
| 781 |
+
raise RuntimeError(
|
| 782 |
+
"Flat forecast loss built a record with no supervised points."
|
| 783 |
+
)
|
| 784 |
+
if bool(torch.any(torch.logical_and(keep_mask, raw_indices >= max_values_len))):
|
| 785 |
+
raise RuntimeError(
|
| 786 |
+
"Flat forecast loss built raw target indices outside the padded target width."
|
| 787 |
+
)
|
| 788 |
+
|
| 789 |
+
# Head projections may run in BF16, but every target transform and loss
|
| 790 |
+
# reduction is FP32. The cast remains differentiable back to the head.
|
| 791 |
+
pred_records = pred_quantiles_batched[record_target_idx, record_patch_idx].to(
|
| 792 |
+
dtype=torch.float32
|
| 793 |
+
)
|
| 794 |
+
safe_raw_indices = torch.clamp(raw_indices, max=max_values_len - 1)
|
| 795 |
+
gt_records = values_padded[record_target_idx.unsqueeze(1), safe_raw_indices]
|
| 796 |
+
roi_records = roi_masks_padded[record_target_idx.unsqueeze(1), safe_raw_indices]
|
| 797 |
+
# Each record was predicted in its owner patch's causal-stats frame, so
|
| 798 |
+
# the targets move into the same configured post-scale value frame.
|
| 799 |
+
owner_mu = context_mu_padded[record_target_idx, record_patch_idx]
|
| 800 |
+
owner_sigma = context_sigma_padded[record_target_idx, record_patch_idx].clamp_min(
|
| 801 |
+
1e-6
|
| 802 |
+
)
|
| 803 |
+
gt_records = _transform_forecast_targets_for_loss(
|
| 804 |
+
gt_records,
|
| 805 |
+
owner_mu=owner_mu,
|
| 806 |
+
owner_sigma=owner_sigma,
|
| 807 |
+
)
|
| 808 |
+
mask_float = keep_mask.to(dtype=dtype)
|
| 809 |
+
|
| 810 |
+
zero = pred_records.new_zeros(())
|
| 811 |
+
roi_supervised_points, roi_supervised_targets = _roi_supervision_counts(
|
| 812 |
+
roi_records=roi_records,
|
| 813 |
+
keep_mask=keep_mask,
|
| 814 |
+
record_target_idx=record_target_idx,
|
| 815 |
+
target_count=len(targets),
|
| 816 |
+
)
|
| 817 |
+
global_point = zero
|
| 818 |
+
total_point = zero
|
| 819 |
+
roi_point_loss = global_point.new_zeros(())
|
| 820 |
+
roi_weighted_point_loss = global_point.new_zeros(())
|
| 821 |
+
global_quantile = zero
|
| 822 |
+
total_quantile = zero
|
| 823 |
+
global_uncapped = zero
|
| 824 |
+
global_cap_scale = zero.new_ones(())
|
| 825 |
+
roi_uncapped = zero
|
| 826 |
+
roi_weighted_uncapped = zero
|
| 827 |
+
roi_cap_scale = zero.new_ones(())
|
| 828 |
+
record_count = int(record_target_idx.numel())
|
| 829 |
+
point_per_record = pred_records.new_zeros((record_count,))
|
| 830 |
+
quantile_per_record = pred_records.new_zeros((record_count,))
|
| 831 |
+
|
| 832 |
+
taus = torch.tensor(self.tsfm_quantile_taus, device=device, dtype=dtype)
|
| 833 |
+
if int(taus.numel()) != q_channels:
|
| 834 |
+
raise RuntimeError(
|
| 835 |
+
"TS expert quantile tau count must match the forecast channel axis: "
|
| 836 |
+
f"taus={int(taus.numel())}, q_channels={q_channels}"
|
| 837 |
+
)
|
| 838 |
+
quantile_mask = taus >= 0
|
| 839 |
+
if torch.any(quantile_mask):
|
| 840 |
+
active_taus = taus[quantile_mask]
|
| 841 |
+
pred_q = pred_records[:, :, quantile_mask]
|
| 842 |
+
err = gt_records[:, :, None] - pred_q
|
| 843 |
+
quantile_by_value = torch.maximum(
|
| 844 |
+
active_taus.view(1, 1, -1) * err,
|
| 845 |
+
(active_taus.view(1, 1, -1) - 1.0) * err,
|
| 846 |
+
)
|
| 847 |
+
quantile_denominator = valid_counts * int(active_taus.shape[0])
|
| 848 |
+
quantile_per_record = (quantile_by_value * mask_float[:, :, None]).sum(
|
| 849 |
+
dim=(1, 2)
|
| 850 |
+
) / quantile_denominator
|
| 851 |
+
|
| 852 |
+
roi_alpha = float(self.ts_roi_mse_alpha)
|
| 853 |
+
point_by_value = _point_forecast_loss_values(
|
| 854 |
+
pred_records[:, :, decode_index],
|
| 855 |
+
gt_records,
|
| 856 |
+
)
|
| 857 |
+
point_per_record = (point_by_value * mask_float).sum(dim=1) / valid_counts
|
| 858 |
+
global_point = _reduce_per_record_loss(
|
| 859 |
+
point_per_record,
|
| 860 |
+
global_count=mot_ts_num_records_in_batch,
|
| 861 |
+
label="TS point record",
|
| 862 |
+
)
|
| 863 |
+
|
| 864 |
+
# Point and quantile are the same TS objective family. Keep their internal
|
| 865 |
+
# contribution unweighted here; the public knob is the outer
|
| 866 |
+
# `mot_ts_loss_weight` applied to the complete `ts_aux_loss`.
|
| 867 |
+
global_per_record_uncapped = point_per_record + quantile_per_record
|
| 868 |
+
global_uncapped = _reduce_per_record_loss(
|
| 869 |
+
global_per_record_uncapped,
|
| 870 |
+
global_count=mot_ts_num_records_in_batch,
|
| 871 |
+
label="TS global record",
|
| 872 |
+
)
|
| 873 |
+
global_record_cap_scales = _detached_per_item_loss_cap_scale(
|
| 874 |
+
global_per_record_uncapped,
|
| 875 |
+
_MOT_TS_GLOBAL_LOSS_DETACHED_CAP,
|
| 876 |
+
)
|
| 877 |
+
global_cap_scale = global_record_cap_scales.mean()
|
| 878 |
+
global_point = _reduce_per_record_loss(
|
| 879 |
+
point_per_record * global_record_cap_scales,
|
| 880 |
+
global_count=mot_ts_num_records_in_batch,
|
| 881 |
+
label="TS capped point record",
|
| 882 |
+
)
|
| 883 |
+
global_quantile = _reduce_per_record_loss(
|
| 884 |
+
quantile_per_record * global_record_cap_scales,
|
| 885 |
+
global_count=mot_ts_num_records_in_batch,
|
| 886 |
+
label="TS capped quantile record",
|
| 887 |
+
)
|
| 888 |
+
total_point = global_point
|
| 889 |
+
total_quantile = global_quantile
|
| 890 |
+
|
| 891 |
+
if roi_alpha > 0.0:
|
| 892 |
+
roi_value_losses = _roi_value_losses_from_predictions(
|
| 893 |
+
pred_records=pred_records,
|
| 894 |
+
gt_records=gt_records,
|
| 895 |
+
decode_index=decode_index,
|
| 896 |
+
)
|
| 897 |
+
reduced_roi_loss, roi_per_target = _roi_loss_from_value_losses(
|
| 898 |
+
value_losses=roi_value_losses,
|
| 899 |
+
roi_records=roi_records,
|
| 900 |
+
keep_mask=mask_float,
|
| 901 |
+
record_target_idx=record_target_idx,
|
| 902 |
+
target_count=len(targets),
|
| 903 |
+
global_target_count=mot_ts_num_roi_targets_in_batch,
|
| 904 |
+
)
|
| 905 |
+
roi_uncapped = reduced_roi_loss
|
| 906 |
+
if int(roi_per_target.numel()) > 0:
|
| 907 |
+
roi_target_cap_scales = _detached_per_item_loss_cap_scale(
|
| 908 |
+
roi_per_target,
|
| 909 |
+
_MOT_TS_ROI_LOSS_DETACHED_CAP,
|
| 910 |
+
)
|
| 911 |
+
roi_loss_for_objective = _reduce_per_record_loss(
|
| 912 |
+
roi_per_target * roi_target_cap_scales,
|
| 913 |
+
global_count=mot_ts_num_roi_targets_in_batch,
|
| 914 |
+
label="capped ROI target",
|
| 915 |
+
)
|
| 916 |
+
roi_cap_scale = roi_target_cap_scales.mean()
|
| 917 |
+
else:
|
| 918 |
+
roi_loss_for_objective = roi_uncapped
|
| 919 |
+
roi_weighted_uncapped = total_quantile.new_tensor(roi_alpha) * roi_uncapped
|
| 920 |
+
roi_point_loss = roi_loss_for_objective
|
| 921 |
+
roi_weighted_point_loss = total_point.new_tensor(roi_alpha) * roi_point_loss
|
| 922 |
+
total_point = total_point + roi_weighted_point_loss
|
| 923 |
+
|
| 924 |
+
return _FlatForecastLossParts(
|
| 925 |
+
point=total_point,
|
| 926 |
+
quantile=total_quantile,
|
| 927 |
+
global_point=global_point,
|
| 928 |
+
global_quantile=global_quantile,
|
| 929 |
+
roi_point=roi_point_loss,
|
| 930 |
+
roi_weighted_point=roi_weighted_point_loss,
|
| 931 |
+
global_uncapped=global_uncapped,
|
| 932 |
+
global_cap_scale=global_cap_scale,
|
| 933 |
+
roi_uncapped=roi_uncapped,
|
| 934 |
+
roi_weighted_uncapped=roi_weighted_uncapped,
|
| 935 |
+
roi_cap_scale=roi_cap_scale,
|
| 936 |
+
roi_supervised_points=roi_supervised_points,
|
| 937 |
+
roi_supervised_targets=roi_supervised_targets,
|
| 938 |
+
)
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
def _predict_mot_span_quantiles(
|
| 942 |
+
self,
|
| 943 |
+
*,
|
| 944 |
+
mot_runtime: "_MoTRuntime",
|
| 945 |
+
target: "_MoTForecastTarget",
|
| 946 |
+
) -> torch.Tensor:
|
| 947 |
+
"""Run the native numeric head on one packed TS target slice (real space)."""
|
| 948 |
+
pred_batched, patch_counts = _predict_mot_span_quantiles_batched(
|
| 949 |
+
self,
|
| 950 |
+
mot_runtime=mot_runtime,
|
| 951 |
+
targets=[target],
|
| 952 |
+
output_space="real",
|
| 953 |
+
)
|
| 954 |
+
if len(patch_counts) != 1:
|
| 955 |
+
raise RuntimeError(
|
| 956 |
+
f"Single-target forecast expected one patch count, got {patch_counts}."
|
| 957 |
+
)
|
| 958 |
+
return pred_batched[0, : int(patch_counts[0])]
|
| 959 |
+
|
| 960 |
+
|
| 961 |
+
def _finalize_forecast_routed_losses(
|
| 962 |
+
self,
|
| 963 |
+
*,
|
| 964 |
+
main_losses: _FlatForecastLossParts,
|
| 965 |
+
extra_losses: Optional[Dict[str, torch.Tensor]] = None,
|
| 966 |
+
) -> Dict[str, torch.Tensor]:
|
| 967 |
+
"""Assemble the routed TS loss dictionary."""
|
| 968 |
+
total_point = main_losses.point
|
| 969 |
+
total_quantile = main_losses.quantile
|
| 970 |
+
total_ts_aux = total_point + total_quantile
|
| 971 |
+
total_roi_loss = main_losses.roi_weighted_point
|
| 972 |
+
routed_losses: Dict[str, torch.Tensor] = {
|
| 973 |
+
"ts_aux_loss": total_ts_aux,
|
| 974 |
+
"ts_aux_point_loss": total_point,
|
| 975 |
+
"ts_aux_quantile_loss": total_quantile,
|
| 976 |
+
"ts_aux_global_point_loss": main_losses.global_point,
|
| 977 |
+
"ts_aux_global_quantile_loss": main_losses.global_quantile,
|
| 978 |
+
"ts_aux_global_uncapped_loss": main_losses.global_uncapped,
|
| 979 |
+
"ts_aux_global_cap_scale": main_losses.global_cap_scale,
|
| 980 |
+
"ts_aux_roi_loss": total_roi_loss,
|
| 981 |
+
"ts_aux_roi_point_loss": main_losses.roi_point,
|
| 982 |
+
"ts_aux_roi_weighted_point_loss": main_losses.roi_weighted_point,
|
| 983 |
+
"ts_aux_roi_uncapped_loss": main_losses.roi_uncapped,
|
| 984 |
+
"ts_aux_roi_weighted_uncapped_loss": main_losses.roi_weighted_uncapped,
|
| 985 |
+
"ts_aux_roi_cap_scale": main_losses.roi_cap_scale,
|
| 986 |
+
"ts_aux_roi_supervised_points": main_losses.roi_supervised_points,
|
| 987 |
+
"ts_aux_roi_supervised_targets": main_losses.roi_supervised_targets,
|
| 988 |
+
"ts_aux_roi_alpha": total_point.new_tensor(float(self.ts_roi_mse_alpha)),
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
if extra_losses:
|
| 992 |
+
weighted_loss = extra_losses.get("ts_aux_understanding_weighted_loss")
|
| 993 |
+
if isinstance(weighted_loss, torch.Tensor):
|
| 994 |
+
total_ts_aux = total_ts_aux + weighted_loss
|
| 995 |
+
routed_losses["ts_aux_loss"] = total_ts_aux
|
| 996 |
+
routed_losses.update(extra_losses)
|
| 997 |
+
|
| 998 |
+
return routed_losses
|
| 999 |
+
|
| 1000 |
+
|
| 1001 |
+
def _zero_flat_forecast_loss_parts(reference: torch.Tensor) -> _FlatForecastLossParts:
|
| 1002 |
+
zero = reference.new_zeros((), dtype=torch.float32)
|
| 1003 |
+
one = reference.new_ones((), dtype=torch.float32)
|
| 1004 |
+
return _FlatForecastLossParts(
|
| 1005 |
+
point=zero,
|
| 1006 |
+
quantile=zero,
|
| 1007 |
+
global_point=zero,
|
| 1008 |
+
global_quantile=zero,
|
| 1009 |
+
roi_point=zero,
|
| 1010 |
+
roi_weighted_point=zero,
|
| 1011 |
+
global_uncapped=zero,
|
| 1012 |
+
global_cap_scale=one,
|
| 1013 |
+
roi_uncapped=zero,
|
| 1014 |
+
roi_weighted_uncapped=zero,
|
| 1015 |
+
roi_cap_scale=one,
|
| 1016 |
+
roi_supervised_points=zero,
|
| 1017 |
+
roi_supervised_targets=zero,
|
| 1018 |
+
)
|
| 1019 |
+
|
| 1020 |
+
|
| 1021 |
+
def _compute_understanding_head_losses(
|
| 1022 |
+
self,
|
| 1023 |
+
*,
|
| 1024 |
+
mot_runtime: "_MoTRuntime",
|
| 1025 |
+
targets: List[Any],
|
| 1026 |
+
mot_ts_understanding_num_points: Optional[object] = None,
|
| 1027 |
+
mot_ts_understanding_num_fft_elements: Optional[object] = None,
|
| 1028 |
+
) -> Dict[str, torch.Tensor]:
|
| 1029 |
+
"""Reconstruct observed/context TS patches from understanding hidden states."""
|
| 1030 |
+
weight = float(self.ts_understanding_loss_weight)
|
| 1031 |
+
understanding_head = self.understanding_head
|
| 1032 |
+
understanding_hidden = mot_runtime.understanding_hidden
|
| 1033 |
+
reference = understanding_hidden
|
| 1034 |
+
if reference is None and targets:
|
| 1035 |
+
reference = targets[0].values
|
| 1036 |
+
if reference is None:
|
| 1037 |
+
reference = torch.zeros((), dtype=torch.float32)
|
| 1038 |
+
zero = reference.new_zeros((), dtype=torch.float32)
|
| 1039 |
+
count_zero = reference.new_zeros((), dtype=torch.float32)
|
| 1040 |
+
disabled = weight <= 0.0
|
| 1041 |
+
if (
|
| 1042 |
+
disabled
|
| 1043 |
+
or understanding_hidden is None
|
| 1044 |
+
or int(understanding_hidden.shape[0]) == 0
|
| 1045 |
+
):
|
| 1046 |
+
return {
|
| 1047 |
+
"ts_aux_understanding_loss": zero,
|
| 1048 |
+
"ts_aux_understanding_weighted_loss": zero,
|
| 1049 |
+
"ts_aux_understanding_time_loss": zero,
|
| 1050 |
+
"ts_aux_understanding_fft_loss": zero,
|
| 1051 |
+
"ts_aux_understanding_patches": count_zero,
|
| 1052 |
+
"ts_aux_understanding_points": count_zero,
|
| 1053 |
+
"ts_aux_understanding_weight": count_zero,
|
| 1054 |
+
}
|
| 1055 |
+
if understanding_head is None:
|
| 1056 |
+
raise RuntimeError(
|
| 1057 |
+
"ts_understanding_loss_weight > 0 requires understanding_head."
|
| 1058 |
+
)
|
| 1059 |
+
|
| 1060 |
+
hidden_parts: List[torch.Tensor] = []
|
| 1061 |
+
value_parts: List[torch.Tensor] = []
|
| 1062 |
+
mask_parts: List[torch.Tensor] = []
|
| 1063 |
+
for target in targets:
|
| 1064 |
+
if int(target.role_id) == int(ROLE_TARGET):
|
| 1065 |
+
continue
|
| 1066 |
+
if int(target.ts_route_id) != int(TS_ROUTE_UNDERSTANDING):
|
| 1067 |
+
continue
|
| 1068 |
+
ts_start = int(target.ts_start)
|
| 1069 |
+
ts_end = int(target.ts_end)
|
| 1070 |
+
if ts_end <= ts_start:
|
| 1071 |
+
continue
|
| 1072 |
+
if not (0 <= ts_start < ts_end <= int(understanding_hidden.shape[0])):
|
| 1073 |
+
raise RuntimeError(
|
| 1074 |
+
"TS understanding reconstruction target points outside packed understanding hidden states: "
|
| 1075 |
+
f"sample={int(target.sample_idx)}, "
|
| 1076 |
+
f"slot={int(target.slot_idx)}, "
|
| 1077 |
+
f"ts_range=[{ts_start},{ts_end}), hidden_count={int(understanding_hidden.shape[0])}."
|
| 1078 |
+
)
|
| 1079 |
+
reconstruction_values = target.reconstruction_values
|
| 1080 |
+
reconstruction_masks = target.reconstruction_masks
|
| 1081 |
+
if reconstruction_values is None or reconstruction_masks is None:
|
| 1082 |
+
raise RuntimeError(
|
| 1083 |
+
"TS understanding reconstruction requires patch-aligned values and masks."
|
| 1084 |
+
)
|
| 1085 |
+
if reconstruction_values.ndim != 3 or int(reconstruction_values.shape[0]) != 1:
|
| 1086 |
+
raise RuntimeError(
|
| 1087 |
+
"TS understanding reconstruction values must be shaped [1,N_patch,patch], got "
|
| 1088 |
+
f"{tuple(reconstruction_values.shape)}."
|
| 1089 |
+
)
|
| 1090 |
+
if reconstruction_masks.shape != reconstruction_values.shape:
|
| 1091 |
+
raise RuntimeError(
|
| 1092 |
+
"TS understanding reconstruction masks must match reconstruction values, got "
|
| 1093 |
+
f"values={tuple(reconstruction_values.shape)}, masks={tuple(reconstruction_masks.shape)}."
|
| 1094 |
+
)
|
| 1095 |
+
patch_count = ts_end - ts_start
|
| 1096 |
+
if int(reconstruction_values.shape[1]) != patch_count:
|
| 1097 |
+
raise RuntimeError(
|
| 1098 |
+
"TS understanding reconstruction patch count must match hidden span length, got "
|
| 1099 |
+
f"values={int(reconstruction_values.shape[1])}, hidden={patch_count}."
|
| 1100 |
+
)
|
| 1101 |
+
valid_mask = torch.logical_not(
|
| 1102 |
+
reconstruction_masks[0].to(
|
| 1103 |
+
device=understanding_hidden.device, dtype=torch.bool
|
| 1104 |
+
)
|
| 1105 |
+
)
|
| 1106 |
+
if not bool(torch.any(valid_mask)):
|
| 1107 |
+
continue
|
| 1108 |
+
hidden_parts.append(understanding_hidden[ts_start:ts_end])
|
| 1109 |
+
value_parts.append(
|
| 1110 |
+
reconstruction_values[0].to(
|
| 1111 |
+
device=understanding_hidden.device, dtype=torch.float32
|
| 1112 |
+
)
|
| 1113 |
+
)
|
| 1114 |
+
mask_parts.append(valid_mask)
|
| 1115 |
+
|
| 1116 |
+
if not hidden_parts:
|
| 1117 |
+
return {
|
| 1118 |
+
"ts_aux_understanding_loss": zero,
|
| 1119 |
+
"ts_aux_understanding_weighted_loss": zero,
|
| 1120 |
+
"ts_aux_understanding_time_loss": zero,
|
| 1121 |
+
"ts_aux_understanding_fft_loss": zero,
|
| 1122 |
+
"ts_aux_understanding_patches": count_zero,
|
| 1123 |
+
"ts_aux_understanding_points": count_zero,
|
| 1124 |
+
"ts_aux_understanding_weight": reference.new_tensor(
|
| 1125 |
+
float(weight), dtype=torch.float32
|
| 1126 |
+
),
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
head_inputs = torch.cat(hidden_parts, dim=0)
|
| 1130 |
+
target_values = torch.cat(value_parts, dim=0)
|
| 1131 |
+
valid_mask = torch.cat(mask_parts, dim=0)
|
| 1132 |
+
pred_values = understanding_head(head_inputs)
|
| 1133 |
+
if pred_values.ndim != 2 or tuple(pred_values.shape) != tuple(target_values.shape):
|
| 1134 |
+
raise RuntimeError(
|
| 1135 |
+
"TS understanding head output must match patch reconstruction targets, got "
|
| 1136 |
+
f"pred={tuple(pred_values.shape)}, target={tuple(target_values.shape)}."
|
| 1137 |
+
)
|
| 1138 |
+
|
| 1139 |
+
pred_f32 = pred_values.to(dtype=torch.float32)
|
| 1140 |
+
target_f32 = target_values.to(dtype=torch.float32)
|
| 1141 |
+
valid_mask_f32 = valid_mask.to(dtype=torch.float32)
|
| 1142 |
+
local_valid_points = valid_mask_f32.sum()
|
| 1143 |
+
global_valid_points = _global_count_tensor(
|
| 1144 |
+
mot_ts_understanding_num_points,
|
| 1145 |
+
reference=pred_f32,
|
| 1146 |
+
label="TS understanding valid-point",
|
| 1147 |
+
)
|
| 1148 |
+
time_denominator = (
|
| 1149 |
+
local_valid_points if global_valid_points is None else global_valid_points
|
| 1150 |
+
)
|
| 1151 |
+
time_loss = (
|
| 1152 |
+
(pred_f32 - target_f32).square() * valid_mask_f32
|
| 1153 |
+
).sum() / time_denominator
|
| 1154 |
+
|
| 1155 |
+
pred_fft = torch.fft.rfft(pred_f32 * valid_mask_f32, dim=-1, norm="ortho")
|
| 1156 |
+
target_fft = torch.fft.rfft(target_f32 * valid_mask_f32, dim=-1, norm="ortho")
|
| 1157 |
+
fft_squared_error = (pred_fft - target_fft).abs().square()
|
| 1158 |
+
global_fft_elements = _global_count_tensor(
|
| 1159 |
+
mot_ts_understanding_num_fft_elements,
|
| 1160 |
+
reference=pred_f32,
|
| 1161 |
+
label="TS understanding FFT-element",
|
| 1162 |
+
)
|
| 1163 |
+
fft_denominator = (
|
| 1164 |
+
fft_squared_error.new_tensor(float(fft_squared_error.numel()))
|
| 1165 |
+
if global_fft_elements is None
|
| 1166 |
+
else global_fft_elements
|
| 1167 |
+
)
|
| 1168 |
+
fft_loss = fft_squared_error.sum() / fft_denominator
|
| 1169 |
+
loss = 0.2 * time_loss + 0.8 * fft_loss
|
| 1170 |
+
weighted_loss = loss.new_tensor(weight) * loss
|
| 1171 |
+
return {
|
| 1172 |
+
"ts_aux_understanding_loss": loss,
|
| 1173 |
+
"ts_aux_understanding_weighted_loss": weighted_loss,
|
| 1174 |
+
"ts_aux_understanding_time_loss": time_loss,
|
| 1175 |
+
"ts_aux_understanding_fft_loss": fft_loss,
|
| 1176 |
+
"ts_aux_understanding_patches": pred_values.new_tensor(
|
| 1177 |
+
float(int(pred_values.shape[0])), dtype=torch.float32
|
| 1178 |
+
),
|
| 1179 |
+
"ts_aux_understanding_points": pred_values.new_tensor(
|
| 1180 |
+
float(valid_mask.sum().item()), dtype=torch.float32
|
| 1181 |
+
),
|
| 1182 |
+
"ts_aux_understanding_weight": pred_values.new_tensor(
|
| 1183 |
+
float(weight), dtype=torch.float32
|
| 1184 |
+
),
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
|
| 1188 |
+
def _sum_flat_loss_parts(parts: List[_FlatForecastLossParts]) -> _FlatForecastLossParts:
|
| 1189 |
+
if not parts:
|
| 1190 |
+
raise RuntimeError("Cannot sum empty TS loss parts.")
|
| 1191 |
+
if len(parts) == 1:
|
| 1192 |
+
return parts[0]
|
| 1193 |
+
values = []
|
| 1194 |
+
for field_idx in range(len(_FlatForecastLossParts._fields)):
|
| 1195 |
+
values.append(sum((part[field_idx] for part in parts[1:]), parts[0][field_idx]))
|
| 1196 |
+
return _FlatForecastLossParts(*values)
|
| 1197 |
+
|
| 1198 |
+
|
| 1199 |
+
def compute_routed_mot_losses(
|
| 1200 |
+
self,
|
| 1201 |
+
mot_runtime: "_MoTRuntime",
|
| 1202 |
+
*,
|
| 1203 |
+
mot_ts_num_records_by_horizon: Optional[object] = None,
|
| 1204 |
+
mot_ts_num_roi_targets_by_horizon: Optional[object] = None,
|
| 1205 |
+
mot_ts_understanding_num_points: Optional[object] = None,
|
| 1206 |
+
mot_ts_understanding_num_fft_elements: Optional[object] = None,
|
| 1207 |
+
) -> Optional[Dict[str, torch.Tensor]]:
|
| 1208 |
+
"""Route TS losses by target type, directly from the packed runtime.
|
| 1209 |
+
|
| 1210 |
+
Contract:
|
| 1211 |
+
- next-patch generation records share one TS record/ROI reducer and the
|
| 1212 |
+
exact GA/DDP denominator bundle
|
| 1213 |
+
- `<ts> -> first patch` and `last patch -> </ts>` boundary routes are removed
|
| 1214 |
+
"""
|
| 1215 |
+
if self.generation_tsfm is None:
|
| 1216 |
+
return None
|
| 1217 |
+
|
| 1218 |
+
understanding_head_losses = _compute_understanding_head_losses(
|
| 1219 |
+
self,
|
| 1220 |
+
mot_runtime=mot_runtime,
|
| 1221 |
+
targets=list(mot_runtime.forecast_targets),
|
| 1222 |
+
mot_ts_understanding_num_points=mot_ts_understanding_num_points,
|
| 1223 |
+
mot_ts_understanding_num_fft_elements=mot_ts_understanding_num_fft_elements,
|
| 1224 |
+
)
|
| 1225 |
+
supervised_targets_all = [
|
| 1226 |
+
target for target in mot_runtime.forecast_targets if target.supervise
|
| 1227 |
+
]
|
| 1228 |
+
if not supervised_targets_all:
|
| 1229 |
+
understanding_weighted = understanding_head_losses.get(
|
| 1230 |
+
"ts_aux_understanding_weighted_loss"
|
| 1231 |
+
)
|
| 1232 |
+
if isinstance(understanding_weighted, torch.Tensor) and bool(
|
| 1233 |
+
torch.any(understanding_weighted != 0)
|
| 1234 |
+
):
|
| 1235 |
+
return _finalize_forecast_routed_losses(
|
| 1236 |
+
self,
|
| 1237 |
+
main_losses=_zero_flat_forecast_loss_parts(understanding_weighted),
|
| 1238 |
+
extra_losses=understanding_head_losses,
|
| 1239 |
+
)
|
| 1240 |
+
return None
|
| 1241 |
+
# Keep conditioning spans out of next-patch loss even for hand-built
|
| 1242 |
+
# runtimes; target spans must use the generation route checked below.
|
| 1243 |
+
generation_next_patch_targets = [
|
| 1244 |
+
target
|
| 1245 |
+
for target in supervised_targets_all
|
| 1246 |
+
if int(target.role_id) == int(ROLE_TARGET)
|
| 1247 |
+
and int(target.ts_route_id) == int(TS_ROUTE_GENERATION)
|
| 1248 |
+
]
|
| 1249 |
+
bad_next_patch_target_routes = [
|
| 1250 |
+
(
|
| 1251 |
+
int(target.sample_idx),
|
| 1252 |
+
int(target.slot_idx),
|
| 1253 |
+
int(target.ts_route_id),
|
| 1254 |
+
)
|
| 1255 |
+
for target in supervised_targets_all
|
| 1256 |
+
if int(target.role_id) == int(ROLE_TARGET)
|
| 1257 |
+
and int(target.ts_route_id) != int(TS_ROUTE_GENERATION)
|
| 1258 |
+
]
|
| 1259 |
+
if bad_next_patch_target_routes:
|
| 1260 |
+
raise RuntimeError(
|
| 1261 |
+
"assistant forecasting target spans must route through the generation TS expert; "
|
| 1262 |
+
f"bad_sample_slot_route={bad_next_patch_target_routes}."
|
| 1263 |
+
)
|
| 1264 |
+
|
| 1265 |
+
loss_parts: List[_FlatForecastLossParts] = []
|
| 1266 |
+
extra_losses: Dict[str, torch.Tensor] = dict(understanding_head_losses)
|
| 1267 |
+
routed_targets = generation_next_patch_targets
|
| 1268 |
+
if routed_targets:
|
| 1269 |
+
pred_quantiles_batched, patch_counts = _predict_mot_span_quantiles_batched(
|
| 1270 |
+
self,
|
| 1271 |
+
mot_runtime=mot_runtime,
|
| 1272 |
+
targets=routed_targets,
|
| 1273 |
+
output_space="normalized",
|
| 1274 |
+
)
|
| 1275 |
+
routed_losses = _compute_flat_forecast_losses_from_predictions(
|
| 1276 |
+
self,
|
| 1277 |
+
pred_quantiles_batched=pred_quantiles_batched,
|
| 1278 |
+
targets=routed_targets,
|
| 1279 |
+
patch_counts=patch_counts,
|
| 1280 |
+
mot_ts_num_records_in_batch=_global_count_for_horizon(
|
| 1281 |
+
mot_ts_num_records_by_horizon,
|
| 1282 |
+
int(self.tsfm_output_patch_len),
|
| 1283 |
+
),
|
| 1284 |
+
mot_ts_num_roi_targets_in_batch=_global_count_for_horizon(
|
| 1285 |
+
mot_ts_num_roi_targets_by_horizon,
|
| 1286 |
+
int(self.tsfm_output_patch_len),
|
| 1287 |
+
),
|
| 1288 |
+
loss_space="normalized",
|
| 1289 |
+
)
|
| 1290 |
+
if routed_losses is not None:
|
| 1291 |
+
loss_parts.append(routed_losses)
|
| 1292 |
+
|
| 1293 |
+
if not loss_parts:
|
| 1294 |
+
understanding_weighted = extra_losses.get("ts_aux_understanding_weighted_loss")
|
| 1295 |
+
if isinstance(understanding_weighted, torch.Tensor) and bool(
|
| 1296 |
+
torch.any(understanding_weighted != 0)
|
| 1297 |
+
):
|
| 1298 |
+
return _finalize_forecast_routed_losses(
|
| 1299 |
+
self,
|
| 1300 |
+
main_losses=_zero_flat_forecast_loss_parts(understanding_weighted),
|
| 1301 |
+
extra_losses=extra_losses,
|
| 1302 |
+
)
|
| 1303 |
+
return None
|
| 1304 |
+
flat_losses = _sum_flat_loss_parts(loss_parts)
|
| 1305 |
+
|
| 1306 |
+
return _finalize_forecast_routed_losses(
|
| 1307 |
+
self,
|
| 1308 |
+
main_losses=flat_losses,
|
| 1309 |
+
extra_losses=extra_losses,
|
| 1310 |
+
)
|
| 1311 |
+
|
| 1312 |
+
|
| 1313 |
+
__all__ = [
|
| 1314 |
+
"compute_routed_mot_losses",
|
| 1315 |
+
"_compute_flat_forecast_losses_from_predictions",
|
| 1316 |
+
"_compute_understanding_head_losses",
|
| 1317 |
+
]
|
timebraid/model/mot/bridge_runtime_mot.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
timebraid/model/mot/config_contract.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TimeBraid Hugging Face config restore contract."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class _Required:
|
| 11 |
+
"""Sentinel for an option the restore contract always supplies."""
|
| 12 |
+
|
| 13 |
+
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
| 14 |
+
return "REQUIRED"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
REQUIRED = _Required()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass(frozen=True, slots=True)
|
| 21 |
+
class MoTOption:
|
| 22 |
+
"""One MoT construction knob, declared once for every boundary it crosses.
|
| 23 |
+
|
| 24 |
+
The same `name` is used in the checkpoint's `config.json`, in the runtime
|
| 25 |
+
option mapping, and in the error messages — so grepping one spelling finds
|
| 26 |
+
every place the knob exists.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
name: str
|
| 30 |
+
kind: str # boolean | integer | number | string | torch_dtype
|
| 31 |
+
default: Any # REQUIRED when the restore contract always supplies it
|
| 32 |
+
persisted: bool # does it live in the checkpoint's config.json
|
| 33 |
+
|
| 34 |
+
@property
|
| 35 |
+
def is_required(self) -> bool:
|
| 36 |
+
return self.default is REQUIRED
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# The single declaration of the MoT option surface.
|
| 40 |
+
#
|
| 41 |
+
# Persisted options are REQUIRED rather than defaulted on purpose. The restore
|
| 42 |
+
# contract always supplies them, so a default could only ever apply to a
|
| 43 |
+
# hand-built option map — and for something like the position lattice that
|
| 44 |
+
# means plausible-but-wrong forecasts instead of an error. Defaults belong
|
| 45 |
+
# only to options no checkpoint carries.
|
| 46 |
+
MOT_OPTIONS: tuple[MoTOption, ...] = (
|
| 47 |
+
# --- persisted: module topology and tokenizer ABI ---
|
| 48 |
+
MoTOption("mot_pairing_mode", "string", REQUIRED, persisted=True),
|
| 49 |
+
MoTOption("mot_t_layers", "integer", REQUIRED, persisted=True),
|
| 50 |
+
MoTOption("mot_understanding_pair_depth", "integer", REQUIRED, persisted=True),
|
| 51 |
+
# The one contract field without the `mot_` prefix. Renaming it would break
|
| 52 |
+
# every checkpoint the training tree has written, so it is left alone until
|
| 53 |
+
# both trees move together.
|
| 54 |
+
MoTOption("has_understanding_head", "boolean", REQUIRED, persisted=True),
|
| 55 |
+
MoTOption("mot_mixed_position_mode", "string", REQUIRED, persisted=True),
|
| 56 |
+
MoTOption("mot_tsfm_num_heads", "integer", REQUIRED, persisted=True),
|
| 57 |
+
MoTOption("mot_tsfm_head_dim", "integer", REQUIRED, persisted=True),
|
| 58 |
+
MoTOption("mot_tsfm_hidden_size", "integer", REQUIRED, persisted=True),
|
| 59 |
+
MoTOption("mot_patch_size", "integer", REQUIRED, persisted=True),
|
| 60 |
+
MoTOption("mot_ts_open_token_id", "integer", REQUIRED, persisted=True),
|
| 61 |
+
MoTOption("mot_ts_close_token_id", "integer", REQUIRED, persisted=True),
|
| 62 |
+
# --- not persisted: supplied per load, or not at all ---
|
| 63 |
+
MoTOption("mot_compute_dtype", "torch_dtype", "float32", persisted=False),
|
| 64 |
+
MoTOption("mot_max_spans_per_sample", "integer", 64, persisted=False),
|
| 65 |
+
MoTOption("mot_ts_gradient_checkpointing", "boolean", False, persisted=False),
|
| 66 |
+
MoTOption(
|
| 67 |
+
"mot_ts_gradient_checkpointing_use_reentrant",
|
| 68 |
+
"boolean",
|
| 69 |
+
False,
|
| 70 |
+
persisted=False,
|
| 71 |
+
),
|
| 72 |
+
# No loader supplies these four; they exist because the loss path is still
|
| 73 |
+
# reachable from the public `runtime_options` argument. See the release
|
| 74 |
+
# audit's "training machinery on the inference path".
|
| 75 |
+
MoTOption("mot_ts_loss_weight", "number", 1.0, persisted=False),
|
| 76 |
+
MoTOption("mot_lm_loss_weight", "number", 1.0, persisted=False),
|
| 77 |
+
MoTOption("mot_ts_roi_mse_alpha", "number", 0.0, persisted=False),
|
| 78 |
+
MoTOption("mot_ts_understanding_loss_weight", "number", 0.0, persisted=False),
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
MOT_OPTION_BY_NAME: dict[str, MoTOption] = {
|
| 82 |
+
option.name: option for option in MOT_OPTIONS
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
# Derived, not restated: the persisted subset is a property of the registry.
|
| 86 |
+
MOT_HF_CONFIG_CONTRACT_FIELDS = tuple(
|
| 87 |
+
option.name for option in MOT_OPTIONS if option.persisted
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def collect_mot_hf_config_contract(config: Any) -> dict[str, object]:
|
| 92 |
+
"""Collect the complete MoT HF restore contract from a config object."""
|
| 93 |
+
is_mapping = isinstance(config, Mapping)
|
| 94 |
+
|
| 95 |
+
def _has(field: str) -> bool:
|
| 96 |
+
return field in config if is_mapping else hasattr(config, field)
|
| 97 |
+
|
| 98 |
+
def _get(field: str) -> object:
|
| 99 |
+
return config.get(field) if is_mapping else getattr(config, field, None)
|
| 100 |
+
|
| 101 |
+
missing = [field for field in MOT_HF_CONFIG_CONTRACT_FIELDS if not _has(field)]
|
| 102 |
+
if missing:
|
| 103 |
+
raise RuntimeError(f"MoT HF config restore metadata is missing {missing}.")
|
| 104 |
+
|
| 105 |
+
contract: dict[str, object] = {}
|
| 106 |
+
for field in MOT_HF_CONFIG_CONTRACT_FIELDS:
|
| 107 |
+
contract[field] = _get(field)
|
| 108 |
+
return validate_mot_hf_config_contract(contract)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def validate_mot_hf_config_contract(
|
| 112 |
+
contract: dict[str, object],
|
| 113 |
+
) -> dict[str, object]:
|
| 114 |
+
"""Validate and canonicalize every persisted MoT restore field."""
|
| 115 |
+
missing = [
|
| 116 |
+
field for field in MOT_HF_CONFIG_CONTRACT_FIELDS if field not in contract
|
| 117 |
+
]
|
| 118 |
+
if missing:
|
| 119 |
+
raise RuntimeError(f"MoT HF config restore metadata is missing {missing}.")
|
| 120 |
+
|
| 121 |
+
validated = dict(contract)
|
| 122 |
+
allowed_strings = {
|
| 123 |
+
"mot_pairing_mode": {"interleaved", "dsfp"},
|
| 124 |
+
"mot_mixed_position_mode": {"span_slot", "patch_slot"},
|
| 125 |
+
}
|
| 126 |
+
for field, allowed in allowed_strings.items():
|
| 127 |
+
value = validated[field]
|
| 128 |
+
if not isinstance(value, str) or value not in allowed:
|
| 129 |
+
raise RuntimeError(
|
| 130 |
+
f"MoT HF config `{field}` must be one of {sorted(allowed)!r}, got {value!r}."
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
positive_integer_fields = (
|
| 134 |
+
"mot_t_layers",
|
| 135 |
+
"mot_tsfm_num_heads",
|
| 136 |
+
"mot_tsfm_head_dim",
|
| 137 |
+
"mot_tsfm_hidden_size",
|
| 138 |
+
"mot_patch_size",
|
| 139 |
+
)
|
| 140 |
+
for field in positive_integer_fields:
|
| 141 |
+
value = validated[field]
|
| 142 |
+
if type(value) is not int or value <= 0:
|
| 143 |
+
raise RuntimeError(
|
| 144 |
+
f"MoT HF config `{field}` must be a positive integer, got {value!r}."
|
| 145 |
+
)
|
| 146 |
+
pair_depth = validated["mot_understanding_pair_depth"]
|
| 147 |
+
if type(pair_depth) is not int or not 0 <= pair_depth <= validated["mot_t_layers"]:
|
| 148 |
+
raise RuntimeError(
|
| 149 |
+
"MoT HF config `mot_understanding_pair_depth` must be an integer in "
|
| 150 |
+
f"[0, mot_t_layers], got {pair_depth!r}."
|
| 151 |
+
)
|
| 152 |
+
has_understanding_head = validated["has_understanding_head"]
|
| 153 |
+
if type(has_understanding_head) is not bool:
|
| 154 |
+
raise RuntimeError(
|
| 155 |
+
"MoT HF config `has_understanding_head` must be a bool, got "
|
| 156 |
+
f"{has_understanding_head!r}."
|
| 157 |
+
)
|
| 158 |
+
if has_understanding_head and pair_depth == 0:
|
| 159 |
+
raise RuntimeError(
|
| 160 |
+
"MoT HF config cannot enable `has_understanding_head` without an "
|
| 161 |
+
"understanding TimesFM tower."
|
| 162 |
+
)
|
| 163 |
+
expected_hidden_size = (
|
| 164 |
+
validated["mot_tsfm_num_heads"] * validated["mot_tsfm_head_dim"]
|
| 165 |
+
)
|
| 166 |
+
if validated["mot_tsfm_hidden_size"] != expected_hidden_size:
|
| 167 |
+
raise RuntimeError(
|
| 168 |
+
"MoT HF config TSFM geometry is inconsistent: "
|
| 169 |
+
f"hidden_size={validated['mot_tsfm_hidden_size']}, "
|
| 170 |
+
f"num_heads*head_dim={expected_hidden_size}."
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
delimiter_ids = []
|
| 174 |
+
for field in ("mot_ts_open_token_id", "mot_ts_close_token_id"):
|
| 175 |
+
value = validated[field]
|
| 176 |
+
if type(value) is not int or value < 0:
|
| 177 |
+
raise RuntimeError(
|
| 178 |
+
f"MoT HF config `{field}` must be a non-negative integer, got {value!r}."
|
| 179 |
+
)
|
| 180 |
+
delimiter_ids.append(value)
|
| 181 |
+
if delimiter_ids[0] == delimiter_ids[1]:
|
| 182 |
+
raise RuntimeError(
|
| 183 |
+
"MoT HF config TS delimiter token IDs must be distinct, got "
|
| 184 |
+
f"{delimiter_ids[0]}."
|
| 185 |
+
)
|
| 186 |
+
return validated
|
timebraid/model/mot/generation_route.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit routing decision for TimeBraid generation.
|
| 2 |
+
|
| 3 |
+
`TimeBraid.generate` accepts ordinary Hugging Face keyword arguments and has to
|
| 4 |
+
decide, from the inputs alone, whether a request wants plain text from the host
|
| 5 |
+
language model or the mixed text/time-series scheduler. That decision used to
|
| 6 |
+
live inline in a five-condition lattice with no name and no way to report
|
| 7 |
+
itself, so a request that fell through to plain text produced text with no
|
| 8 |
+
diagnostic. Everything needed to make that decision is gathered here instead:
|
| 9 |
+
the route is a named value, the reason is a sentence, and both are reachable
|
| 10 |
+
from a test without constructing a model.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
from collections.abc import Mapping, Sequence
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from enum import Enum
|
| 18 |
+
from typing import Optional
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from .structures import TIMEBRAID_REQUIRED_TS_PAYLOAD_FIELDS, TimeBraidPayload
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class FinishReason(str, Enum):
|
| 26 |
+
"""Why the mixed scheduler stopped producing tokens.
|
| 27 |
+
|
| 28 |
+
These two spellings used to be bare string literals at five construction
|
| 29 |
+
sites, with the processor raising on anything it did not recognise — so a
|
| 30 |
+
third reason would have broken decoding at a distance rather than at the
|
| 31 |
+
call site that introduced it.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
TEXT_BUDGET = "text_budget"
|
| 35 |
+
EOS_OR_PROTOCOL_STOP = "eos_or_protocol_stop"
|
| 36 |
+
|
| 37 |
+
# Enum overrides str.__str__, so without this `str(member)` and any
|
| 38 |
+
# f-string yield 'ClassName.MEMBER' instead of the value — which is
|
| 39 |
+
# exactly what a JSON payload or a log line would then carry.
|
| 40 |
+
__str__ = str.__str__
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class GenerationRoute(str, Enum):
|
| 44 |
+
"""Which generation implementation a request resolves to.
|
| 45 |
+
|
| 46 |
+
Inherits from `str` so the value survives JSON serialization and compares
|
| 47 |
+
equal to its own name in rollout records and logs.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
PLAIN_TEXT = "plain_text"
|
| 51 |
+
MIXED_TIMESERIES = "mixed_timeseries"
|
| 52 |
+
|
| 53 |
+
# Enum overrides str.__str__, so without this `str(member)` and any
|
| 54 |
+
# f-string yield 'ClassName.MEMBER' instead of the value — which is
|
| 55 |
+
# exactly what a JSON payload or a log line would then carry.
|
| 56 |
+
__str__ = str.__str__
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@dataclass(frozen=True, slots=True)
|
| 60 |
+
class RouteDecision:
|
| 61 |
+
"""A resolved route together with the reason it was chosen."""
|
| 62 |
+
|
| 63 |
+
route: GenerationRoute
|
| 64 |
+
reason: str
|
| 65 |
+
|
| 66 |
+
@property
|
| 67 |
+
def is_mixed(self) -> bool:
|
| 68 |
+
return self.route is GenerationRoute.MIXED_TIMESERIES
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _contains_ts_delimiter(
|
| 72 |
+
route_input: object, *, ts_open_token_id: int, ts_close_token_id: int
|
| 73 |
+
) -> bool:
|
| 74 |
+
"""Scan a token tensor for either TS delimiter id."""
|
| 75 |
+
if not isinstance(route_input, torch.Tensor):
|
| 76 |
+
return False
|
| 77 |
+
return bool(
|
| 78 |
+
torch.logical_or(
|
| 79 |
+
route_input.eq(int(ts_open_token_id)),
|
| 80 |
+
route_input.eq(int(ts_close_token_id)),
|
| 81 |
+
)
|
| 82 |
+
.any()
|
| 83 |
+
.item()
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def resolve_generation_route(
|
| 88 |
+
*,
|
| 89 |
+
payload: TimeBraidPayload,
|
| 90 |
+
route_input: object,
|
| 91 |
+
target_horizons: Optional[Sequence[int]],
|
| 92 |
+
target_total_lengths: Optional[Sequence[int]],
|
| 93 |
+
ts_open_token_id: int,
|
| 94 |
+
ts_close_token_id: int,
|
| 95 |
+
) -> RouteDecision:
|
| 96 |
+
"""Decide which generation route a request takes.
|
| 97 |
+
|
| 98 |
+
Three independent signals request the mixed scheduler: a tensor-valued TS
|
| 99 |
+
payload, a `<ts>`/`</ts>` delimiter in the prompt, or explicit
|
| 100 |
+
`target_horizons`. Absent all three, the request is plain text.
|
| 101 |
+
|
| 102 |
+
One tolerance is preserved from the original lattice: an explicit all-zero
|
| 103 |
+
`target_horizons` on a text-only batch still produces text, because
|
| 104 |
+
external evaluation harnesses pass `target_horizons=0` to mean "forbid TS
|
| 105 |
+
rollout" rather than "run the TS scheduler with nothing to forecast".
|
| 106 |
+
|
| 107 |
+
`target_total_lengths` is deliberately asymmetric against that tolerance:
|
| 108 |
+
all-zero lengths are an explicit request for the scheduler's empty-target
|
| 109 |
+
validation, so they defeat it. An absent field is *not* all-zero.
|
| 110 |
+
|
| 111 |
+
This is only consulted by the `generate` compatibility shim, which is the
|
| 112 |
+
only entry point that has to infer intent from loose keyword arguments.
|
| 113 |
+
`generate_mixed` states its intent in its signature and validates its
|
| 114 |
+
inputs directly.
|
| 115 |
+
"""
|
| 116 |
+
has_payload = payload.has_runtime_inputs()
|
| 117 |
+
|
| 118 |
+
horizons_given = target_horizons is not None
|
| 119 |
+
horizons_all_zero = horizons_given and all(
|
| 120 |
+
horizon == 0 for horizon in target_horizons
|
| 121 |
+
)
|
| 122 |
+
lengths_all_zero = target_total_lengths is not None and all(
|
| 123 |
+
total == 0 for total in target_total_lengths
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# A positive horizon, or an explicit empty-target request, goes straight to
|
| 127 |
+
# the scheduler; the delimiter scan below is then pure overhead.
|
| 128 |
+
horizons_force_mixed = horizons_given and (
|
| 129 |
+
not horizons_all_zero or lengths_all_zero
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
has_delimiter = False
|
| 133 |
+
if not has_payload and not horizons_force_mixed:
|
| 134 |
+
has_delimiter = _contains_ts_delimiter(
|
| 135 |
+
route_input,
|
| 136 |
+
ts_open_token_id=ts_open_token_id,
|
| 137 |
+
ts_close_token_id=ts_close_token_id,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
horizons_request_mixed = horizons_given
|
| 141 |
+
if (
|
| 142 |
+
horizons_request_mixed
|
| 143 |
+
and horizons_all_zero
|
| 144 |
+
and not lengths_all_zero
|
| 145 |
+
and not has_payload
|
| 146 |
+
and isinstance(route_input, torch.Tensor)
|
| 147 |
+
and not has_delimiter
|
| 148 |
+
):
|
| 149 |
+
horizons_request_mixed = False
|
| 150 |
+
|
| 151 |
+
if has_payload:
|
| 152 |
+
return RouteDecision(
|
| 153 |
+
GenerationRoute.MIXED_TIMESERIES,
|
| 154 |
+
"a tensor-valued TS payload was supplied",
|
| 155 |
+
)
|
| 156 |
+
if has_delimiter:
|
| 157 |
+
return RouteDecision(
|
| 158 |
+
GenerationRoute.MIXED_TIMESERIES,
|
| 159 |
+
"the prompt contains a TS delimiter token",
|
| 160 |
+
)
|
| 161 |
+
if horizons_request_mixed:
|
| 162 |
+
return RouteDecision(
|
| 163 |
+
GenerationRoute.MIXED_TIMESERIES,
|
| 164 |
+
"explicit target_horizons were supplied",
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
absent = []
|
| 168 |
+
if not has_payload:
|
| 169 |
+
absent.append("no TS payload")
|
| 170 |
+
if not has_delimiter:
|
| 171 |
+
absent.append("no TS delimiter token in the prompt")
|
| 172 |
+
if not horizons_given:
|
| 173 |
+
absent.append("no target_horizons")
|
| 174 |
+
elif horizons_all_zero:
|
| 175 |
+
absent.append("target_horizons are all zero")
|
| 176 |
+
return RouteDecision(
|
| 177 |
+
GenerationRoute.PLAIN_TEXT,
|
| 178 |
+
"plain text because " + ", ".join(absent),
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def require_mixed_generation_inputs(inputs: Mapping[str, object]) -> None:
|
| 183 |
+
"""Reject a mapping that cannot drive mixed generation.
|
| 184 |
+
|
| 185 |
+
Validated structurally rather than by type so a test can pass a plain dict
|
| 186 |
+
and so a caller that assembles its own batch is not forced through the
|
| 187 |
+
processor. The error names the missing fields and where they come from,
|
| 188 |
+
because their absence used to route silently to plain text.
|
| 189 |
+
"""
|
| 190 |
+
if not isinstance(inputs, Mapping):
|
| 191 |
+
raise TypeError(
|
| 192 |
+
"generate_mixed requires the mapping returned by "
|
| 193 |
+
"TimeBraidProcessor.apply_chat_template (or an equivalent mapping), "
|
| 194 |
+
f"got {type(inputs).__name__}."
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
if "input_ids" not in inputs or inputs["input_ids"] is None:
|
| 198 |
+
raise ValueError(
|
| 199 |
+
"generate_mixed requires `input_ids`; "
|
| 200 |
+
"TimeBraidProcessor.apply_chat_template supplies it."
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
missing = [
|
| 204 |
+
name
|
| 205 |
+
for name in TIMEBRAID_REQUIRED_TS_PAYLOAD_FIELDS
|
| 206 |
+
if inputs.get(name) is None
|
| 207 |
+
]
|
| 208 |
+
if missing:
|
| 209 |
+
raise ValueError(
|
| 210 |
+
"generate_mixed requires a complete TS payload; missing "
|
| 211 |
+
f"{missing}. Pass `timeseries=` to "
|
| 212 |
+
"TimeBraidProcessor.apply_chat_template, which emits every TS "
|
| 213 |
+
"payload field, or call `generate` for plain text."
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
__all__ = [
|
| 218 |
+
"FinishReason",
|
| 219 |
+
"GenerationRoute",
|
| 220 |
+
"RouteDecision",
|
| 221 |
+
"require_mixed_generation_inputs",
|
| 222 |
+
"resolve_generation_route",
|
| 223 |
+
]
|
timebraid/model/mot/kv_cache.py
ADDED
|
@@ -0,0 +1,1614 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cache state and validation for TimeBraid generation."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
from dataclasses import dataclass, fields, is_dataclass
|
| 7 |
+
from typing import Mapping, Sequence
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from transformers.cache_utils import DynamicCache, DynamicLayer, StaticCache
|
| 11 |
+
|
| 12 |
+
_RequestId = str | int
|
| 13 |
+
_SUPPORTED_CACHE_IMPLEMENTATIONS = {None, "dynamic", "dynamic_full"}
|
| 14 |
+
|
| 15 |
+
# One incremental KV stream registry key: (kind, row, layer, span).
|
| 16 |
+
# kind="llm"/"residual" streams live for the whole request row (span=None,
|
| 17 |
+
# layer is the Qwen layer index). kind="ts" streams live for one generated
|
| 18 |
+
# span (span is the target slot, layer is the TimesFM layer index).
|
| 19 |
+
StreamKey = tuple[str, int, int, int | None]
|
| 20 |
+
_STREAM_KINDS = ("llm", "residual", "ts")
|
| 21 |
+
_STREAM_LIFECYCLE = {"llm": "row", "residual": "row", "ts": "span"}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _validate_stream_key(key: object) -> StreamKey:
|
| 25 |
+
if not isinstance(key, tuple) or len(key) != 4:
|
| 26 |
+
raise TypeError(
|
| 27 |
+
f"Stream key must be a (kind, row, layer, span) tuple, got {key!r}."
|
| 28 |
+
)
|
| 29 |
+
kind, row, layer, span = key
|
| 30 |
+
if kind not in _STREAM_KINDS:
|
| 31 |
+
raise ValueError(f"Stream kind must be one of {_STREAM_KINDS}, got {kind!r}.")
|
| 32 |
+
if isinstance(row, bool) or not isinstance(row, int) or row < 0:
|
| 33 |
+
raise ValueError(f"Stream row must be a non-negative int, got {row!r}.")
|
| 34 |
+
if isinstance(layer, bool) or not isinstance(layer, int) or layer < 0:
|
| 35 |
+
raise ValueError(f"Stream layer must be a non-negative int, got {layer!r}.")
|
| 36 |
+
if _STREAM_LIFECYCLE[kind] == "row":
|
| 37 |
+
if span is not None:
|
| 38 |
+
raise ValueError(
|
| 39 |
+
f"Row-lifetime {kind!r} streams must use span=None, got {span!r}."
|
| 40 |
+
)
|
| 41 |
+
elif isinstance(span, bool) or not isinstance(span, int) or span < 0:
|
| 42 |
+
raise ValueError(
|
| 43 |
+
f"Span-lifetime {kind!r} streams need a non-negative span slot, got {span!r}."
|
| 44 |
+
)
|
| 45 |
+
return key
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _validate_request_id(request_id: object) -> _RequestId:
|
| 49 |
+
if isinstance(request_id, bool) or not isinstance(request_id, (str, int)):
|
| 50 |
+
raise TypeError(
|
| 51 |
+
f"request_id must be str or int, got {type(request_id).__name__}."
|
| 52 |
+
)
|
| 53 |
+
if isinstance(request_id, str) and not request_id:
|
| 54 |
+
raise ValueError("request_id must not be empty.")
|
| 55 |
+
return request_id
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _normalize_request_ids(request_ids: Sequence[_RequestId]) -> tuple[_RequestId, ...]:
|
| 59 |
+
normalized = tuple(_validate_request_id(request_id) for request_id in request_ids)
|
| 60 |
+
if not normalized:
|
| 61 |
+
raise ValueError("MoTDynamicCache requires at least one request_id.")
|
| 62 |
+
if len(set(normalized)) != len(normalized):
|
| 63 |
+
raise ValueError(f"Initial request_ids must be unique, got {normalized}.")
|
| 64 |
+
return normalized
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _update_payload_hash(digest: "hashlib._Hash", value: object) -> None:
|
| 68 |
+
if isinstance(value, torch.Tensor):
|
| 69 |
+
try:
|
| 70 |
+
version = int(value._version)
|
| 71 |
+
except RuntimeError as exc:
|
| 72 |
+
raise RuntimeError(
|
| 73 |
+
"Payload tensors must be created outside torch.inference_mode so in-place mutation is observable."
|
| 74 |
+
) from exc
|
| 75 |
+
tensor_state = (
|
| 76 |
+
"tensor",
|
| 77 |
+
str(value.dtype),
|
| 78 |
+
str(value.device),
|
| 79 |
+
tuple(value.shape),
|
| 80 |
+
tuple(value.stride()),
|
| 81 |
+
int(value.storage_offset()),
|
| 82 |
+
int(value.untyped_storage().data_ptr()) if value.numel() > 0 else 0,
|
| 83 |
+
version,
|
| 84 |
+
)
|
| 85 |
+
digest.update(repr(tensor_state).encode("utf-8"))
|
| 86 |
+
return
|
| 87 |
+
|
| 88 |
+
if value is None or isinstance(value, (bool, int, float, str)):
|
| 89 |
+
digest.update(repr((type(value).__name__, value)).encode("utf-8"))
|
| 90 |
+
return
|
| 91 |
+
|
| 92 |
+
if isinstance(value, Mapping):
|
| 93 |
+
digest.update(b"mapping{")
|
| 94 |
+
for key in sorted(value):
|
| 95 |
+
if not isinstance(key, str):
|
| 96 |
+
raise TypeError(
|
| 97 |
+
f"Payload mapping keys must be strings, got {type(key).__name__}."
|
| 98 |
+
)
|
| 99 |
+
digest.update(key.encode("utf-8"))
|
| 100 |
+
_update_payload_hash(digest, value[key])
|
| 101 |
+
digest.update(b"}")
|
| 102 |
+
return
|
| 103 |
+
|
| 104 |
+
if isinstance(value, (list, tuple)):
|
| 105 |
+
digest.update(f"{type(value).__name__}[".encode("utf-8"))
|
| 106 |
+
for item in value:
|
| 107 |
+
_update_payload_hash(digest, item)
|
| 108 |
+
digest.update(b"]")
|
| 109 |
+
return
|
| 110 |
+
|
| 111 |
+
if is_dataclass(value) and not isinstance(value, type):
|
| 112 |
+
digest.update(f"dataclass:{type(value).__qualname__}{{".encode("utf-8"))
|
| 113 |
+
for field in fields(value):
|
| 114 |
+
digest.update(field.name.encode("utf-8"))
|
| 115 |
+
try:
|
| 116 |
+
_update_payload_hash(digest, getattr(value, field.name))
|
| 117 |
+
except RuntimeError as exc:
|
| 118 |
+
raise RuntimeError(
|
| 119 |
+
f"Payload field `{field.name}` is not fingerprintable: {exc}"
|
| 120 |
+
) from exc
|
| 121 |
+
digest.update(b"}")
|
| 122 |
+
return
|
| 123 |
+
|
| 124 |
+
raise TypeError(f"Unsupported payload fingerprint value: {type(value).__name__}.")
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def fingerprint_payload_state(payload: object) -> str:
|
| 128 |
+
"""Fingerprint tensor identity/version metadata without copying payload values."""
|
| 129 |
+
digest = hashlib.sha256()
|
| 130 |
+
_update_payload_hash(digest, payload)
|
| 131 |
+
return digest.hexdigest()
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def validate_mot_cache_inputs(
|
| 135 |
+
*,
|
| 136 |
+
use_cache: bool | None,
|
| 137 |
+
past_key_values: object | None,
|
| 138 |
+
cache_implementation: str | None = None,
|
| 139 |
+
) -> None:
|
| 140 |
+
"""Validate the supported MoT cache surface before any cache can mutate."""
|
| 141 |
+
if cache_implementation not in _SUPPORTED_CACHE_IMPLEMENTATIONS:
|
| 142 |
+
if cache_implementation == "static" or isinstance(past_key_values, StaticCache):
|
| 143 |
+
raise RuntimeError(
|
| 144 |
+
"MoT KV cache does not support StaticCache: HF static generation replaces the required 2D "
|
| 145 |
+
"layout mask with a compiled 4D mask. Use the dynamic cache implementation."
|
| 146 |
+
)
|
| 147 |
+
raise RuntimeError(
|
| 148 |
+
"MoT KV cache supports cache_implementation in {None, 'dynamic', 'dynamic_full'}, got "
|
| 149 |
+
f"{cache_implementation!r}."
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
if isinstance(past_key_values, StaticCache):
|
| 153 |
+
raise RuntimeError(
|
| 154 |
+
"MoT KV cache does not support StaticCache; use MoTDynamicCache."
|
| 155 |
+
)
|
| 156 |
+
if past_key_values is not None and use_cache is not True:
|
| 157 |
+
raise RuntimeError(
|
| 158 |
+
"past_key_values requires use_cache=True because Qwen mutates a supplied cache even when use_cache=False."
|
| 159 |
+
)
|
| 160 |
+
if use_cache is False and cache_implementation is not None:
|
| 161 |
+
raise RuntimeError("cache_implementation must be unset when use_cache=False.")
|
| 162 |
+
if past_key_values is not None and not isinstance(past_key_values, MoTDynamicCache):
|
| 163 |
+
raise TypeError(
|
| 164 |
+
"MoT continuation requires MoTDynamicCache with runtime sidecars, got "
|
| 165 |
+
f"{type(past_key_values).__name__}."
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def _require_bool_mask(
|
| 170 |
+
mask: torch.Tensor, *, name: str, shape: tuple[int, ...]
|
| 171 |
+
) -> torch.Tensor:
|
| 172 |
+
if not isinstance(mask, torch.Tensor):
|
| 173 |
+
raise TypeError(f"{name} must be a tensor, got {type(mask).__name__}.")
|
| 174 |
+
if mask.dtype is not torch.bool:
|
| 175 |
+
raise TypeError(f"{name} must have dtype torch.bool, got {mask.dtype}.")
|
| 176 |
+
if tuple(mask.shape) != shape:
|
| 177 |
+
raise ValueError(f"{name} must have shape {shape}, got {tuple(mask.shape)}.")
|
| 178 |
+
return mask
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _require_positions(
|
| 182 |
+
positions: torch.Tensor,
|
| 183 |
+
*,
|
| 184 |
+
name: str,
|
| 185 |
+
shape: tuple[int, ...],
|
| 186 |
+
device: torch.device,
|
| 187 |
+
) -> torch.Tensor:
|
| 188 |
+
if not isinstance(positions, torch.Tensor):
|
| 189 |
+
raise TypeError(f"{name} must be a tensor, got {type(positions).__name__}.")
|
| 190 |
+
if positions.dtype is not torch.long:
|
| 191 |
+
raise TypeError(f"{name} must have dtype torch.long, got {positions.dtype}.")
|
| 192 |
+
if tuple(positions.shape) != shape:
|
| 193 |
+
raise ValueError(
|
| 194 |
+
f"{name} must have shape {shape}, got {tuple(positions.shape)}."
|
| 195 |
+
)
|
| 196 |
+
if positions.device != device:
|
| 197 |
+
raise ValueError(f"{name} must be on {device}, got {positions.device}.")
|
| 198 |
+
return positions
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _expanded_positions(
|
| 202 |
+
positions: torch.Tensor,
|
| 203 |
+
*,
|
| 204 |
+
batch_size: int,
|
| 205 |
+
token_count: int,
|
| 206 |
+
name: str,
|
| 207 |
+
device: torch.device,
|
| 208 |
+
) -> torch.Tensor:
|
| 209 |
+
if not isinstance(positions, torch.Tensor):
|
| 210 |
+
raise TypeError(f"{name} must be a tensor, got {type(positions).__name__}.")
|
| 211 |
+
if positions.dtype is not torch.long:
|
| 212 |
+
raise TypeError(f"{name} must have dtype torch.long, got {positions.dtype}.")
|
| 213 |
+
if positions.device != device:
|
| 214 |
+
raise ValueError(f"{name} must be on {device}, got {positions.device}.")
|
| 215 |
+
if tuple(positions.shape) == (token_count,):
|
| 216 |
+
return positions.view(1, token_count).expand(batch_size, token_count)
|
| 217 |
+
if tuple(positions.shape) == (batch_size, token_count):
|
| 218 |
+
return positions
|
| 219 |
+
raise ValueError(
|
| 220 |
+
f"{name} must have shape {(token_count,)} or {(batch_size, token_count)}, got {tuple(positions.shape)}."
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _geometric_capacity(current: int, required: int, initial: int) -> int:
|
| 225 |
+
if required <= current:
|
| 226 |
+
return current
|
| 227 |
+
capacity = max(1, current, initial)
|
| 228 |
+
while capacity < required:
|
| 229 |
+
capacity *= 2
|
| 230 |
+
return capacity
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
@dataclass(frozen=True)
|
| 234 |
+
class MoTSidecarView:
|
| 235 |
+
"""Logical view over one named per-layer sidecar stream."""
|
| 236 |
+
|
| 237 |
+
keys: torch.Tensor
|
| 238 |
+
values: torch.Tensor
|
| 239 |
+
valid_mask: torch.Tensor
|
| 240 |
+
physical_positions: torch.Tensor
|
| 241 |
+
logical_positions: torch.Tensor
|
| 242 |
+
rope_positions: torch.Tensor
|
| 243 |
+
capacity: int
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
class _PositionBuffer:
|
| 247 |
+
def __init__(self, *, batch_size: int, initial_capacity: int) -> None:
|
| 248 |
+
self.batch_size = batch_size
|
| 249 |
+
self.initial_capacity = initial_capacity
|
| 250 |
+
self.length = 0
|
| 251 |
+
self.capacity = 0
|
| 252 |
+
self.valid_mask: torch.Tensor | None = None
|
| 253 |
+
self.logical_positions: torch.Tensor | None = None
|
| 254 |
+
self.rope_positions: torch.Tensor | None = None
|
| 255 |
+
|
| 256 |
+
@property
|
| 257 |
+
def device(self) -> torch.device | None:
|
| 258 |
+
return None if self.valid_mask is None else self.valid_mask.device
|
| 259 |
+
|
| 260 |
+
def _ensure_capacity(self, required: int, *, device: torch.device) -> None:
|
| 261 |
+
target = _geometric_capacity(self.capacity, required, self.initial_capacity)
|
| 262 |
+
if target == self.capacity:
|
| 263 |
+
if self.device != device:
|
| 264 |
+
raise ValueError(
|
| 265 |
+
f"Position metadata is on {self.device}, got append on {device}."
|
| 266 |
+
)
|
| 267 |
+
return
|
| 268 |
+
|
| 269 |
+
valid = torch.zeros((self.batch_size, target), dtype=torch.bool, device=device)
|
| 270 |
+
logical = torch.full(
|
| 271 |
+
(self.batch_size, target), -1, dtype=torch.long, device=device
|
| 272 |
+
)
|
| 273 |
+
rope = torch.full(
|
| 274 |
+
(self.batch_size, target), -1, dtype=torch.long, device=device
|
| 275 |
+
)
|
| 276 |
+
if self.length > 0:
|
| 277 |
+
if self.device != device:
|
| 278 |
+
raise ValueError(
|
| 279 |
+
f"Position metadata is on {self.device}, got append on {device}."
|
| 280 |
+
)
|
| 281 |
+
valid[:, : self.length].copy_(self.valid_mask[:, : self.length])
|
| 282 |
+
logical[:, : self.length].copy_(self.logical_positions[:, : self.length])
|
| 283 |
+
rope[:, : self.length].copy_(self.rope_positions[:, : self.length])
|
| 284 |
+
self.valid_mask = valid
|
| 285 |
+
self.logical_positions = logical
|
| 286 |
+
self.rope_positions = rope
|
| 287 |
+
self.capacity = target
|
| 288 |
+
|
| 289 |
+
def append(
|
| 290 |
+
self,
|
| 291 |
+
*,
|
| 292 |
+
valid_mask: torch.Tensor,
|
| 293 |
+
logical_positions: torch.Tensor,
|
| 294 |
+
rope_positions: torch.Tensor,
|
| 295 |
+
) -> None:
|
| 296 |
+
token_count = int(valid_mask.shape[1])
|
| 297 |
+
required = self.length + token_count
|
| 298 |
+
self._ensure_capacity(required, device=valid_mask.device)
|
| 299 |
+
start, end = self.length, required
|
| 300 |
+
self.valid_mask[:, start:end].copy_(valid_mask)
|
| 301 |
+
self.logical_positions[:, start:end].copy_(logical_positions)
|
| 302 |
+
self.rope_positions[:, start:end].copy_(rope_positions)
|
| 303 |
+
invalid = ~valid_mask
|
| 304 |
+
self.logical_positions[:, start:end].masked_fill_(invalid, -1)
|
| 305 |
+
self.rope_positions[:, start:end].masked_fill_(invalid, -1)
|
| 306 |
+
self.length = required
|
| 307 |
+
|
| 308 |
+
def crop(self, target_length: int) -> None:
|
| 309 |
+
self.length = min(self.length, target_length)
|
| 310 |
+
if self.capacity == 0:
|
| 311 |
+
return
|
| 312 |
+
self.valid_mask[:, self.length :].zero_()
|
| 313 |
+
self.logical_positions[:, self.length :].fill_(-1)
|
| 314 |
+
self.rope_positions[:, self.length :].fill_(-1)
|
| 315 |
+
|
| 316 |
+
def reset(self) -> None:
|
| 317 |
+
self.length = 0
|
| 318 |
+
if self.capacity == 0:
|
| 319 |
+
return
|
| 320 |
+
self.valid_mask.zero_()
|
| 321 |
+
self.logical_positions.fill_(-1)
|
| 322 |
+
self.rope_positions.fill_(-1)
|
| 323 |
+
|
| 324 |
+
def index_select(self, indices: torch.LongTensor) -> None:
|
| 325 |
+
if self.capacity == 0:
|
| 326 |
+
self.batch_size = int(indices.numel())
|
| 327 |
+
return
|
| 328 |
+
local_indices = indices.to(device=self.valid_mask.device)
|
| 329 |
+
self.valid_mask = self.valid_mask.index_select(0, local_indices)
|
| 330 |
+
self.logical_positions = self.logical_positions.index_select(0, local_indices)
|
| 331 |
+
self.rope_positions = self.rope_positions.index_select(0, local_indices)
|
| 332 |
+
self.batch_size = int(indices.numel())
|
| 333 |
+
|
| 334 |
+
def repeat_interleave(self, repeats: int) -> None:
|
| 335 |
+
if self.capacity > 0:
|
| 336 |
+
self.valid_mask = self.valid_mask.repeat_interleave(repeats, dim=0)
|
| 337 |
+
self.logical_positions = self.logical_positions.repeat_interleave(
|
| 338 |
+
repeats, dim=0
|
| 339 |
+
)
|
| 340 |
+
self.rope_positions = self.rope_positions.repeat_interleave(repeats, dim=0)
|
| 341 |
+
self.batch_size *= repeats
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
class _SidecarBuffer:
|
| 345 |
+
# Sidecar K/V uses packed-runtime order [B, capacity, H, D]. The native HF
|
| 346 |
+
# cache remains compact Qwen order [B, H_kv, T, D].
|
| 347 |
+
def __init__(self, *, batch_size: int, initial_capacity: int) -> None:
|
| 348 |
+
self.batch_size = batch_size
|
| 349 |
+
self.initial_capacity = initial_capacity
|
| 350 |
+
self.length = 0
|
| 351 |
+
self.capacity = 0
|
| 352 |
+
self.keys: torch.Tensor | None = None
|
| 353 |
+
self.values: torch.Tensor | None = None
|
| 354 |
+
self.valid_mask: torch.Tensor | None = None
|
| 355 |
+
self.physical_positions: torch.Tensor | None = None
|
| 356 |
+
self.logical_positions: torch.Tensor | None = None
|
| 357 |
+
self.rope_positions: torch.Tensor | None = None
|
| 358 |
+
|
| 359 |
+
def _ensure_capacity(self, required: int, *, reference: torch.Tensor) -> None:
|
| 360 |
+
target = _geometric_capacity(self.capacity, required, self.initial_capacity)
|
| 361 |
+
if target == self.capacity:
|
| 362 |
+
if (
|
| 363 |
+
self.keys.device != reference.device
|
| 364 |
+
or self.keys.dtype != reference.dtype
|
| 365 |
+
):
|
| 366 |
+
raise ValueError(
|
| 367 |
+
"Sidecar append dtype/device changed: "
|
| 368 |
+
f"expected {self.keys.dtype} on {self.keys.device}, got {reference.dtype} on {reference.device}."
|
| 369 |
+
)
|
| 370 |
+
return
|
| 371 |
+
|
| 372 |
+
_, _, num_heads, head_dim = reference.shape
|
| 373 |
+
keys = torch.zeros(
|
| 374 |
+
(self.batch_size, target, num_heads, head_dim),
|
| 375 |
+
dtype=reference.dtype,
|
| 376 |
+
device=reference.device,
|
| 377 |
+
)
|
| 378 |
+
values = torch.zeros_like(keys)
|
| 379 |
+
valid = torch.zeros(
|
| 380 |
+
(self.batch_size, target), dtype=torch.bool, device=reference.device
|
| 381 |
+
)
|
| 382 |
+
physical = torch.full(
|
| 383 |
+
(self.batch_size, target), -1, dtype=torch.long, device=reference.device
|
| 384 |
+
)
|
| 385 |
+
logical = torch.full_like(physical, -1)
|
| 386 |
+
rope = torch.full_like(physical, -1)
|
| 387 |
+
if self.length > 0:
|
| 388 |
+
if (
|
| 389 |
+
self.keys.device != reference.device
|
| 390 |
+
or self.keys.dtype != reference.dtype
|
| 391 |
+
):
|
| 392 |
+
raise ValueError(
|
| 393 |
+
"Sidecar append dtype/device changed: "
|
| 394 |
+
f"expected {self.keys.dtype} on {self.keys.device}, got {reference.dtype} on {reference.device}."
|
| 395 |
+
)
|
| 396 |
+
if tuple(self.keys.shape[2:]) != (num_heads, head_dim):
|
| 397 |
+
raise ValueError(
|
| 398 |
+
"Sidecar head shape changed: "
|
| 399 |
+
f"expected {tuple(self.keys.shape[2:])}, got {(num_heads, head_dim)}."
|
| 400 |
+
)
|
| 401 |
+
keys[:, : self.length].copy_(self.keys[:, : self.length])
|
| 402 |
+
values[:, : self.length].copy_(self.values[:, : self.length])
|
| 403 |
+
valid[:, : self.length].copy_(self.valid_mask[:, : self.length])
|
| 404 |
+
physical[:, : self.length].copy_(self.physical_positions[:, : self.length])
|
| 405 |
+
logical[:, : self.length].copy_(self.logical_positions[:, : self.length])
|
| 406 |
+
rope[:, : self.length].copy_(self.rope_positions[:, : self.length])
|
| 407 |
+
self.keys = keys
|
| 408 |
+
self.values = values
|
| 409 |
+
self.valid_mask = valid
|
| 410 |
+
self.physical_positions = physical
|
| 411 |
+
self.logical_positions = logical
|
| 412 |
+
self.rope_positions = rope
|
| 413 |
+
self.capacity = target
|
| 414 |
+
|
| 415 |
+
def append(
|
| 416 |
+
self,
|
| 417 |
+
*,
|
| 418 |
+
keys: torch.Tensor,
|
| 419 |
+
values: torch.Tensor,
|
| 420 |
+
valid_mask: torch.Tensor,
|
| 421 |
+
physical_positions: torch.Tensor,
|
| 422 |
+
logical_positions: torch.Tensor,
|
| 423 |
+
rope_positions: torch.Tensor,
|
| 424 |
+
) -> None:
|
| 425 |
+
required = self.length + int(keys.shape[1])
|
| 426 |
+
self._ensure_capacity(required, reference=keys)
|
| 427 |
+
if tuple(self.keys.shape[2:]) != tuple(keys.shape[2:]):
|
| 428 |
+
raise ValueError(
|
| 429 |
+
f"Sidecar head shape changed: expected {tuple(self.keys.shape[2:])}, got {tuple(keys.shape[2:])}."
|
| 430 |
+
)
|
| 431 |
+
start, end = self.length, required
|
| 432 |
+
self.keys[:, start:end].copy_(keys)
|
| 433 |
+
self.values[:, start:end].copy_(values)
|
| 434 |
+
self.valid_mask[:, start:end].copy_(valid_mask)
|
| 435 |
+
self.physical_positions[:, start:end].copy_(physical_positions)
|
| 436 |
+
self.logical_positions[:, start:end].copy_(logical_positions)
|
| 437 |
+
self.rope_positions[:, start:end].copy_(rope_positions)
|
| 438 |
+
invalid = ~valid_mask
|
| 439 |
+
expanded_invalid = invalid[:, :, None, None]
|
| 440 |
+
self.keys[:, start:end].masked_fill_(expanded_invalid, 0)
|
| 441 |
+
self.values[:, start:end].masked_fill_(expanded_invalid, 0)
|
| 442 |
+
self.physical_positions[:, start:end].masked_fill_(invalid, -1)
|
| 443 |
+
self.logical_positions[:, start:end].masked_fill_(invalid, -1)
|
| 444 |
+
self.rope_positions[:, start:end].masked_fill_(invalid, -1)
|
| 445 |
+
self.length = required
|
| 446 |
+
|
| 447 |
+
def view(self) -> MoTSidecarView:
|
| 448 |
+
if self.capacity == 0:
|
| 449 |
+
raise RuntimeError("Sidecar buffer is not initialized.")
|
| 450 |
+
return MoTSidecarView(
|
| 451 |
+
keys=self.keys[:, : self.length],
|
| 452 |
+
values=self.values[:, : self.length],
|
| 453 |
+
valid_mask=self.valid_mask[:, : self.length],
|
| 454 |
+
physical_positions=self.physical_positions[:, : self.length],
|
| 455 |
+
logical_positions=self.logical_positions[:, : self.length],
|
| 456 |
+
rope_positions=self.rope_positions[:, : self.length],
|
| 457 |
+
capacity=self.capacity,
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
def crop_physical(self, target_length: int) -> None:
|
| 461 |
+
if self.capacity == 0:
|
| 462 |
+
return
|
| 463 |
+
remove = self.valid_mask[:, : self.length] & self.physical_positions[
|
| 464 |
+
:, : self.length
|
| 465 |
+
].ge(target_length)
|
| 466 |
+
if torch.any(remove):
|
| 467 |
+
expanded_remove = remove[:, :, None, None]
|
| 468 |
+
self.keys[:, : self.length].masked_fill_(expanded_remove, 0)
|
| 469 |
+
self.values[:, : self.length].masked_fill_(expanded_remove, 0)
|
| 470 |
+
self.valid_mask[:, : self.length].masked_fill_(remove, False)
|
| 471 |
+
self.physical_positions[:, : self.length].masked_fill_(remove, -1)
|
| 472 |
+
self.logical_positions[:, : self.length].masked_fill_(remove, -1)
|
| 473 |
+
self.rope_positions[:, : self.length].masked_fill_(remove, -1)
|
| 474 |
+
while self.length > 0 and not bool(
|
| 475 |
+
torch.any(self.valid_mask[:, self.length - 1]).item()
|
| 476 |
+
):
|
| 477 |
+
self.length -= 1
|
| 478 |
+
|
| 479 |
+
def truncate(self, target_length: int) -> None:
|
| 480 |
+
if target_length < 0 or target_length > self.length:
|
| 481 |
+
raise ValueError(
|
| 482 |
+
f"Sidecar target_length must be in [0, {self.length}], got {target_length}."
|
| 483 |
+
)
|
| 484 |
+
if target_length == self.length:
|
| 485 |
+
return
|
| 486 |
+
self.keys[:, target_length : self.length].zero_()
|
| 487 |
+
self.values[:, target_length : self.length].zero_()
|
| 488 |
+
self.valid_mask[:, target_length : self.length].zero_()
|
| 489 |
+
self.physical_positions[:, target_length : self.length].fill_(-1)
|
| 490 |
+
self.logical_positions[:, target_length : self.length].fill_(-1)
|
| 491 |
+
self.rope_positions[:, target_length : self.length].fill_(-1)
|
| 492 |
+
self.length = target_length
|
| 493 |
+
|
| 494 |
+
def reset(self) -> None:
|
| 495 |
+
self.length = 0
|
| 496 |
+
if self.capacity == 0:
|
| 497 |
+
return
|
| 498 |
+
self.keys.zero_()
|
| 499 |
+
self.values.zero_()
|
| 500 |
+
self.valid_mask.zero_()
|
| 501 |
+
self.physical_positions.fill_(-1)
|
| 502 |
+
self.logical_positions.fill_(-1)
|
| 503 |
+
self.rope_positions.fill_(-1)
|
| 504 |
+
|
| 505 |
+
def index_select(self, indices: torch.LongTensor) -> None:
|
| 506 |
+
if self.capacity == 0:
|
| 507 |
+
self.batch_size = int(indices.numel())
|
| 508 |
+
return
|
| 509 |
+
local_indices = indices.to(device=self.keys.device)
|
| 510 |
+
self.keys = self.keys.index_select(0, local_indices)
|
| 511 |
+
self.values = self.values.index_select(0, local_indices)
|
| 512 |
+
self.valid_mask = self.valid_mask.index_select(0, local_indices)
|
| 513 |
+
self.physical_positions = self.physical_positions.index_select(0, local_indices)
|
| 514 |
+
self.logical_positions = self.logical_positions.index_select(0, local_indices)
|
| 515 |
+
self.rope_positions = self.rope_positions.index_select(0, local_indices)
|
| 516 |
+
self.batch_size = int(indices.numel())
|
| 517 |
+
|
| 518 |
+
def repeat_interleave(self, repeats: int) -> None:
|
| 519 |
+
if self.capacity > 0:
|
| 520 |
+
self.keys = self.keys.repeat_interleave(repeats, dim=0)
|
| 521 |
+
self.values = self.values.repeat_interleave(repeats, dim=0)
|
| 522 |
+
self.valid_mask = self.valid_mask.repeat_interleave(repeats, dim=0)
|
| 523 |
+
self.physical_positions = self.physical_positions.repeat_interleave(
|
| 524 |
+
repeats, dim=0
|
| 525 |
+
)
|
| 526 |
+
self.logical_positions = self.logical_positions.repeat_interleave(
|
| 527 |
+
repeats, dim=0
|
| 528 |
+
)
|
| 529 |
+
self.rope_positions = self.rope_positions.repeat_interleave(repeats, dim=0)
|
| 530 |
+
self.batch_size *= repeats
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
class _GeometricDynamicLayer(DynamicLayer):
|
| 534 |
+
"""HF-compatible dynamic layer whose appends reuse geometrically grown storage."""
|
| 535 |
+
|
| 536 |
+
def __init__(self, *, initial_capacity: int) -> None:
|
| 537 |
+
super().__init__()
|
| 538 |
+
self._initial_capacity = int(initial_capacity)
|
| 539 |
+
self._length = 0
|
| 540 |
+
self._capacity = 0
|
| 541 |
+
self._key_storage: torch.Tensor | None = None
|
| 542 |
+
self._value_storage: torch.Tensor | None = None
|
| 543 |
+
|
| 544 |
+
@property
|
| 545 |
+
def capacity(self) -> int:
|
| 546 |
+
return self._capacity
|
| 547 |
+
|
| 548 |
+
def lazy_initialization(self, key_states: torch.Tensor) -> None:
|
| 549 |
+
self.dtype = key_states.dtype
|
| 550 |
+
self.device = key_states.device
|
| 551 |
+
self.keys = key_states.new_empty(
|
| 552 |
+
(*key_states.shape[:-2], 0, key_states.shape[-1])
|
| 553 |
+
)
|
| 554 |
+
self.values = self.keys.clone()
|
| 555 |
+
self.is_initialized = True
|
| 556 |
+
|
| 557 |
+
def _refresh_views(self) -> None:
|
| 558 |
+
if self._key_storage is None or self._value_storage is None:
|
| 559 |
+
return
|
| 560 |
+
self.keys = self._key_storage[..., : self._length, :]
|
| 561 |
+
self.values = self._value_storage[..., : self._length, :]
|
| 562 |
+
|
| 563 |
+
def _ensure_capacity(self, key_states: torch.Tensor, required: int) -> None:
|
| 564 |
+
if required <= self._capacity:
|
| 565 |
+
return
|
| 566 |
+
if self._capacity == 0:
|
| 567 |
+
capacity = max(
|
| 568 |
+
self._initial_capacity,
|
| 569 |
+
required + max(64, required // 8),
|
| 570 |
+
)
|
| 571 |
+
else:
|
| 572 |
+
capacity = max(required, self._capacity * 2)
|
| 573 |
+
storage_shape = (*key_states.shape[:-2], capacity, key_states.shape[-1])
|
| 574 |
+
key_storage = key_states.new_empty(storage_shape)
|
| 575 |
+
value_storage = key_states.new_empty(storage_shape)
|
| 576 |
+
if self._length > 0:
|
| 577 |
+
key_storage[..., : self._length, :].copy_(
|
| 578 |
+
self._key_storage[..., : self._length, :]
|
| 579 |
+
)
|
| 580 |
+
value_storage[..., : self._length, :].copy_(
|
| 581 |
+
self._value_storage[..., : self._length, :]
|
| 582 |
+
)
|
| 583 |
+
self._key_storage = key_storage
|
| 584 |
+
self._value_storage = value_storage
|
| 585 |
+
self._capacity = capacity
|
| 586 |
+
|
| 587 |
+
def update(
|
| 588 |
+
self,
|
| 589 |
+
key_states: torch.Tensor,
|
| 590 |
+
value_states: torch.Tensor,
|
| 591 |
+
cache_kwargs: Mapping[str, object] | None = None,
|
| 592 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 593 |
+
del cache_kwargs
|
| 594 |
+
if not self.is_initialized:
|
| 595 |
+
self.lazy_initialization(key_states)
|
| 596 |
+
if tuple(key_states.shape) != tuple(value_states.shape):
|
| 597 |
+
raise ValueError(
|
| 598 |
+
f"Native layer K/V shape mismatch: keys={tuple(key_states.shape)}, values={tuple(value_states.shape)}."
|
| 599 |
+
)
|
| 600 |
+
if self._length > 0 and (
|
| 601 |
+
key_states.dtype != self._key_storage.dtype
|
| 602 |
+
or key_states.device != self._key_storage.device
|
| 603 |
+
or tuple(key_states.shape[:-2]) != tuple(self._key_storage.shape[:-2])
|
| 604 |
+
or int(key_states.shape[-1]) != int(self._key_storage.shape[-1])
|
| 605 |
+
):
|
| 606 |
+
raise ValueError(
|
| 607 |
+
"Native layer append dtype, device, batch, head, and head_dim must stay fixed."
|
| 608 |
+
)
|
| 609 |
+
token_count = int(key_states.shape[-2])
|
| 610 |
+
required = self._length + token_count
|
| 611 |
+
self._ensure_capacity(key_states, required)
|
| 612 |
+
self._key_storage[..., self._length : required, :].copy_(key_states)
|
| 613 |
+
self._value_storage[..., self._length : required, :].copy_(value_states)
|
| 614 |
+
self._length = required
|
| 615 |
+
self._refresh_views()
|
| 616 |
+
return self.keys, self.values
|
| 617 |
+
|
| 618 |
+
def get_seq_length(self) -> int:
|
| 619 |
+
return self._length
|
| 620 |
+
|
| 621 |
+
def crop(self, max_length: int) -> None:
|
| 622 |
+
if max_length < 0:
|
| 623 |
+
max_length = self._length - abs(max_length)
|
| 624 |
+
self._length = max(0, min(self._length, int(max_length)))
|
| 625 |
+
self._refresh_views()
|
| 626 |
+
|
| 627 |
+
def reset(self) -> None:
|
| 628 |
+
if self._length > 0:
|
| 629 |
+
self._key_storage[..., : self._length, :].zero_()
|
| 630 |
+
self._value_storage[..., : self._length, :].zero_()
|
| 631 |
+
self._length = 0
|
| 632 |
+
self._refresh_views()
|
| 633 |
+
|
| 634 |
+
def _select_batch(self, indices: torch.LongTensor) -> None:
|
| 635 |
+
if self._capacity == 0:
|
| 636 |
+
return
|
| 637 |
+
local_indices = indices.to(device=self._key_storage.device)
|
| 638 |
+
self._key_storage = self._key_storage.index_select(0, local_indices)
|
| 639 |
+
self._value_storage = self._value_storage.index_select(0, local_indices)
|
| 640 |
+
self._refresh_views()
|
| 641 |
+
|
| 642 |
+
def reorder_cache(self, beam_idx: torch.LongTensor) -> None:
|
| 643 |
+
self._select_batch(beam_idx)
|
| 644 |
+
|
| 645 |
+
def batch_select_indices(self, indices: torch.Tensor) -> None:
|
| 646 |
+
self._select_batch(indices.to(dtype=torch.long))
|
| 647 |
+
|
| 648 |
+
def batch_repeat_interleave(self, repeats: int) -> None:
|
| 649 |
+
if self._capacity > 0:
|
| 650 |
+
self._key_storage = self._key_storage.repeat_interleave(repeats, dim=0)
|
| 651 |
+
self._value_storage = self._value_storage.repeat_interleave(repeats, dim=0)
|
| 652 |
+
self._refresh_views()
|
| 653 |
+
|
| 654 |
+
def offload(self) -> None:
|
| 655 |
+
if self._capacity > 0:
|
| 656 |
+
self._key_storage = self._key_storage.to("cpu", non_blocking=True)
|
| 657 |
+
self._value_storage = self._value_storage.to("cpu", non_blocking=True)
|
| 658 |
+
self._refresh_views()
|
| 659 |
+
|
| 660 |
+
def prefetch(self) -> None:
|
| 661 |
+
if self._capacity > 0 and self._key_storage.device != self.device:
|
| 662 |
+
self._key_storage = self._key_storage.to(self.device, non_blocking=True)
|
| 663 |
+
self._value_storage = self._value_storage.to(self.device, non_blocking=True)
|
| 664 |
+
self._refresh_views()
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
class MoTDynamicCache(DynamicCache):
|
| 668 |
+
"""HF dynamic cache plus batch-stable MoT runtime sidecars."""
|
| 669 |
+
|
| 670 |
+
is_mot_generation_cache = True
|
| 671 |
+
|
| 672 |
+
def __init__(
|
| 673 |
+
self,
|
| 674 |
+
*,
|
| 675 |
+
request_ids: Sequence[_RequestId],
|
| 676 |
+
config=None,
|
| 677 |
+
initial_sidecar_capacity: int = 16,
|
| 678 |
+
offloading: bool = False,
|
| 679 |
+
offload_only_non_sliding: bool = False,
|
| 680 |
+
) -> None:
|
| 681 |
+
if initial_sidecar_capacity <= 0:
|
| 682 |
+
raise ValueError(
|
| 683 |
+
f"initial_sidecar_capacity must be positive, got {initial_sidecar_capacity}."
|
| 684 |
+
)
|
| 685 |
+
if offloading or offload_only_non_sliding:
|
| 686 |
+
raise RuntimeError(
|
| 687 |
+
"MoTDynamicCache does not support offloading because runtime sidecars must remain colocated."
|
| 688 |
+
)
|
| 689 |
+
super().__init__(
|
| 690 |
+
config=config,
|
| 691 |
+
offloading=False,
|
| 692 |
+
offload_only_non_sliding=False,
|
| 693 |
+
)
|
| 694 |
+
if any(bool(getattr(layer, "is_sliding", False)) for layer in self.layers):
|
| 695 |
+
raise RuntimeError(
|
| 696 |
+
"MoTDynamicCache does not support sliding-window decoder layers."
|
| 697 |
+
)
|
| 698 |
+
self.layers = [
|
| 699 |
+
_GeometricDynamicLayer(initial_capacity=initial_sidecar_capacity)
|
| 700 |
+
for _layer in self.layers
|
| 701 |
+
]
|
| 702 |
+
# Config-less caches are still supported: `update` appends the same
|
| 703 |
+
# geometric layer type instead of DynamicCache's concatenating default.
|
| 704 |
+
self.layer_class_to_replicate = None
|
| 705 |
+
self._request_ids = _normalize_request_ids(request_ids)
|
| 706 |
+
self._batch_size = len(self._request_ids)
|
| 707 |
+
self._initial_sidecar_capacity = int(initial_sidecar_capacity)
|
| 708 |
+
self._position_buffer = _PositionBuffer(
|
| 709 |
+
batch_size=self._batch_size,
|
| 710 |
+
initial_capacity=self._initial_sidecar_capacity,
|
| 711 |
+
)
|
| 712 |
+
self._sidecars: dict[str, dict[int, _SidecarBuffer]] = {}
|
| 713 |
+
self._physical_length = 0
|
| 714 |
+
self._row_logical_next_positions = torch.zeros(
|
| 715 |
+
self._batch_size, dtype=torch.long
|
| 716 |
+
)
|
| 717 |
+
self._row_rope_next_positions = torch.zeros(self._batch_size, dtype=torch.long)
|
| 718 |
+
self._payload_fingerprints: tuple[str, ...] | None = None
|
| 719 |
+
self._payload_epochs = torch.zeros(self._batch_size, dtype=torch.long)
|
| 720 |
+
self._config = (
|
| 721 |
+
config.get_text_config(decoder=True) if config is not None else None
|
| 722 |
+
)
|
| 723 |
+
# Cached mixed generation owns these request-local values. Declaring the
|
| 724 |
+
# complete schema here keeps cache lifetime and reset behavior explicit.
|
| 725 |
+
# `_streams` is the single registry for every incremental KV stream,
|
| 726 |
+
# keyed by (kind, row, layer, span):
|
| 727 |
+
# kind="llm"/"residual": layer is the Qwen layer index, span is None,
|
| 728 |
+
# and the stream lives for the whole request row.
|
| 729 |
+
# kind="ts": layer is the TimesFM layer index, span is the target
|
| 730 |
+
# slot, and the stream lives for one generated span.
|
| 731 |
+
# `None` means no incremental runtime streams are currently bound.
|
| 732 |
+
self._streams: dict[StreamKey, dict[str, object]] | None = None
|
| 733 |
+
self._mot_active_segment_ids: tuple[int, ...] = ()
|
| 734 |
+
self._mot_identity_runtime: bool = False
|
| 735 |
+
# Non-KV per-span decode state (hidden, patch_count, target, ...).
|
| 736 |
+
self._span_states: dict[tuple[int, int], dict[str, object]] = {}
|
| 737 |
+
self._mot_pending_text_logits: torch.Tensor | None = None
|
| 738 |
+
self._mot_stats: list[dict[str, int]] = []
|
| 739 |
+
self._mot_pad_token_id: int = 0
|
| 740 |
+
|
| 741 |
+
@classmethod
|
| 742 |
+
def from_legacy_cache(
|
| 743 |
+
cls,
|
| 744 |
+
past_key_values: tuple[tuple[torch.Tensor, torch.Tensor], ...],
|
| 745 |
+
) -> "MoTDynamicCache":
|
| 746 |
+
del past_key_values
|
| 747 |
+
raise RuntimeError(
|
| 748 |
+
"MoT cannot recover runtime, position, and payload sidecars from a legacy tuple cache."
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
def to_legacy_cache(self) -> tuple[tuple[torch.Tensor, torch.Tensor], ...]:
|
| 752 |
+
raise RuntimeError(
|
| 753 |
+
"MoT cache cannot be converted to a legacy tuple without losing required sidecars."
|
| 754 |
+
)
|
| 755 |
+
|
| 756 |
+
def offload(self, layer_idx: int, only_non_sliding: bool = True) -> None:
|
| 757 |
+
del layer_idx, only_non_sliding
|
| 758 |
+
raise RuntimeError(
|
| 759 |
+
"MoTDynamicCache does not support offloading required MoT sidecars."
|
| 760 |
+
)
|
| 761 |
+
|
| 762 |
+
def prefetch(self, layer_idx: int, only_non_sliding: bool = True) -> None:
|
| 763 |
+
del layer_idx, only_non_sliding
|
| 764 |
+
raise RuntimeError(
|
| 765 |
+
"MoTDynamicCache does not support offloaded sidecar prefetch."
|
| 766 |
+
)
|
| 767 |
+
|
| 768 |
+
@property
|
| 769 |
+
def request_ids(self) -> tuple[_RequestId, ...]:
|
| 770 |
+
return self._request_ids
|
| 771 |
+
|
| 772 |
+
@property
|
| 773 |
+
def batch_size(self) -> int:
|
| 774 |
+
return self._batch_size
|
| 775 |
+
|
| 776 |
+
@property
|
| 777 |
+
def physical_length(self) -> int:
|
| 778 |
+
return self._physical_length
|
| 779 |
+
|
| 780 |
+
@property
|
| 781 |
+
def row_logical_next_positions(self) -> torch.LongTensor:
|
| 782 |
+
return self._row_logical_next_positions
|
| 783 |
+
|
| 784 |
+
@property
|
| 785 |
+
def row_rope_next_positions(self) -> torch.LongTensor:
|
| 786 |
+
return self._row_rope_next_positions
|
| 787 |
+
|
| 788 |
+
@property
|
| 789 |
+
def payload_fingerprints(self) -> tuple[str, ...] | None:
|
| 790 |
+
return self._payload_fingerprints
|
| 791 |
+
|
| 792 |
+
@property
|
| 793 |
+
def payload_epochs(self) -> tuple[int, ...]:
|
| 794 |
+
return tuple(int(epoch) for epoch in self._payload_epochs.tolist())
|
| 795 |
+
|
| 796 |
+
@property
|
| 797 |
+
def has_streams(self) -> bool:
|
| 798 |
+
return self._streams is not None
|
| 799 |
+
|
| 800 |
+
def bind_streams(self, streams: Mapping[StreamKey, dict]) -> None:
|
| 801 |
+
"""Bind the complete prefill stream registry exactly once."""
|
| 802 |
+
if self._streams is not None:
|
| 803 |
+
raise RuntimeError(
|
| 804 |
+
"MoT stream registry is already bound; reset the cache first."
|
| 805 |
+
)
|
| 806 |
+
validated: dict[StreamKey, dict[str, object]] = {}
|
| 807 |
+
for key, entry in streams.items():
|
| 808 |
+
_validate_stream_key(key)
|
| 809 |
+
if not isinstance(entry, dict):
|
| 810 |
+
raise TypeError(
|
| 811 |
+
f"Stream entry for {key!r} must be a dict, got {type(entry).__name__}."
|
| 812 |
+
)
|
| 813 |
+
if int(key[1]) >= self._batch_size:
|
| 814 |
+
raise ValueError(
|
| 815 |
+
f"Stream key {key!r} row exceeds cache batch size {self._batch_size}."
|
| 816 |
+
)
|
| 817 |
+
validated[key] = entry
|
| 818 |
+
self._streams = validated
|
| 819 |
+
|
| 820 |
+
def add_stream(self, key: StreamKey, entry: dict) -> None:
|
| 821 |
+
"""Register one lazily created stream (e.g. a new TS target layer)."""
|
| 822 |
+
if self._streams is None:
|
| 823 |
+
raise RuntimeError("Cannot add a stream before the registry is bound.")
|
| 824 |
+
_validate_stream_key(key)
|
| 825 |
+
if key in self._streams:
|
| 826 |
+
raise RuntimeError(f"Stream {key!r} is already registered.")
|
| 827 |
+
if not isinstance(entry, dict):
|
| 828 |
+
raise TypeError(
|
| 829 |
+
f"Stream entry for {key!r} must be a dict, got {type(entry).__name__}."
|
| 830 |
+
)
|
| 831 |
+
self._streams[key] = entry
|
| 832 |
+
|
| 833 |
+
def get_stream(self, key: StreamKey) -> dict | None:
|
| 834 |
+
"""Fetch one stream; None means the key is legitimately absent (e.g. unpaired layer)."""
|
| 835 |
+
if self._streams is None:
|
| 836 |
+
raise RuntimeError("MoT stream registry is not bound.")
|
| 837 |
+
return self._streams.get(_validate_stream_key(key))
|
| 838 |
+
|
| 839 |
+
def require_stream(self, key: StreamKey) -> dict:
|
| 840 |
+
stream = self.get_stream(key)
|
| 841 |
+
if stream is None:
|
| 842 |
+
raise RuntimeError(f"MoT stream registry has no entry for {key!r}.")
|
| 843 |
+
return stream
|
| 844 |
+
|
| 845 |
+
def iter_streams(
|
| 846 |
+
self,
|
| 847 |
+
*,
|
| 848 |
+
kind: str | None = None,
|
| 849 |
+
row: int | None = None,
|
| 850 |
+
span: int | None = None,
|
| 851 |
+
):
|
| 852 |
+
"""Yield (key, entry) pairs; the single enumeration point for reset/rollback/stats."""
|
| 853 |
+
if self._streams is None:
|
| 854 |
+
return
|
| 855 |
+
for key, entry in self._streams.items():
|
| 856 |
+
if kind is not None and key[0] != kind:
|
| 857 |
+
continue
|
| 858 |
+
if row is not None and key[1] != row:
|
| 859 |
+
continue
|
| 860 |
+
if span is not None and key[3] != span:
|
| 861 |
+
continue
|
| 862 |
+
yield key, entry
|
| 863 |
+
|
| 864 |
+
def _validate_native_kv(
|
| 865 |
+
self,
|
| 866 |
+
key_states: torch.Tensor,
|
| 867 |
+
value_states: torch.Tensor,
|
| 868 |
+
layer_idx: int,
|
| 869 |
+
cache_kwargs: Mapping[str, object] | None,
|
| 870 |
+
) -> torch.LongTensor:
|
| 871 |
+
if not isinstance(key_states, torch.Tensor) or not isinstance(
|
| 872 |
+
value_states, torch.Tensor
|
| 873 |
+
):
|
| 874 |
+
raise TypeError("Native cache key_states and value_states must be tensors.")
|
| 875 |
+
if key_states.ndim != 4 or value_states.ndim != 4:
|
| 876 |
+
raise ValueError(
|
| 877 |
+
"Native Qwen K/V must have shape [B,H_kv,T,D], got "
|
| 878 |
+
f"keys={tuple(key_states.shape)}, values={tuple(value_states.shape)}."
|
| 879 |
+
)
|
| 880 |
+
if tuple(key_states.shape) != tuple(value_states.shape):
|
| 881 |
+
raise ValueError(
|
| 882 |
+
f"Native Qwen K/V shape mismatch: keys={tuple(key_states.shape)}, values={tuple(value_states.shape)}."
|
| 883 |
+
)
|
| 884 |
+
if (
|
| 885 |
+
key_states.dtype != value_states.dtype
|
| 886 |
+
or key_states.device != value_states.device
|
| 887 |
+
):
|
| 888 |
+
raise ValueError(
|
| 889 |
+
"Native Qwen K/V dtype/device mismatch: "
|
| 890 |
+
f"keys={key_states.dtype}/{key_states.device}, values={value_states.dtype}/{value_states.device}."
|
| 891 |
+
)
|
| 892 |
+
if not torch.is_floating_point(key_states):
|
| 893 |
+
raise TypeError(
|
| 894 |
+
f"Native Qwen K/V must be floating point, got {key_states.dtype}."
|
| 895 |
+
)
|
| 896 |
+
if int(key_states.shape[0]) != self._batch_size:
|
| 897 |
+
raise ValueError(
|
| 898 |
+
f"Native Qwen K/V batch must match request_ids: expected {self._batch_size}, got {key_states.shape[0]}."
|
| 899 |
+
)
|
| 900 |
+
if int(key_states.shape[2]) <= 0:
|
| 901 |
+
raise ValueError("Native Qwen K/V update must contain at least one token.")
|
| 902 |
+
if layer_idx < 0 or (
|
| 903 |
+
self._config is not None
|
| 904 |
+
and layer_idx >= int(self._config.num_hidden_layers)
|
| 905 |
+
):
|
| 906 |
+
raise ValueError(f"Native cache layer_idx out of range: {layer_idx}.")
|
| 907 |
+
if self._config is not None:
|
| 908 |
+
expected_heads = int(self._config.num_key_value_heads)
|
| 909 |
+
expected_head_dim = int(
|
| 910 |
+
getattr(
|
| 911 |
+
self._config,
|
| 912 |
+
"head_dim",
|
| 913 |
+
self._config.hidden_size // self._config.num_attention_heads,
|
| 914 |
+
)
|
| 915 |
+
)
|
| 916 |
+
if tuple(key_states.shape[1::2]) != (expected_heads, expected_head_dim):
|
| 917 |
+
raise ValueError(
|
| 918 |
+
"Native cache must keep compact Qwen KV heads: "
|
| 919 |
+
f"expected H_kv/D={(expected_heads, expected_head_dim)}, "
|
| 920 |
+
f"got {(int(key_states.shape[1]), int(key_states.shape[3]))}."
|
| 921 |
+
)
|
| 922 |
+
|
| 923 |
+
previous_length = int(self.get_seq_length(layer_idx))
|
| 924 |
+
token_count = int(key_states.shape[2])
|
| 925 |
+
raw_cache_position = (
|
| 926 |
+
None if cache_kwargs is None else cache_kwargs.get("cache_position")
|
| 927 |
+
)
|
| 928 |
+
if raw_cache_position is None:
|
| 929 |
+
cache_position = torch.arange(
|
| 930 |
+
previous_length,
|
| 931 |
+
previous_length + token_count,
|
| 932 |
+
device=key_states.device,
|
| 933 |
+
dtype=torch.long,
|
| 934 |
+
)
|
| 935 |
+
else:
|
| 936 |
+
if not isinstance(raw_cache_position, torch.Tensor):
|
| 937 |
+
raise TypeError(
|
| 938 |
+
"cache_position must be a tensor for native cache update, got "
|
| 939 |
+
f"{type(raw_cache_position).__name__}."
|
| 940 |
+
)
|
| 941 |
+
cache_position = _require_positions(
|
| 942 |
+
raw_cache_position,
|
| 943 |
+
name="cache_position",
|
| 944 |
+
shape=(token_count,),
|
| 945 |
+
device=key_states.device,
|
| 946 |
+
)
|
| 947 |
+
expected = torch.arange(
|
| 948 |
+
previous_length,
|
| 949 |
+
previous_length + token_count,
|
| 950 |
+
device=key_states.device,
|
| 951 |
+
dtype=torch.long,
|
| 952 |
+
)
|
| 953 |
+
if not torch.equal(cache_position, expected):
|
| 954 |
+
raise ValueError(
|
| 955 |
+
"Dynamic cache_position must be a contiguous append: "
|
| 956 |
+
f"expected {expected.tolist()}, got {cache_position.tolist()}."
|
| 957 |
+
)
|
| 958 |
+
return cache_position
|
| 959 |
+
|
| 960 |
+
def update(
|
| 961 |
+
self,
|
| 962 |
+
key_states: torch.Tensor,
|
| 963 |
+
value_states: torch.Tensor,
|
| 964 |
+
layer_idx: int,
|
| 965 |
+
cache_kwargs: Mapping[str, object] | None = None,
|
| 966 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 967 |
+
cache_position = self._validate_native_kv(
|
| 968 |
+
key_states, value_states, layer_idx, cache_kwargs
|
| 969 |
+
)
|
| 970 |
+
while len(self.layers) <= layer_idx:
|
| 971 |
+
self.layers.append(
|
| 972 |
+
_GeometricDynamicLayer(initial_capacity=self._initial_sidecar_capacity)
|
| 973 |
+
)
|
| 974 |
+
keys, values = super().update(
|
| 975 |
+
key_states, value_states, layer_idx, dict(cache_kwargs or {})
|
| 976 |
+
)
|
| 977 |
+
resulting_length = int(self.get_seq_length(layer_idx))
|
| 978 |
+
expected_length = int(cache_position[-1].item()) + 1
|
| 979 |
+
if resulting_length != expected_length:
|
| 980 |
+
raise RuntimeError(
|
| 981 |
+
f"Native cache length drifted at layer {layer_idx}: expected {expected_length}, got {resulting_length}."
|
| 982 |
+
)
|
| 983 |
+
if layer_idx == 0:
|
| 984 |
+
if self._physical_length not in (
|
| 985 |
+
int(cache_position[0].item()),
|
| 986 |
+
expected_length,
|
| 987 |
+
):
|
| 988 |
+
raise RuntimeError(
|
| 989 |
+
"Native layer 0 update disagrees with physical_length: "
|
| 990 |
+
f"physical_length={self._physical_length}, cache_position={cache_position.tolist()}."
|
| 991 |
+
)
|
| 992 |
+
self._physical_length = expected_length
|
| 993 |
+
elif self._physical_length != expected_length:
|
| 994 |
+
raise RuntimeError(
|
| 995 |
+
"Native layers must update with the same physical timeline: "
|
| 996 |
+
f"layer={layer_idx}, layer_length={expected_length}, physical_length={self._physical_length}."
|
| 997 |
+
)
|
| 998 |
+
return keys, values
|
| 999 |
+
|
| 1000 |
+
def record_position_metadata(
|
| 1001 |
+
self,
|
| 1002 |
+
*,
|
| 1003 |
+
cache_position: torch.LongTensor,
|
| 1004 |
+
valid_mask: torch.Tensor,
|
| 1005 |
+
logical_positions: torch.LongTensor,
|
| 1006 |
+
rope_positions: torch.LongTensor,
|
| 1007 |
+
active_rows: torch.Tensor | None = None,
|
| 1008 |
+
) -> None:
|
| 1009 |
+
if not isinstance(cache_position, torch.Tensor) or cache_position.ndim != 1:
|
| 1010 |
+
raise ValueError(
|
| 1011 |
+
"cache_position metadata must be a 1D tensor, got "
|
| 1012 |
+
f"{None if not isinstance(cache_position, torch.Tensor) else tuple(cache_position.shape)}."
|
| 1013 |
+
)
|
| 1014 |
+
token_count = int(cache_position.numel())
|
| 1015 |
+
if token_count <= 0:
|
| 1016 |
+
raise ValueError(
|
| 1017 |
+
"Position metadata must contain at least one physical token."
|
| 1018 |
+
)
|
| 1019 |
+
valid_mask = _require_bool_mask(
|
| 1020 |
+
valid_mask,
|
| 1021 |
+
name="valid_mask",
|
| 1022 |
+
shape=(self._batch_size, token_count),
|
| 1023 |
+
)
|
| 1024 |
+
if (
|
| 1025 |
+
cache_position.device != valid_mask.device
|
| 1026 |
+
or cache_position.dtype is not torch.long
|
| 1027 |
+
):
|
| 1028 |
+
raise ValueError(
|
| 1029 |
+
"cache_position must be torch.long on the metadata device, got "
|
| 1030 |
+
f"{cache_position.dtype} on {cache_position.device}, metadata on {valid_mask.device}."
|
| 1031 |
+
)
|
| 1032 |
+
logical_positions = _require_positions(
|
| 1033 |
+
logical_positions,
|
| 1034 |
+
name="logical_positions",
|
| 1035 |
+
shape=(self._batch_size, token_count),
|
| 1036 |
+
device=valid_mask.device,
|
| 1037 |
+
)
|
| 1038 |
+
rope_positions = _require_positions(
|
| 1039 |
+
rope_positions,
|
| 1040 |
+
name="rope_positions",
|
| 1041 |
+
shape=(self._batch_size, token_count),
|
| 1042 |
+
device=valid_mask.device,
|
| 1043 |
+
)
|
| 1044 |
+
expected_start = self._position_buffer.length
|
| 1045 |
+
expected = torch.arange(
|
| 1046 |
+
expected_start,
|
| 1047 |
+
expected_start + token_count,
|
| 1048 |
+
device=cache_position.device,
|
| 1049 |
+
dtype=torch.long,
|
| 1050 |
+
)
|
| 1051 |
+
if not torch.equal(cache_position, expected):
|
| 1052 |
+
raise ValueError(
|
| 1053 |
+
"Position metadata must append contiguously: "
|
| 1054 |
+
f"expected {expected.tolist()}, got {cache_position.tolist()}."
|
| 1055 |
+
)
|
| 1056 |
+
if int(cache_position[-1].item()) >= self._physical_length:
|
| 1057 |
+
raise RuntimeError(
|
| 1058 |
+
"Position metadata cannot run ahead of native K/V: "
|
| 1059 |
+
f"last_position={int(cache_position[-1].item())}, physical_length={self._physical_length}."
|
| 1060 |
+
)
|
| 1061 |
+
if active_rows is not None:
|
| 1062 |
+
active_rows = _require_bool_mask(
|
| 1063 |
+
active_rows,
|
| 1064 |
+
name="active_rows",
|
| 1065 |
+
shape=(self._batch_size,),
|
| 1066 |
+
)
|
| 1067 |
+
if active_rows.device != valid_mask.device:
|
| 1068 |
+
raise ValueError(
|
| 1069 |
+
f"active_rows must be on {valid_mask.device}, got {active_rows.device}."
|
| 1070 |
+
)
|
| 1071 |
+
valid_mask = valid_mask & active_rows[:, None]
|
| 1072 |
+
if torch.any(logical_positions[valid_mask] < 0):
|
| 1073 |
+
raise ValueError("Valid logical_positions must be non-negative.")
|
| 1074 |
+
if torch.any(rope_positions[valid_mask] < 0):
|
| 1075 |
+
raise ValueError("Valid rope_positions must be non-negative.")
|
| 1076 |
+
|
| 1077 |
+
self._position_buffer.append(
|
| 1078 |
+
valid_mask=valid_mask,
|
| 1079 |
+
logical_positions=logical_positions,
|
| 1080 |
+
rope_positions=rope_positions,
|
| 1081 |
+
)
|
| 1082 |
+
if self._row_logical_next_positions.device != valid_mask.device:
|
| 1083 |
+
if expected_start != 0:
|
| 1084 |
+
raise RuntimeError(
|
| 1085 |
+
"Row logical position metadata changed device after prefill."
|
| 1086 |
+
)
|
| 1087 |
+
self._row_logical_next_positions = self._row_logical_next_positions.to(
|
| 1088 |
+
valid_mask.device
|
| 1089 |
+
)
|
| 1090 |
+
self._row_rope_next_positions = self._row_rope_next_positions.to(
|
| 1091 |
+
valid_mask.device
|
| 1092 |
+
)
|
| 1093 |
+
for row_idx in range(self._batch_size):
|
| 1094 |
+
row_valid = valid_mask[row_idx]
|
| 1095 |
+
if bool(torch.any(row_valid).item()):
|
| 1096 |
+
last_idx = int(torch.nonzero(row_valid, as_tuple=False)[-1].item())
|
| 1097 |
+
self._row_logical_next_positions[row_idx] = (
|
| 1098 |
+
logical_positions[row_idx, last_idx] + 1
|
| 1099 |
+
)
|
| 1100 |
+
self._row_rope_next_positions[row_idx] = (
|
| 1101 |
+
rope_positions[row_idx, last_idx] + 1
|
| 1102 |
+
)
|
| 1103 |
+
|
| 1104 |
+
def _write_sidecar(
|
| 1105 |
+
self,
|
| 1106 |
+
*,
|
| 1107 |
+
name: str,
|
| 1108 |
+
layer_idx: int,
|
| 1109 |
+
keys: torch.Tensor,
|
| 1110 |
+
values: torch.Tensor,
|
| 1111 |
+
valid_mask: torch.Tensor,
|
| 1112 |
+
physical_positions: torch.LongTensor,
|
| 1113 |
+
logical_positions: torch.LongTensor,
|
| 1114 |
+
rope_positions: torch.LongTensor,
|
| 1115 |
+
active_rows: torch.Tensor | None = None,
|
| 1116 |
+
replace_start: int | None = None,
|
| 1117 |
+
) -> MoTSidecarView:
|
| 1118 |
+
if not isinstance(name, str) or not name:
|
| 1119 |
+
raise ValueError(f"Sidecar name must be a non-empty string, got {name!r}.")
|
| 1120 |
+
if layer_idx < 0:
|
| 1121 |
+
raise ValueError(
|
| 1122 |
+
f"Sidecar layer_idx must be non-negative, got {layer_idx}."
|
| 1123 |
+
)
|
| 1124 |
+
if not isinstance(keys, torch.Tensor) or not isinstance(values, torch.Tensor):
|
| 1125 |
+
raise TypeError("Sidecar keys and values must be tensors.")
|
| 1126 |
+
if keys.ndim != 4 or values.ndim != 4:
|
| 1127 |
+
raise ValueError(
|
| 1128 |
+
"Sidecar K/V must have shape [B,T,H,D], got "
|
| 1129 |
+
f"keys={tuple(keys.shape)}, values={tuple(values.shape)}."
|
| 1130 |
+
)
|
| 1131 |
+
if tuple(keys.shape) != tuple(values.shape):
|
| 1132 |
+
raise ValueError(
|
| 1133 |
+
f"Sidecar K/V shape mismatch: keys={tuple(keys.shape)}, values={tuple(values.shape)}."
|
| 1134 |
+
)
|
| 1135 |
+
if keys.dtype != values.dtype or keys.device != values.device:
|
| 1136 |
+
raise ValueError(
|
| 1137 |
+
"Sidecar K/V dtype/device mismatch: "
|
| 1138 |
+
f"keys={keys.dtype}/{keys.device}, values={values.dtype}/{values.device}."
|
| 1139 |
+
)
|
| 1140 |
+
if not torch.is_floating_point(keys):
|
| 1141 |
+
raise TypeError(f"Sidecar K/V must be floating point, got {keys.dtype}.")
|
| 1142 |
+
batch_size, token_count = int(keys.shape[0]), int(keys.shape[1])
|
| 1143 |
+
if batch_size != self._batch_size:
|
| 1144 |
+
raise ValueError(
|
| 1145 |
+
f"Sidecar batch must be {self._batch_size}, got {batch_size}."
|
| 1146 |
+
)
|
| 1147 |
+
if token_count <= 0 or int(keys.shape[2]) <= 0 or int(keys.shape[3]) <= 0:
|
| 1148 |
+
raise ValueError(
|
| 1149 |
+
f"Sidecar K/V dimensions must be positive, got {tuple(keys.shape)}."
|
| 1150 |
+
)
|
| 1151 |
+
valid_mask = _require_bool_mask(
|
| 1152 |
+
valid_mask,
|
| 1153 |
+
name="valid_mask",
|
| 1154 |
+
shape=(self._batch_size, token_count),
|
| 1155 |
+
)
|
| 1156 |
+
if valid_mask.device != keys.device:
|
| 1157 |
+
raise ValueError(
|
| 1158 |
+
f"valid_mask must be on {keys.device}, got {valid_mask.device}."
|
| 1159 |
+
)
|
| 1160 |
+
if active_rows is not None:
|
| 1161 |
+
active_rows = _require_bool_mask(
|
| 1162 |
+
active_rows,
|
| 1163 |
+
name="active_rows",
|
| 1164 |
+
shape=(self._batch_size,),
|
| 1165 |
+
)
|
| 1166 |
+
if active_rows.device != keys.device:
|
| 1167 |
+
raise ValueError(
|
| 1168 |
+
f"active_rows must be on {keys.device}, got {active_rows.device}."
|
| 1169 |
+
)
|
| 1170 |
+
valid_mask = valid_mask & active_rows[:, None]
|
| 1171 |
+
physical_positions = _expanded_positions(
|
| 1172 |
+
physical_positions,
|
| 1173 |
+
batch_size=self._batch_size,
|
| 1174 |
+
token_count=token_count,
|
| 1175 |
+
name="physical_positions",
|
| 1176 |
+
device=keys.device,
|
| 1177 |
+
)
|
| 1178 |
+
logical_positions = _require_positions(
|
| 1179 |
+
logical_positions,
|
| 1180 |
+
name="logical_positions",
|
| 1181 |
+
shape=(self._batch_size, token_count),
|
| 1182 |
+
device=keys.device,
|
| 1183 |
+
)
|
| 1184 |
+
rope_positions = _require_positions(
|
| 1185 |
+
rope_positions,
|
| 1186 |
+
name="rope_positions",
|
| 1187 |
+
shape=(self._batch_size, token_count),
|
| 1188 |
+
device=keys.device,
|
| 1189 |
+
)
|
| 1190 |
+
for positions, position_name in (
|
| 1191 |
+
(physical_positions, "physical_positions"),
|
| 1192 |
+
(logical_positions, "logical_positions"),
|
| 1193 |
+
(rope_positions, "rope_positions"),
|
| 1194 |
+
):
|
| 1195 |
+
if torch.any(positions[valid_mask] < 0):
|
| 1196 |
+
raise ValueError(f"Valid {position_name} must be non-negative.")
|
| 1197 |
+
if torch.any(physical_positions[valid_mask] >= self._physical_length):
|
| 1198 |
+
raise ValueError(
|
| 1199 |
+
"Sidecar physical_positions must refer to existing native cache columns: "
|
| 1200 |
+
f"physical_length={self._physical_length}."
|
| 1201 |
+
)
|
| 1202 |
+
|
| 1203 |
+
layer_buffers = self._sidecars.setdefault(name, {})
|
| 1204 |
+
buffer = layer_buffers.get(layer_idx)
|
| 1205 |
+
if buffer is None:
|
| 1206 |
+
if replace_start is not None:
|
| 1207 |
+
raise KeyError(
|
| 1208 |
+
f"Cannot replace absent MoT sidecar {name!r} at layer {layer_idx}."
|
| 1209 |
+
)
|
| 1210 |
+
buffer = _SidecarBuffer(
|
| 1211 |
+
batch_size=self._batch_size,
|
| 1212 |
+
initial_capacity=self._initial_sidecar_capacity,
|
| 1213 |
+
)
|
| 1214 |
+
layer_buffers[layer_idx] = buffer
|
| 1215 |
+
if replace_start is not None:
|
| 1216 |
+
if isinstance(replace_start, bool) or not isinstance(replace_start, int):
|
| 1217 |
+
raise TypeError(
|
| 1218 |
+
f"replace_start must be an integer, got {type(replace_start).__name__}."
|
| 1219 |
+
)
|
| 1220 |
+
if buffer.capacity > 0 and (
|
| 1221 |
+
buffer.keys.dtype != keys.dtype
|
| 1222 |
+
or buffer.keys.device != keys.device
|
| 1223 |
+
or tuple(buffer.keys.shape[2:]) != tuple(keys.shape[2:])
|
| 1224 |
+
):
|
| 1225 |
+
raise ValueError(
|
| 1226 |
+
"Replacement sidecar dtype/device/head shape must match the existing stream: "
|
| 1227 |
+
f"existing={buffer.keys.dtype}/{buffer.keys.device}/{tuple(buffer.keys.shape[2:])}, "
|
| 1228 |
+
f"replacement={keys.dtype}/{keys.device}/{tuple(keys.shape[2:])}."
|
| 1229 |
+
)
|
| 1230 |
+
buffer.truncate(replace_start)
|
| 1231 |
+
buffer.append(
|
| 1232 |
+
keys=keys,
|
| 1233 |
+
values=values,
|
| 1234 |
+
valid_mask=valid_mask,
|
| 1235 |
+
physical_positions=physical_positions,
|
| 1236 |
+
logical_positions=logical_positions,
|
| 1237 |
+
rope_positions=rope_positions,
|
| 1238 |
+
)
|
| 1239 |
+
return buffer.view()
|
| 1240 |
+
|
| 1241 |
+
def append_sidecar(
|
| 1242 |
+
self,
|
| 1243 |
+
*,
|
| 1244 |
+
name: str,
|
| 1245 |
+
layer_idx: int,
|
| 1246 |
+
keys: torch.Tensor,
|
| 1247 |
+
values: torch.Tensor,
|
| 1248 |
+
valid_mask: torch.Tensor,
|
| 1249 |
+
physical_positions: torch.LongTensor,
|
| 1250 |
+
logical_positions: torch.LongTensor,
|
| 1251 |
+
rope_positions: torch.LongTensor,
|
| 1252 |
+
active_rows: torch.Tensor | None = None,
|
| 1253 |
+
) -> MoTSidecarView:
|
| 1254 |
+
return self._write_sidecar(
|
| 1255 |
+
name=name,
|
| 1256 |
+
layer_idx=layer_idx,
|
| 1257 |
+
keys=keys,
|
| 1258 |
+
values=values,
|
| 1259 |
+
valid_mask=valid_mask,
|
| 1260 |
+
physical_positions=physical_positions,
|
| 1261 |
+
logical_positions=logical_positions,
|
| 1262 |
+
rope_positions=rope_positions,
|
| 1263 |
+
active_rows=active_rows,
|
| 1264 |
+
)
|
| 1265 |
+
|
| 1266 |
+
def rollback_sidecar(
|
| 1267 |
+
self, name: str, layer_idx: int, *, target_length: int
|
| 1268 |
+
) -> MoTSidecarView:
|
| 1269 |
+
"""Drop one mutable sidecar tail without touching native or sibling streams."""
|
| 1270 |
+
try:
|
| 1271 |
+
buffer = self._sidecars[name][layer_idx]
|
| 1272 |
+
except KeyError as exc:
|
| 1273 |
+
raise KeyError(
|
| 1274 |
+
f"No MoT sidecar named {name!r} at layer {layer_idx}."
|
| 1275 |
+
) from exc
|
| 1276 |
+
if isinstance(target_length, bool) or not isinstance(target_length, int):
|
| 1277 |
+
raise TypeError(
|
| 1278 |
+
f"target_length must be an integer, got {type(target_length).__name__}."
|
| 1279 |
+
)
|
| 1280 |
+
buffer.truncate(target_length)
|
| 1281 |
+
return buffer.view()
|
| 1282 |
+
|
| 1283 |
+
def replace_sidecar_tail(
|
| 1284 |
+
self,
|
| 1285 |
+
*,
|
| 1286 |
+
name: str,
|
| 1287 |
+
layer_idx: int,
|
| 1288 |
+
start: int,
|
| 1289 |
+
keys: torch.Tensor,
|
| 1290 |
+
values: torch.Tensor,
|
| 1291 |
+
valid_mask: torch.Tensor,
|
| 1292 |
+
physical_positions: torch.LongTensor,
|
| 1293 |
+
logical_positions: torch.LongTensor,
|
| 1294 |
+
rope_positions: torch.LongTensor,
|
| 1295 |
+
active_rows: torch.Tensor | None = None,
|
| 1296 |
+
) -> MoTSidecarView:
|
| 1297 |
+
"""Validate a replacement, then atomically discard and rewrite one stream tail."""
|
| 1298 |
+
return self._write_sidecar(
|
| 1299 |
+
name=name,
|
| 1300 |
+
layer_idx=layer_idx,
|
| 1301 |
+
keys=keys,
|
| 1302 |
+
values=values,
|
| 1303 |
+
valid_mask=valid_mask,
|
| 1304 |
+
physical_positions=physical_positions,
|
| 1305 |
+
logical_positions=logical_positions,
|
| 1306 |
+
rope_positions=rope_positions,
|
| 1307 |
+
active_rows=active_rows,
|
| 1308 |
+
replace_start=start,
|
| 1309 |
+
)
|
| 1310 |
+
|
| 1311 |
+
def get_sidecar(self, name: str, layer_idx: int) -> MoTSidecarView:
|
| 1312 |
+
try:
|
| 1313 |
+
return self._sidecars[name][layer_idx].view()
|
| 1314 |
+
except KeyError as exc:
|
| 1315 |
+
raise KeyError(
|
| 1316 |
+
f"No MoT sidecar named {name!r} at layer {layer_idx}."
|
| 1317 |
+
) from exc
|
| 1318 |
+
|
| 1319 |
+
def bind_payload_fingerprints(self, fingerprints: Sequence[str]) -> None:
|
| 1320 |
+
normalized = tuple(fingerprints)
|
| 1321 |
+
if len(normalized) != self._batch_size or any(
|
| 1322 |
+
not isinstance(value, str) or not value for value in normalized
|
| 1323 |
+
):
|
| 1324 |
+
raise ValueError(
|
| 1325 |
+
f"Payload fingerprints must contain {self._batch_size} non-empty strings, got {normalized}."
|
| 1326 |
+
)
|
| 1327 |
+
if self._payload_fingerprints is None:
|
| 1328 |
+
self._payload_fingerprints = normalized
|
| 1329 |
+
return
|
| 1330 |
+
self.validate_payload_fingerprints(normalized)
|
| 1331 |
+
|
| 1332 |
+
def validate_payload_fingerprints(self, fingerprints: Sequence[str]) -> None:
|
| 1333 |
+
if self._payload_fingerprints is None:
|
| 1334 |
+
raise RuntimeError("Payload fingerprints are not bound to this cache.")
|
| 1335 |
+
candidate = tuple(fingerprints)
|
| 1336 |
+
if len(candidate) != self._batch_size:
|
| 1337 |
+
raise ValueError(
|
| 1338 |
+
f"Expected {self._batch_size} payload fingerprints, got {len(candidate)}."
|
| 1339 |
+
)
|
| 1340 |
+
changed = [
|
| 1341 |
+
self._request_ids[row_idx]
|
| 1342 |
+
for row_idx, (cached, current) in enumerate(
|
| 1343 |
+
zip(self._payload_fingerprints, candidate, strict=True)
|
| 1344 |
+
)
|
| 1345 |
+
if cached != current
|
| 1346 |
+
]
|
| 1347 |
+
if changed:
|
| 1348 |
+
raise RuntimeError(
|
| 1349 |
+
"MoT payload changed while KV cache was live; start a new payload epoch for request_ids="
|
| 1350 |
+
f"{changed}."
|
| 1351 |
+
)
|
| 1352 |
+
|
| 1353 |
+
def start_payload_epoch(self, fingerprints: Sequence[str]) -> None:
|
| 1354 |
+
if self._streams is not None:
|
| 1355 |
+
raise RuntimeError(
|
| 1356 |
+
"External payload epochs cannot preserve bound MoT runtime streams; start a fresh prefill cache."
|
| 1357 |
+
)
|
| 1358 |
+
if self._payload_fingerprints is None:
|
| 1359 |
+
raise RuntimeError(
|
| 1360 |
+
"Cannot advance payload epoch before binding initial fingerprints."
|
| 1361 |
+
)
|
| 1362 |
+
candidate = tuple(fingerprints)
|
| 1363 |
+
if len(candidate) != self._batch_size or any(
|
| 1364 |
+
not isinstance(value, str) or not value for value in candidate
|
| 1365 |
+
):
|
| 1366 |
+
raise ValueError(
|
| 1367 |
+
f"Expected {self._batch_size} non-empty payload fingerprints, got {candidate}."
|
| 1368 |
+
)
|
| 1369 |
+
changed = torch.tensor(
|
| 1370 |
+
[
|
| 1371 |
+
cached != current
|
| 1372 |
+
for cached, current in zip(
|
| 1373 |
+
self._payload_fingerprints, candidate, strict=True
|
| 1374 |
+
)
|
| 1375 |
+
],
|
| 1376 |
+
dtype=torch.bool,
|
| 1377 |
+
)
|
| 1378 |
+
if not bool(torch.any(changed).item()):
|
| 1379 |
+
raise ValueError(
|
| 1380 |
+
"start_payload_epoch requires at least one changed payload fingerprint."
|
| 1381 |
+
)
|
| 1382 |
+
next_epochs = self._payload_epochs + changed.to(dtype=torch.long)
|
| 1383 |
+
self._clear_content(clear_payload=False)
|
| 1384 |
+
self._payload_fingerprints = candidate
|
| 1385 |
+
self._payload_epochs = next_epochs
|
| 1386 |
+
|
| 1387 |
+
def commit_payload_mutation(
|
| 1388 |
+
self,
|
| 1389 |
+
fingerprints: Sequence[str],
|
| 1390 |
+
*,
|
| 1391 |
+
changed_rows: Sequence[int],
|
| 1392 |
+
) -> None:
|
| 1393 |
+
"""Record a payload mutation only after its TS and fusion streams were synchronized."""
|
| 1394 |
+
if self._payload_fingerprints is None:
|
| 1395 |
+
raise RuntimeError(
|
| 1396 |
+
"Cannot commit payload mutation before binding initial fingerprints."
|
| 1397 |
+
)
|
| 1398 |
+
candidate = tuple(fingerprints)
|
| 1399 |
+
if len(candidate) != self._batch_size or any(
|
| 1400 |
+
not isinstance(value, str) or not value for value in candidate
|
| 1401 |
+
):
|
| 1402 |
+
raise ValueError(
|
| 1403 |
+
f"Expected {self._batch_size} non-empty payload fingerprints, got {candidate}."
|
| 1404 |
+
)
|
| 1405 |
+
normalized_rows = tuple(int(row_idx) for row_idx in changed_rows)
|
| 1406 |
+
if len(set(normalized_rows)) != len(normalized_rows):
|
| 1407 |
+
raise ValueError(f"changed_rows must be unique, got {normalized_rows}.")
|
| 1408 |
+
if any(
|
| 1409 |
+
row_idx < 0 or row_idx >= self._batch_size for row_idx in normalized_rows
|
| 1410 |
+
):
|
| 1411 |
+
raise IndexError(
|
| 1412 |
+
f"changed_rows must stay in [0,{self._batch_size}), got {normalized_rows}."
|
| 1413 |
+
)
|
| 1414 |
+
observed_changed = {
|
| 1415 |
+
row_idx
|
| 1416 |
+
for row_idx, (cached, current) in enumerate(
|
| 1417 |
+
zip(self._payload_fingerprints, candidate, strict=True)
|
| 1418 |
+
)
|
| 1419 |
+
if cached != current
|
| 1420 |
+
}
|
| 1421 |
+
expected_changed = set(normalized_rows)
|
| 1422 |
+
if observed_changed != expected_changed:
|
| 1423 |
+
raise RuntimeError(
|
| 1424 |
+
"Controlled payload mutation rows do not match fingerprint changes: "
|
| 1425 |
+
f"expected={sorted(expected_changed)}, observed={sorted(observed_changed)}."
|
| 1426 |
+
)
|
| 1427 |
+
if not observed_changed:
|
| 1428 |
+
raise ValueError(
|
| 1429 |
+
"commit_payload_mutation requires at least one changed payload fingerprint."
|
| 1430 |
+
)
|
| 1431 |
+
changed_mask = torch.tensor(
|
| 1432 |
+
[row_idx in observed_changed for row_idx in range(self._batch_size)],
|
| 1433 |
+
dtype=torch.long,
|
| 1434 |
+
)
|
| 1435 |
+
self._payload_epochs = self._payload_epochs + changed_mask
|
| 1436 |
+
self._payload_fingerprints = candidate
|
| 1437 |
+
|
| 1438 |
+
def _reject_bound_batch_transform(self, operation: str) -> None:
|
| 1439 |
+
if self._streams is not None:
|
| 1440 |
+
raise RuntimeError(
|
| 1441 |
+
f"MoT cache {operation} is unavailable while request-local runtime streams are bound."
|
| 1442 |
+
)
|
| 1443 |
+
|
| 1444 |
+
def _validated_batch_indices(
|
| 1445 |
+
self, indices: torch.LongTensor, *, name: str
|
| 1446 |
+
) -> torch.LongTensor:
|
| 1447 |
+
if (
|
| 1448 |
+
not isinstance(indices, torch.Tensor)
|
| 1449 |
+
or indices.dtype is not torch.long
|
| 1450 |
+
or indices.ndim != 1
|
| 1451 |
+
):
|
| 1452 |
+
raise TypeError(f"{name} must be a 1D torch.long tensor.")
|
| 1453 |
+
if indices.numel() <= 0:
|
| 1454 |
+
raise ValueError(f"{name} must select at least one row.")
|
| 1455 |
+
indices_cpu = indices.detach().to(device="cpu")
|
| 1456 |
+
if (
|
| 1457 |
+
int(indices_cpu.min().item()) < 0
|
| 1458 |
+
or int(indices_cpu.max().item()) >= self._batch_size
|
| 1459 |
+
):
|
| 1460 |
+
raise IndexError(
|
| 1461 |
+
f"{name} values must be in [0, {self._batch_size}), got {indices_cpu.tolist()}."
|
| 1462 |
+
)
|
| 1463 |
+
return indices_cpu
|
| 1464 |
+
|
| 1465 |
+
def _select_metadata(self, indices: torch.LongTensor) -> None:
|
| 1466 |
+
selected = indices.tolist()
|
| 1467 |
+
self._request_ids = tuple(self._request_ids[index] for index in selected)
|
| 1468 |
+
self._payload_epochs = self._payload_epochs.index_select(0, indices)
|
| 1469 |
+
if self._payload_fingerprints is not None:
|
| 1470 |
+
self._payload_fingerprints = tuple(
|
| 1471 |
+
self._payload_fingerprints[index] for index in selected
|
| 1472 |
+
)
|
| 1473 |
+
row_indices = indices.to(device=self._row_logical_next_positions.device)
|
| 1474 |
+
self._row_logical_next_positions = (
|
| 1475 |
+
self._row_logical_next_positions.index_select(0, row_indices)
|
| 1476 |
+
)
|
| 1477 |
+
self._row_rope_next_positions = self._row_rope_next_positions.index_select(
|
| 1478 |
+
0, row_indices
|
| 1479 |
+
)
|
| 1480 |
+
self._position_buffer.index_select(indices)
|
| 1481 |
+
for layer_buffers in self._sidecars.values():
|
| 1482 |
+
for buffer in layer_buffers.values():
|
| 1483 |
+
buffer.index_select(indices)
|
| 1484 |
+
self._batch_size = int(indices.numel())
|
| 1485 |
+
|
| 1486 |
+
def reorder_cache(self, beam_idx: torch.LongTensor) -> None:
|
| 1487 |
+
self._reject_bound_batch_transform("reorder")
|
| 1488 |
+
indices = self._validated_batch_indices(beam_idx, name="beam_idx")
|
| 1489 |
+
super().reorder_cache(indices)
|
| 1490 |
+
self._select_metadata(indices)
|
| 1491 |
+
|
| 1492 |
+
def batch_select_indices(self, indices: torch.Tensor) -> None:
|
| 1493 |
+
self._reject_bound_batch_transform("batch selection")
|
| 1494 |
+
indices_cpu = self._validated_batch_indices(indices, name="indices")
|
| 1495 |
+
super().batch_select_indices(indices_cpu)
|
| 1496 |
+
self._select_metadata(indices_cpu)
|
| 1497 |
+
|
| 1498 |
+
def batch_repeat_interleave(self, repeats: int) -> None:
|
| 1499 |
+
self._reject_bound_batch_transform("batch repeat")
|
| 1500 |
+
if isinstance(repeats, bool) or not isinstance(repeats, int) or repeats <= 0:
|
| 1501 |
+
raise ValueError(f"repeats must be a positive integer, got {repeats!r}.")
|
| 1502 |
+
super().batch_repeat_interleave(repeats)
|
| 1503 |
+
self._request_ids = tuple(
|
| 1504 |
+
request_id for request_id in self._request_ids for _ in range(repeats)
|
| 1505 |
+
)
|
| 1506 |
+
self._payload_epochs = self._payload_epochs.repeat_interleave(repeats)
|
| 1507 |
+
if self._payload_fingerprints is not None:
|
| 1508 |
+
self._payload_fingerprints = tuple(
|
| 1509 |
+
fingerprint
|
| 1510 |
+
for fingerprint in self._payload_fingerprints
|
| 1511 |
+
for _ in range(repeats)
|
| 1512 |
+
)
|
| 1513 |
+
self._row_logical_next_positions = (
|
| 1514 |
+
self._row_logical_next_positions.repeat_interleave(repeats)
|
| 1515 |
+
)
|
| 1516 |
+
self._row_rope_next_positions = self._row_rope_next_positions.repeat_interleave(
|
| 1517 |
+
repeats
|
| 1518 |
+
)
|
| 1519 |
+
self._position_buffer.repeat_interleave(repeats)
|
| 1520 |
+
for layer_buffers in self._sidecars.values():
|
| 1521 |
+
for buffer in layer_buffers.values():
|
| 1522 |
+
buffer.repeat_interleave(repeats)
|
| 1523 |
+
self._batch_size *= repeats
|
| 1524 |
+
|
| 1525 |
+
def _target_crop_length(self, max_length: int) -> int:
|
| 1526 |
+
if isinstance(max_length, bool) or not isinstance(max_length, int):
|
| 1527 |
+
raise TypeError(
|
| 1528 |
+
f"max_length must be an integer, got {type(max_length).__name__}."
|
| 1529 |
+
)
|
| 1530 |
+
target = (
|
| 1531 |
+
self._physical_length - abs(max_length) if max_length < 0 else max_length
|
| 1532 |
+
)
|
| 1533 |
+
return max(0, min(self._physical_length, target))
|
| 1534 |
+
|
| 1535 |
+
def crop(self, max_length: int) -> None:
|
| 1536 |
+
self._reject_bound_batch_transform("crop")
|
| 1537 |
+
target = self._target_crop_length(max_length)
|
| 1538 |
+
super().crop(target)
|
| 1539 |
+
self._physical_length = target
|
| 1540 |
+
self._position_buffer.crop(target)
|
| 1541 |
+
for layer_buffers in self._sidecars.values():
|
| 1542 |
+
for buffer in layer_buffers.values():
|
| 1543 |
+
buffer.crop_physical(target)
|
| 1544 |
+
self._recompute_row_next_positions()
|
| 1545 |
+
|
| 1546 |
+
def _recompute_row_next_positions(self) -> None:
|
| 1547 |
+
self._row_logical_next_positions.zero_()
|
| 1548 |
+
self._row_rope_next_positions.zero_()
|
| 1549 |
+
if self._position_buffer.length == 0:
|
| 1550 |
+
return
|
| 1551 |
+
valid = self._position_buffer.valid_mask[:, : self._position_buffer.length]
|
| 1552 |
+
logical = self._position_buffer.logical_positions[
|
| 1553 |
+
:, : self._position_buffer.length
|
| 1554 |
+
]
|
| 1555 |
+
rope = self._position_buffer.rope_positions[:, : self._position_buffer.length]
|
| 1556 |
+
for row_idx in range(self._batch_size):
|
| 1557 |
+
row_valid = valid[row_idx]
|
| 1558 |
+
if bool(torch.any(row_valid).item()):
|
| 1559 |
+
last_idx = int(torch.nonzero(row_valid, as_tuple=False)[-1].item())
|
| 1560 |
+
self._row_logical_next_positions[row_idx] = (
|
| 1561 |
+
logical[row_idx, last_idx] + 1
|
| 1562 |
+
)
|
| 1563 |
+
self._row_rope_next_positions[row_idx] = rope[row_idx, last_idx] + 1
|
| 1564 |
+
|
| 1565 |
+
def _clear_content(self, *, clear_payload: bool) -> None:
|
| 1566 |
+
super().crop(0)
|
| 1567 |
+
self._physical_length = 0
|
| 1568 |
+
self._position_buffer.reset()
|
| 1569 |
+
self._row_logical_next_positions.zero_()
|
| 1570 |
+
self._row_rope_next_positions.zero_()
|
| 1571 |
+
for layer_buffers in self._sidecars.values():
|
| 1572 |
+
for buffer in layer_buffers.values():
|
| 1573 |
+
buffer.reset()
|
| 1574 |
+
if clear_payload:
|
| 1575 |
+
self._payload_fingerprints = None
|
| 1576 |
+
self._payload_epochs.zero_()
|
| 1577 |
+
|
| 1578 |
+
def reset(self) -> None:
|
| 1579 |
+
self._clear_content(clear_payload=True)
|
| 1580 |
+
self._streams = None
|
| 1581 |
+
self._mot_active_segment_ids = ()
|
| 1582 |
+
self._mot_identity_runtime = False
|
| 1583 |
+
self._span_states = {}
|
| 1584 |
+
self._mot_pending_text_logits = None
|
| 1585 |
+
self._mot_stats = []
|
| 1586 |
+
self._mot_pad_token_id = 0
|
| 1587 |
+
|
| 1588 |
+
def validate_consistency(self, *, require_position_metadata: bool = True) -> None:
|
| 1589 |
+
for layer_idx, layer in enumerate(self.layers):
|
| 1590 |
+
if (
|
| 1591 |
+
layer.is_initialized
|
| 1592 |
+
and int(layer.get_seq_length()) != self._physical_length
|
| 1593 |
+
):
|
| 1594 |
+
raise RuntimeError(
|
| 1595 |
+
"Native cache layer length mismatch: "
|
| 1596 |
+
f"layer={layer_idx}, layer_length={int(layer.get_seq_length())}, "
|
| 1597 |
+
f"physical_length={self._physical_length}."
|
| 1598 |
+
)
|
| 1599 |
+
if (
|
| 1600 |
+
require_position_metadata
|
| 1601 |
+
and self._position_buffer.length != self._physical_length
|
| 1602 |
+
):
|
| 1603 |
+
raise RuntimeError(
|
| 1604 |
+
"Position metadata length mismatch: "
|
| 1605 |
+
f"metadata={self._position_buffer.length}, physical_length={self._physical_length}."
|
| 1606 |
+
)
|
| 1607 |
+
|
| 1608 |
+
|
| 1609 |
+
__all__ = [
|
| 1610 |
+
"MoTDynamicCache",
|
| 1611 |
+
"MoTSidecarView",
|
| 1612 |
+
"fingerprint_payload_state",
|
| 1613 |
+
"validate_mot_cache_inputs",
|
| 1614 |
+
]
|
timebraid/model/mot/model.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
timebraid/model/mot/pairing.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Layer-pairing helpers for the TimeBraid model core."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
_QWEN_PAIRABLE_LAYER_TYPE = "full_attention"
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _build_pair_map(
|
| 11 |
+
num_q_layers: int,
|
| 12 |
+
num_t_layers: int,
|
| 13 |
+
pairing_mode: str,
|
| 14 |
+
q_layer_indices: Optional[List[int]] = None,
|
| 15 |
+
) -> Dict[int, int]:
|
| 16 |
+
"""Build the Q-layer -> TimesFM-layer mapping for one pairing mode."""
|
| 17 |
+
if num_t_layers <= 0:
|
| 18 |
+
raise ValueError(f"num_t_layers must be positive, got {num_t_layers}")
|
| 19 |
+
if q_layer_indices is None:
|
| 20 |
+
available_q_layers = list(range(num_q_layers))
|
| 21 |
+
else:
|
| 22 |
+
available_q_layers = [int(idx) for idx in q_layer_indices]
|
| 23 |
+
if not available_q_layers:
|
| 24 |
+
raise ValueError("No pairable Q layers are available for MoT bridge mapping.")
|
| 25 |
+
if available_q_layers != sorted(available_q_layers):
|
| 26 |
+
raise ValueError(
|
| 27 |
+
f"q_layer_indices must be sorted ascending, got {available_q_layers}"
|
| 28 |
+
)
|
| 29 |
+
if available_q_layers[0] < 0 or available_q_layers[-1] >= num_q_layers:
|
| 30 |
+
raise ValueError(
|
| 31 |
+
"q_layer_indices must stay inside [0, num_q_layers). "
|
| 32 |
+
f"got indices={available_q_layers}, q_layers={num_q_layers}"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
if len(available_q_layers) > len(set(available_q_layers)):
|
| 36 |
+
raise ValueError(f"q_layer_indices must be unique, got {available_q_layers}")
|
| 37 |
+
if num_t_layers > num_q_layers and q_layer_indices is None:
|
| 38 |
+
raise ValueError(
|
| 39 |
+
f"TimesFM layer count exceeds Qwen layers: t_layers={num_t_layers}, q_layers={num_q_layers}"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
if pairing_mode == "interleaved":
|
| 43 |
+
if len(available_q_layers) < num_t_layers:
|
| 44 |
+
raise ValueError(
|
| 45 |
+
"`interleaved` requires at least as many pairable Qwen layers as active TimesFM layers, "
|
| 46 |
+
f"got pairable_q_layers={len(available_q_layers)}, t_layers={num_t_layers}."
|
| 47 |
+
)
|
| 48 |
+
if num_t_layers == 1:
|
| 49 |
+
return {available_q_layers[0]: 0}
|
| 50 |
+
q_last = len(available_q_layers) - 1
|
| 51 |
+
t_last = num_t_layers - 1
|
| 52 |
+
selected_q_layers = [
|
| 53 |
+
available_q_layers[round(t_idx * q_last / t_last)]
|
| 54 |
+
for t_idx in range(num_t_layers)
|
| 55 |
+
]
|
| 56 |
+
return {q_idx: t_idx for t_idx, q_idx in enumerate(selected_q_layers)}
|
| 57 |
+
|
| 58 |
+
if pairing_mode == "dsfp":
|
| 59 |
+
if len(available_q_layers) != num_q_layers:
|
| 60 |
+
raise ValueError(
|
| 61 |
+
"`dsfp` requires every Qwen layer to be pairable, got "
|
| 62 |
+
f"pairable_q_layers={len(available_q_layers)}, q_layers={num_q_layers}."
|
| 63 |
+
)
|
| 64 |
+
if num_t_layers != num_q_layers:
|
| 65 |
+
raise ValueError(
|
| 66 |
+
"`dsfp` maps the expanded TimesFM stack one-to-one onto Qwen depth, got "
|
| 67 |
+
f"t_layers={num_t_layers}, q_layers={num_q_layers}."
|
| 68 |
+
)
|
| 69 |
+
return {q_idx: t_idx for t_idx, q_idx in enumerate(available_q_layers)}
|
| 70 |
+
|
| 71 |
+
raise ValueError(
|
| 72 |
+
f"Unsupported MoT pairing mode: {pairing_mode!r}. Expected 'interleaved' or 'dsfp'."
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _resolve_pairable_q_layer_indices(config: Any, num_q_layers: int) -> List[int]:
|
| 77 |
+
"""Resolve which Qwen decoder layers can participate in the TimeBraid layer plan."""
|
| 78 |
+
layer_types = getattr(config, "layer_types", None)
|
| 79 |
+
if layer_types is None:
|
| 80 |
+
return list(range(num_q_layers))
|
| 81 |
+
if len(layer_types) != num_q_layers:
|
| 82 |
+
raise ValueError(
|
| 83 |
+
"config.layer_types length mismatch with num_hidden_layers: "
|
| 84 |
+
f"len(layer_types)={len(layer_types)}, num_hidden_layers={num_q_layers}"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
pairable = [
|
| 88 |
+
idx
|
| 89 |
+
for idx, layer_type in enumerate(layer_types)
|
| 90 |
+
if layer_type == _QWEN_PAIRABLE_LAYER_TYPE
|
| 91 |
+
]
|
| 92 |
+
if not pairable:
|
| 93 |
+
raise ValueError(
|
| 94 |
+
f"MoT requires at least one `{_QWEN_PAIRABLE_LAYER_TYPE}` layer, but none were found in "
|
| 95 |
+
"config.layer_types."
|
| 96 |
+
)
|
| 97 |
+
return pairable
|
timebraid/model/mot/runtime_state.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Immutable per-model MoT construction options."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
from types import MappingProxyType
|
| 7 |
+
from typing import Any, Mapping
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
from .config_contract import MOT_OPTION_BY_NAME
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class MoTRuntimeOptions:
|
| 15 |
+
"""Read-only option view used while initializing one TimeBraid model."""
|
| 16 |
+
|
| 17 |
+
__slots__ = ("_values",)
|
| 18 |
+
|
| 19 |
+
def __init__(self, values: Mapping[str, Any] | None = None) -> None:
|
| 20 |
+
if values is None:
|
| 21 |
+
values = {}
|
| 22 |
+
if not isinstance(values, Mapping):
|
| 23 |
+
raise TypeError(
|
| 24 |
+
f"MoT runtime options must be a mapping, got {type(values).__name__}."
|
| 25 |
+
)
|
| 26 |
+
# A read-only copy prevents both caller mutation and cross-model state
|
| 27 |
+
# leakage. Model construction can therefore overlap safely in threads;
|
| 28 |
+
# there is no process-global configuration transaction to race on.
|
| 29 |
+
self._values = MappingProxyType(dict(values))
|
| 30 |
+
|
| 31 |
+
def _value(self, name: str, default: Any) -> Any:
|
| 32 |
+
value = self._values.get(name, default)
|
| 33 |
+
return default if value is None else value
|
| 34 |
+
|
| 35 |
+
def value(self, name: str) -> Any:
|
| 36 |
+
"""Read one option, taking its type and default from the registry.
|
| 37 |
+
|
| 38 |
+
Call sites used to restate both at every read, which is how the same
|
| 39 |
+
knob ended up with disagreeing defaults in different files. Here the
|
| 40 |
+
registry answers once.
|
| 41 |
+
"""
|
| 42 |
+
option = MOT_OPTION_BY_NAME.get(name)
|
| 43 |
+
if option is None:
|
| 44 |
+
raise KeyError(
|
| 45 |
+
f"Unknown MoT runtime option {name!r}; declare it in MOT_OPTIONS."
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
present = self._values.get(name)
|
| 49 |
+
if option.is_required and present is None:
|
| 50 |
+
raise RuntimeError(
|
| 51 |
+
f"MoT runtime option {name!r} is supplied by the checkpoint restore "
|
| 52 |
+
"contract and has no default. A hand-built option map must set it "
|
| 53 |
+
"explicitly; substituting a value here would silently change the "
|
| 54 |
+
"trained topology."
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# For a required option the value is present, so passing it as its own
|
| 58 |
+
# default simply routes it through the type check below.
|
| 59 |
+
default = present if option.is_required else option.default
|
| 60 |
+
if option.kind == "boolean":
|
| 61 |
+
return self.boolean(name, default)
|
| 62 |
+
if option.kind == "integer":
|
| 63 |
+
return self.integer(name, default)
|
| 64 |
+
if option.kind == "number":
|
| 65 |
+
return self.number(name, default)
|
| 66 |
+
if option.kind == "string":
|
| 67 |
+
return self.string(name, default)
|
| 68 |
+
if option.kind == "torch_dtype":
|
| 69 |
+
return self.torch_dtype(name, default)
|
| 70 |
+
raise KeyError(f"MoT option {name!r} has an unknown kind {option.kind!r}.")
|
| 71 |
+
|
| 72 |
+
def boolean(self, name: str, default: bool = False) -> bool:
|
| 73 |
+
value = self._values.get(name, default)
|
| 74 |
+
if type(value) is not bool:
|
| 75 |
+
raise TypeError(
|
| 76 |
+
f"MoT runtime option {name!r} must be a boolean, got {value!r}."
|
| 77 |
+
)
|
| 78 |
+
return value
|
| 79 |
+
|
| 80 |
+
def integer(self, name: str, default: int) -> int:
|
| 81 |
+
value = self._value(name, default)
|
| 82 |
+
if type(value) is not int:
|
| 83 |
+
raise TypeError(
|
| 84 |
+
f"MoT runtime option {name!r} must be an integer, got {value!r}."
|
| 85 |
+
)
|
| 86 |
+
return value
|
| 87 |
+
|
| 88 |
+
def string(self, name: str, default: str) -> str:
|
| 89 |
+
value = self._value(name, default)
|
| 90 |
+
if not isinstance(value, str):
|
| 91 |
+
raise TypeError(
|
| 92 |
+
f"MoT runtime option {name!r} must be a string, got {value!r}."
|
| 93 |
+
)
|
| 94 |
+
return value
|
| 95 |
+
|
| 96 |
+
def number(self, name: str, default: float) -> float:
|
| 97 |
+
value = self._value(name, default)
|
| 98 |
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
| 99 |
+
raise TypeError(
|
| 100 |
+
f"MoT runtime option {name!r} must be numeric, got {value!r}."
|
| 101 |
+
)
|
| 102 |
+
result = float(value)
|
| 103 |
+
if not math.isfinite(result):
|
| 104 |
+
raise ValueError(
|
| 105 |
+
f"MoT runtime option {name!r} must be finite, got {value!r}."
|
| 106 |
+
)
|
| 107 |
+
return result
|
| 108 |
+
|
| 109 |
+
def torch_dtype(self, name: str, default: torch.dtype) -> torch.dtype:
|
| 110 |
+
value = self._value(name, default)
|
| 111 |
+
|
| 112 |
+
if isinstance(value, torch.dtype):
|
| 113 |
+
return value
|
| 114 |
+
|
| 115 |
+
if not isinstance(value, str):
|
| 116 |
+
raise TypeError(
|
| 117 |
+
f"MoT runtime option {name!r} must resolve to a torch dtype, "
|
| 118 |
+
f"got {value!r}."
|
| 119 |
+
)
|
| 120 |
+
text = value.strip().lower()
|
| 121 |
+
if text in {"torch.bfloat16", "bfloat16", "bf16"}:
|
| 122 |
+
return torch.bfloat16
|
| 123 |
+
if text in {"torch.float16", "float16", "fp16", "half"}:
|
| 124 |
+
return torch.float16
|
| 125 |
+
if text in {"torch.float32", "float32", "fp32", "float"}:
|
| 126 |
+
return torch.float32
|
| 127 |
+
|
| 128 |
+
raise ValueError(
|
| 129 |
+
f"MoT runtime option {name!r} must resolve to a torch dtype, got {value!r}."
|
| 130 |
+
)
|
timebraid/model/mot/structures.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared structural types and tag helpers for TimeBraid."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from typing import NamedTuple, Optional
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
IGNORE_INDEX = -100
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _extract_text_position_ids(
|
| 15 |
+
position_ids: Optional[torch.LongTensor],
|
| 16 |
+
) -> Optional[torch.LongTensor]:
|
| 17 |
+
"""Normalize position ids to MoT's 2D text timeline."""
|
| 18 |
+
if position_ids is None:
|
| 19 |
+
return None
|
| 20 |
+
if position_ids.ndim == 2:
|
| 21 |
+
return position_ids
|
| 22 |
+
if position_ids.ndim == 3 and position_ids.shape[0] in {3, 4}:
|
| 23 |
+
return position_ids[0]
|
| 24 |
+
raise RuntimeError(f"Unsupported position_ids shape: {tuple(position_ids.shape)}")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
ROLE_OBSERVED = 0
|
| 28 |
+
ROLE_CONTEXT = 1
|
| 29 |
+
ROLE_TARGET = 2
|
| 30 |
+
TS_ROUTE_GENERATION = 0
|
| 31 |
+
TS_ROUTE_UNDERSTANDING = 2
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
TIMEBRAID_TS_PAYLOAD_FIELDS = (
|
| 35 |
+
"ts_values",
|
| 36 |
+
"ts_lengths",
|
| 37 |
+
"ts_loss_start_idxs",
|
| 38 |
+
"ts_loss_roi_masks",
|
| 39 |
+
"ts_roles",
|
| 40 |
+
"ts_segment_ids",
|
| 41 |
+
"ts_span_mask",
|
| 42 |
+
"ts_text_start_token_idxs",
|
| 43 |
+
"ts_text_end_token_idxs",
|
| 44 |
+
)
|
| 45 |
+
TIMEBRAID_REQUIRED_TS_PAYLOAD_FIELDS = tuple(
|
| 46 |
+
name for name in TIMEBRAID_TS_PAYLOAD_FIELDS if name != "ts_loss_roi_masks"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class TimeBraidPayload:
|
| 52 |
+
"""Curve tensors carried together through TimeBraid runtime compilation and rollout."""
|
| 53 |
+
|
| 54 |
+
ts_values: Optional[torch.Tensor] = None
|
| 55 |
+
ts_lengths: Optional[torch.Tensor] = None
|
| 56 |
+
ts_loss_start_idxs: Optional[torch.Tensor] = None
|
| 57 |
+
ts_loss_roi_masks: Optional[torch.Tensor] = None
|
| 58 |
+
ts_roles: Optional[torch.Tensor] = None
|
| 59 |
+
ts_segment_ids: Optional[torch.Tensor] = None
|
| 60 |
+
ts_span_mask: Optional[torch.Tensor] = None
|
| 61 |
+
ts_text_start_token_idxs: Optional[torch.Tensor] = None
|
| 62 |
+
ts_text_end_token_idxs: Optional[torch.Tensor] = None
|
| 63 |
+
|
| 64 |
+
@classmethod
|
| 65 |
+
def pop_from_kwargs(cls, kwargs: dict[str, object]) -> "TimeBraidPayload":
|
| 66 |
+
return cls(
|
| 67 |
+
**{name: kwargs.pop(name, None) for name in TIMEBRAID_TS_PAYLOAD_FIELDS}
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def inject_generation_inputs(self, model_inputs: dict[str, object]) -> None:
|
| 71 |
+
"""Flatten the payload only at the public Hugging Face model-kwargs boundary."""
|
| 72 |
+
for name in TIMEBRAID_TS_PAYLOAD_FIELDS:
|
| 73 |
+
value = getattr(self, name)
|
| 74 |
+
if value is not None:
|
| 75 |
+
model_inputs[name] = value
|
| 76 |
+
|
| 77 |
+
def has_runtime_inputs(self) -> bool:
|
| 78 |
+
return any(
|
| 79 |
+
getattr(self, name) is not None for name in TIMEBRAID_TS_PAYLOAD_FIELDS
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
def runtime_kwargs(self) -> dict[str, object]:
|
| 83 |
+
"""Flatten the payload for public model calls that retain the HF tensor ABI."""
|
| 84 |
+
return {name: getattr(self, name) for name in TIMEBRAID_TS_PAYLOAD_FIELDS}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@dataclass(frozen=True)
|
| 88 |
+
class TimeBraidLayerStep:
|
| 89 |
+
"""One LLM layer and its optional generation/understanding TimesFM layers."""
|
| 90 |
+
|
| 91 |
+
llm_layer: int
|
| 92 |
+
generation_tsfm_layer: Optional[int] = None
|
| 93 |
+
understanding_tsfm_layer: Optional[int] = None
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def build_timebraid_layer_plan(
|
| 97 |
+
*,
|
| 98 |
+
num_llm_layers: int,
|
| 99 |
+
generation_tsfm_by_llm_layer: Mapping[int, int],
|
| 100 |
+
understanding_tsfm_by_llm_layer: Mapping[int, int],
|
| 101 |
+
) -> tuple[TimeBraidLayerStep, ...]:
|
| 102 |
+
"""Materialize validated per-layer execution steps from paired-layer maps."""
|
| 103 |
+
if type(num_llm_layers) is not int or num_llm_layers <= 0:
|
| 104 |
+
raise ValueError(
|
| 105 |
+
f"num_llm_layers must be a positive integer, got {num_llm_layers!r}."
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
def _validate_tsfm_map(mapping: Mapping[int, int], *, role: str) -> dict[int, int]:
|
| 109 |
+
if not isinstance(mapping, Mapping):
|
| 110 |
+
raise TypeError(
|
| 111 |
+
f"{role}_tsfm_by_llm_layer must be a mapping, got "
|
| 112 |
+
f"{type(mapping).__name__}."
|
| 113 |
+
)
|
| 114 |
+
normalized: dict[int, int] = {}
|
| 115 |
+
for llm_layer, tsfm_layer in mapping.items():
|
| 116 |
+
if type(llm_layer) is not int or not 0 <= llm_layer < num_llm_layers:
|
| 117 |
+
raise ValueError(
|
| 118 |
+
f"{role} LLM layer must be an integer in [0, {num_llm_layers}), "
|
| 119 |
+
f"got {llm_layer!r}."
|
| 120 |
+
)
|
| 121 |
+
if type(tsfm_layer) is not int or tsfm_layer < 0:
|
| 122 |
+
raise ValueError(
|
| 123 |
+
f"{role} TimesFM layer must be a non-negative integer, got "
|
| 124 |
+
f"{tsfm_layer!r}."
|
| 125 |
+
)
|
| 126 |
+
normalized[llm_layer] = tsfm_layer
|
| 127 |
+
|
| 128 |
+
observed_tsfm_layers = [
|
| 129 |
+
normalized[llm_layer] for llm_layer in sorted(normalized)
|
| 130 |
+
]
|
| 131 |
+
expected_tsfm_layers = list(range(len(normalized)))
|
| 132 |
+
if observed_tsfm_layers != expected_tsfm_layers:
|
| 133 |
+
raise ValueError(
|
| 134 |
+
f"{role} TimesFM layers must advance in contiguous 0..N-1 order, "
|
| 135 |
+
f"got {observed_tsfm_layers}."
|
| 136 |
+
)
|
| 137 |
+
return normalized
|
| 138 |
+
|
| 139 |
+
generation = _validate_tsfm_map(generation_tsfm_by_llm_layer, role="generation")
|
| 140 |
+
if not generation:
|
| 141 |
+
raise ValueError("generation_tsfm_by_llm_layer must not be empty.")
|
| 142 |
+
understanding = _validate_tsfm_map(
|
| 143 |
+
understanding_tsfm_by_llm_layer, role="understanding"
|
| 144 |
+
)
|
| 145 |
+
return tuple(
|
| 146 |
+
TimeBraidLayerStep(
|
| 147 |
+
llm_layer=llm_layer,
|
| 148 |
+
generation_tsfm_layer=generation.get(llm_layer),
|
| 149 |
+
understanding_tsfm_layer=understanding.get(llm_layer),
|
| 150 |
+
)
|
| 151 |
+
for llm_layer in range(num_llm_layers)
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class MoTForecastLossAccounting(NamedTuple):
|
| 156 |
+
record_target_idx: torch.Tensor
|
| 157 |
+
record_patch_idx: torch.Tensor
|
| 158 |
+
target_starts: torch.Tensor
|
| 159 |
+
target_lens: torch.Tensor
|
| 160 |
+
raw_indices: torch.Tensor
|
| 161 |
+
keep_mask: torch.Tensor
|
| 162 |
+
valid_counts: torch.Tensor
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def build_mot_forecast_loss_accounting(
|
| 166 |
+
*,
|
| 167 |
+
patch_lengths: torch.Tensor,
|
| 168 |
+
patch_counts: torch.Tensor,
|
| 169 |
+
value_lengths: torch.Tensor,
|
| 170 |
+
loss_start_idxs: torch.Tensor,
|
| 171 |
+
forecast_horizon: int,
|
| 172 |
+
) -> Optional[MoTForecastLossAccounting]:
|
| 173 |
+
"""
|
| 174 |
+
Build the canonical TS forecast record mask.
|
| 175 |
+
|
| 176 |
+
`loss_start` is both a point-level supervision boundary and an owner-patch
|
| 177 |
+
eligibility boundary. A historical owner patch whose raw end is before
|
| 178 |
+
`loss_start` must not receive loss just because a long horizon overlaps the
|
| 179 |
+
future suffix.
|
| 180 |
+
"""
|
| 181 |
+
if patch_lengths.ndim != 2:
|
| 182 |
+
raise RuntimeError(
|
| 183 |
+
f"TS loss accounting expects patch_lengths [target,patch], got {tuple(patch_lengths.shape)}."
|
| 184 |
+
)
|
| 185 |
+
target_count, max_patches = int(patch_lengths.shape[0]), int(patch_lengths.shape[1])
|
| 186 |
+
for name, tensor in (
|
| 187 |
+
("patch_counts", patch_counts),
|
| 188 |
+
("value_lengths", value_lengths),
|
| 189 |
+
("loss_start_idxs", loss_start_idxs),
|
| 190 |
+
):
|
| 191 |
+
if tensor.ndim != 1 or int(tensor.shape[0]) != target_count:
|
| 192 |
+
raise RuntimeError(
|
| 193 |
+
f"TS loss accounting expects {name} [{target_count}], got {tuple(tensor.shape)}."
|
| 194 |
+
)
|
| 195 |
+
forecast_horizon = int(forecast_horizon)
|
| 196 |
+
if forecast_horizon <= 0:
|
| 197 |
+
raise RuntimeError(
|
| 198 |
+
f"TS loss accounting requires positive forecast_horizon, got {forecast_horizon}."
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
device = patch_lengths.device
|
| 202 |
+
prefix_ends = torch.cumsum(patch_lengths, dim=1)
|
| 203 |
+
patch_axis = torch.arange(max_patches, device=device, dtype=torch.long).unsqueeze(0)
|
| 204 |
+
successor_patch_mask = patch_axis < (patch_counts - 1).unsqueeze(1)
|
| 205 |
+
|
| 206 |
+
# Each record is owned by a patch and predicts the raw suffix starting right
|
| 207 |
+
# after that patch's last raw point.
|
| 208 |
+
target_starts_by_patch = prefix_ends
|
| 209 |
+
next_available_lengths = value_lengths[:, None] - prefix_ends
|
| 210 |
+
forecast_horizon_tensor = torch.full_like(prefix_ends, forecast_horizon)
|
| 211 |
+
target_lens_by_patch = torch.minimum(
|
| 212 |
+
forecast_horizon_tensor, next_available_lengths
|
| 213 |
+
)
|
| 214 |
+
keep_starts_by_patch = torch.clamp(
|
| 215 |
+
loss_start_idxs[:, None] - target_starts_by_patch, min=0
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
# An owner patch before the history/future boundary is context only. The
|
| 219 |
+
# boundary owner itself is the first record allowed to predict supervised
|
| 220 |
+
# future points.
|
| 221 |
+
owner_reaches_loss_start = prefix_ends >= loss_start_idxs[:, None]
|
| 222 |
+
base_record_mask = torch.logical_and(successor_patch_mask, owner_reaches_loss_start)
|
| 223 |
+
record_mask = torch.logical_and(
|
| 224 |
+
base_record_mask,
|
| 225 |
+
torch.logical_and(
|
| 226 |
+
target_lens_by_patch > 0, keep_starts_by_patch < target_lens_by_patch
|
| 227 |
+
),
|
| 228 |
+
)
|
| 229 |
+
record_target_idx, record_patch_idx = torch.nonzero(record_mask, as_tuple=True)
|
| 230 |
+
if int(record_target_idx.numel()) == 0:
|
| 231 |
+
return None
|
| 232 |
+
|
| 233 |
+
target_lens = target_lens_by_patch[record_target_idx, record_patch_idx]
|
| 234 |
+
target_starts = target_starts_by_patch[record_target_idx, record_patch_idx]
|
| 235 |
+
horizon_idx = torch.arange(forecast_horizon, device=device, dtype=torch.long)
|
| 236 |
+
raw_indices = target_starts.unsqueeze(1) + horizon_idx.unsqueeze(0)
|
| 237 |
+
keep_mask = torch.logical_and(
|
| 238 |
+
horizon_idx.unsqueeze(0) < target_lens.unsqueeze(1),
|
| 239 |
+
raw_indices >= loss_start_idxs[record_target_idx].unsqueeze(1),
|
| 240 |
+
)
|
| 241 |
+
nonempty_record_mask = torch.any(keep_mask, dim=1)
|
| 242 |
+
if not bool(torch.any(nonempty_record_mask)):
|
| 243 |
+
return None
|
| 244 |
+
|
| 245 |
+
keep_mask = keep_mask[nonempty_record_mask]
|
| 246 |
+
target_lens = target_lens[nonempty_record_mask]
|
| 247 |
+
target_starts = target_starts[nonempty_record_mask]
|
| 248 |
+
record_target_idx = record_target_idx[nonempty_record_mask]
|
| 249 |
+
record_patch_idx = record_patch_idx[nonempty_record_mask]
|
| 250 |
+
raw_indices = raw_indices[nonempty_record_mask]
|
| 251 |
+
valid_counts = keep_mask.to(dtype=torch.long).sum(dim=1)
|
| 252 |
+
if bool(torch.any(valid_counts <= 0)):
|
| 253 |
+
raise RuntimeError(
|
| 254 |
+
"TS loss accounting built a record with no supervised points."
|
| 255 |
+
)
|
| 256 |
+
return MoTForecastLossAccounting(
|
| 257 |
+
record_target_idx=record_target_idx,
|
| 258 |
+
record_patch_idx=record_patch_idx,
|
| 259 |
+
target_starts=target_starts,
|
| 260 |
+
target_lens=target_lens,
|
| 261 |
+
raw_indices=raw_indices,
|
| 262 |
+
keep_mask=keep_mask,
|
| 263 |
+
valid_counts=valid_counts,
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
@dataclass
|
| 268 |
+
class _Span:
|
| 269 |
+
start_token_idx: int
|
| 270 |
+
end_token_idx: int
|
timebraid/model/timebraid.py
ADDED
|
@@ -0,0 +1,1021 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HF-native TimeBraid model with directly registered language and TS components."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Iterable, Mapping, Sequence
|
| 6 |
+
from dataclasses import dataclass, fields
|
| 7 |
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn as nn
|
| 11 |
+
from transformers import (
|
| 12 |
+
AutoConfig,
|
| 13 |
+
AutoModelForCausalLM,
|
| 14 |
+
PretrainedConfig,
|
| 15 |
+
PreTrainedModel,
|
| 16 |
+
)
|
| 17 |
+
from transformers.cache_utils import Cache
|
| 18 |
+
from transformers.modeling_outputs import CausalLMOutputWithPast
|
| 19 |
+
from transformers.modeling_utils import get_parameter_dtype
|
| 20 |
+
from transformers.utils import ModelOutput
|
| 21 |
+
|
| 22 |
+
from .mot.bridge_runtime import initialize_timebraid_components
|
| 23 |
+
from .mot.bridge_runtime_mot import sync_gradient_checkpointing_from_decoder_layers
|
| 24 |
+
from .mot.config_contract import (
|
| 25 |
+
MOT_HF_CONFIG_CONTRACT_FIELDS,
|
| 26 |
+
collect_mot_hf_config_contract,
|
| 27 |
+
)
|
| 28 |
+
from .mot.generation_route import (
|
| 29 |
+
GenerationRoute,
|
| 30 |
+
require_mixed_generation_inputs,
|
| 31 |
+
resolve_generation_route,
|
| 32 |
+
)
|
| 33 |
+
from .mot.kv_cache import MoTDynamicCache
|
| 34 |
+
from .mot.model import (
|
| 35 |
+
NormalizedMoTGenerationControls,
|
| 36 |
+
TimeBraidGenerateOutput,
|
| 37 |
+
TimeBraidPayload,
|
| 38 |
+
merge_timebraid_losses,
|
| 39 |
+
normalize_mot_generation_controls,
|
| 40 |
+
run_timebraid_decoder,
|
| 41 |
+
run_timebraid_generate,
|
| 42 |
+
validate_mot_generation_input_ids,
|
| 43 |
+
)
|
| 44 |
+
from .mot.structures import IGNORE_INDEX, TIMEBRAID_TS_PAYLOAD_FIELDS
|
| 45 |
+
|
| 46 |
+
if TYPE_CHECKING: # pragma: no cover - import cycle only matters at runtime
|
| 47 |
+
from ..processing_timebraid import TimeBraidBatchFeature
|
| 48 |
+
|
| 49 |
+
_FINITE_SCAN_CHUNK_ELEMENTS = 8 * 1024 * 1024
|
| 50 |
+
_NON_INFERENCE_CONFIG_FIELDS = (
|
| 51 |
+
"mot_timesfm_model_name_or_path",
|
| 52 |
+
"mot_ts_roi_mse_alpha",
|
| 53 |
+
"mot_ts_loss_weight",
|
| 54 |
+
"mot_lm_loss_weight",
|
| 55 |
+
"mot_ts_understanding_loss_weight",
|
| 56 |
+
)
|
| 57 |
+
_FORBIDDEN_CONFIG_FIELDS = _NON_INFERENCE_CONFIG_FIELDS
|
| 58 |
+
_MOT_GENERATION_CONTROL_FIELDS = (
|
| 59 |
+
"mot_target_horizons",
|
| 60 |
+
"mot_forecast_head_len",
|
| 61 |
+
"mot_target_total_lengths",
|
| 62 |
+
"mot_target_history_span_idxs",
|
| 63 |
+
"mot_return_forecast_quantiles",
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class TimeBraidLossBreakdown(ModelOutput):
|
| 69 |
+
"""Fixed scalar loss accounting returned beside the differentiable loss."""
|
| 70 |
+
|
| 71 |
+
loss: Optional[torch.Tensor] = None
|
| 72 |
+
lm_loss: Optional[torch.Tensor] = None
|
| 73 |
+
ts_aux_loss: Optional[torch.Tensor] = None
|
| 74 |
+
ts_aux_point_loss: Optional[torch.Tensor] = None
|
| 75 |
+
ts_aux_quantile_loss: Optional[torch.Tensor] = None
|
| 76 |
+
ts_aux_global_point_loss: Optional[torch.Tensor] = None
|
| 77 |
+
ts_aux_global_quantile_loss: Optional[torch.Tensor] = None
|
| 78 |
+
ts_aux_global_uncapped_loss: Optional[torch.Tensor] = None
|
| 79 |
+
ts_aux_global_cap_scale: Optional[torch.Tensor] = None
|
| 80 |
+
ts_aux_roi_loss: Optional[torch.Tensor] = None
|
| 81 |
+
ts_aux_roi_point_loss: Optional[torch.Tensor] = None
|
| 82 |
+
ts_aux_roi_weighted_point_loss: Optional[torch.Tensor] = None
|
| 83 |
+
ts_aux_roi_uncapped_loss: Optional[torch.Tensor] = None
|
| 84 |
+
ts_aux_roi_weighted_uncapped_loss: Optional[torch.Tensor] = None
|
| 85 |
+
ts_aux_roi_cap_scale: Optional[torch.Tensor] = None
|
| 86 |
+
ts_aux_roi_supervised_points: Optional[torch.Tensor] = None
|
| 87 |
+
ts_aux_roi_supervised_targets: Optional[torch.Tensor] = None
|
| 88 |
+
ts_aux_roi_alpha: Optional[torch.Tensor] = None
|
| 89 |
+
ts_aux_understanding_loss: Optional[torch.Tensor] = None
|
| 90 |
+
ts_aux_understanding_weighted_loss: Optional[torch.Tensor] = None
|
| 91 |
+
ts_aux_understanding_time_loss: Optional[torch.Tensor] = None
|
| 92 |
+
ts_aux_understanding_fft_loss: Optional[torch.Tensor] = None
|
| 93 |
+
ts_aux_understanding_patches: Optional[torch.Tensor] = None
|
| 94 |
+
ts_aux_understanding_points: Optional[torch.Tensor] = None
|
| 95 |
+
ts_aux_understanding_weight: Optional[torch.Tensor] = None
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@dataclass
|
| 99 |
+
class TimeBraidOutput(CausalLMOutputWithPast):
|
| 100 |
+
"""Stable causal-LM output for every dictionary-returning TimeBraid forward."""
|
| 101 |
+
|
| 102 |
+
token_accuracy: Optional[torch.Tensor] = None
|
| 103 |
+
predicted_tokens: Optional[torch.Tensor] = None
|
| 104 |
+
loss_breakdown: Optional[TimeBraidLossBreakdown] = None
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _present_forbidden_config_fields(config: object) -> list[str]:
|
| 108 |
+
if isinstance(config, Mapping):
|
| 109 |
+
return sorted(field for field in _FORBIDDEN_CONFIG_FIELDS if field in config)
|
| 110 |
+
return sorted(field for field in _FORBIDDEN_CONFIG_FIELDS if hasattr(config, field))
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _validate_finite_named_tensors(
|
| 114 |
+
named_tensors: Iterable[tuple[str, torch.Tensor]],
|
| 115 |
+
*,
|
| 116 |
+
owner: str,
|
| 117 |
+
) -> None:
|
| 118 |
+
for name, tensor in named_tensors:
|
| 119 |
+
if not tensor.is_floating_point() and not tensor.is_complex():
|
| 120 |
+
continue
|
| 121 |
+
flat = tensor.detach().contiguous().view(-1)
|
| 122 |
+
for start in range(0, int(flat.numel()), _FINITE_SCAN_CHUNK_ELEMENTS):
|
| 123 |
+
chunk = flat[start : start + _FINITE_SCAN_CHUNK_ELEMENTS]
|
| 124 |
+
finite_mask = torch.isfinite(chunk)
|
| 125 |
+
if bool(torch.all(finite_mask)):
|
| 126 |
+
continue
|
| 127 |
+
chunk_index = int(
|
| 128 |
+
torch.nonzero(torch.logical_not(finite_mask), as_tuple=False)[0]
|
| 129 |
+
.detach()
|
| 130 |
+
.cpu()
|
| 131 |
+
.item()
|
| 132 |
+
)
|
| 133 |
+
raise RuntimeError(
|
| 134 |
+
f"{owner} contains a non-finite tensor value: "
|
| 135 |
+
f"name={name!r}, flat_index={start + chunk_index}, dtype={tensor.dtype}."
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _validate_finite_model_tensors(model: nn.Module, *, owner: str) -> None:
|
| 140 |
+
"""Reject non-finite floating or complex model state before use."""
|
| 141 |
+
_validate_finite_named_tensors(
|
| 142 |
+
(*model.named_parameters(), *model.named_buffers()),
|
| 143 |
+
owner=owner,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _runtime_options_from_timebraid_config(config: "TimeBraidConfig") -> dict[str, Any]:
|
| 148 |
+
"""Build direct-component initialization options from checkpoint metadata.
|
| 149 |
+
|
| 150 |
+
A contract field's name *is* its runtime option name, so this copies
|
| 151 |
+
rather than translates. The hand-maintained rename table it replaced was
|
| 152 |
+
what allowed one knob to carry two spellings and drift apart.
|
| 153 |
+
"""
|
| 154 |
+
contract = collect_mot_hf_config_contract(config)
|
| 155 |
+
options: dict[str, Any] = {
|
| 156 |
+
name: contract[name] for name in MOT_HF_CONFIG_CONTRACT_FIELDS
|
| 157 |
+
}
|
| 158 |
+
# Never restored from a checkpoint: inference always runs with gradient
|
| 159 |
+
# checkpointing off.
|
| 160 |
+
options["mot_ts_gradient_checkpointing"] = False
|
| 161 |
+
options["mot_ts_gradient_checkpointing_use_reentrant"] = False
|
| 162 |
+
compute_dtype = getattr(config, "dtype", None) or getattr(
|
| 163 |
+
config.llm_config, "dtype", None
|
| 164 |
+
)
|
| 165 |
+
if compute_dtype is not None:
|
| 166 |
+
options["mot_compute_dtype"] = compute_dtype
|
| 167 |
+
return options
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
_LLM_MIRROR_FIELDS = (
|
| 171 |
+
"vocab_size",
|
| 172 |
+
"hidden_size",
|
| 173 |
+
"intermediate_size",
|
| 174 |
+
"num_hidden_layers",
|
| 175 |
+
"num_attention_heads",
|
| 176 |
+
"num_key_value_heads",
|
| 177 |
+
"head_dim",
|
| 178 |
+
"max_position_embeddings",
|
| 179 |
+
"tie_word_embeddings",
|
| 180 |
+
"dtype",
|
| 181 |
+
"bos_token_id",
|
| 182 |
+
"eos_token_id",
|
| 183 |
+
"pad_token_id",
|
| 184 |
+
"use_cache",
|
| 185 |
+
"sliding_window",
|
| 186 |
+
"layer_types",
|
| 187 |
+
"_attn_implementation",
|
| 188 |
+
"_attn_implementation_internal",
|
| 189 |
+
)
|
| 190 |
+
_LLM_SERIALIZED_MIRROR_FIELDS = tuple(
|
| 191 |
+
field for field in _LLM_MIRROR_FIELDS if not field.startswith("_")
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class TimeBraidConfig(PretrainedConfig):
|
| 196 |
+
r"""Composite config for TimeBraid's language and time-series components."""
|
| 197 |
+
|
| 198 |
+
model_type = "timebraid"
|
| 199 |
+
is_composition = True
|
| 200 |
+
has_no_defaults_at_init = True
|
| 201 |
+
sub_configs = {"llm_config": AutoConfig}
|
| 202 |
+
|
| 203 |
+
def __init__(
|
| 204 |
+
self,
|
| 205 |
+
llm_config: Optional[PretrainedConfig | dict[str, Any]] = None,
|
| 206 |
+
**kwargs: Any,
|
| 207 |
+
) -> None:
|
| 208 |
+
if llm_config is None:
|
| 209 |
+
raise ValueError("TimeBraidConfig requires `llm_config`.")
|
| 210 |
+
forbidden_fields = _present_forbidden_config_fields(kwargs)
|
| 211 |
+
if forbidden_fields:
|
| 212 |
+
raise ValueError(
|
| 213 |
+
"TimeBraidConfig does not restore training-only or external "
|
| 214 |
+
f"TimesFM fields: {forbidden_fields}."
|
| 215 |
+
)
|
| 216 |
+
nested_forbidden_fields = _present_forbidden_config_fields(llm_config)
|
| 217 |
+
if nested_forbidden_fields:
|
| 218 |
+
raise ValueError(
|
| 219 |
+
"TimeBraidConfig llm_config does not restore training-only or "
|
| 220 |
+
f"external TimesFM fields: {nested_forbidden_fields}."
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
if isinstance(llm_config, PretrainedConfig):
|
| 224 |
+
resolved_llm_config = llm_config
|
| 225 |
+
else:
|
| 226 |
+
llm_config_dict = dict(llm_config)
|
| 227 |
+
llm_model_type = llm_config_dict.pop("model_type", None)
|
| 228 |
+
if llm_model_type is None:
|
| 229 |
+
raise ValueError(
|
| 230 |
+
"TimeBraidConfig `llm_config` must include `model_type`."
|
| 231 |
+
)
|
| 232 |
+
resolved_llm_config = AutoConfig.for_model(
|
| 233 |
+
llm_model_type, **llm_config_dict
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
outer_mirrors = sorted(
|
| 237 |
+
field for field in _LLM_SERIALIZED_MIRROR_FIELDS if field in kwargs
|
| 238 |
+
)
|
| 239 |
+
if outer_mirrors:
|
| 240 |
+
raise ValueError(
|
| 241 |
+
"TimeBraidConfig stores LLM fields only in llm_config; remove outer fields "
|
| 242 |
+
f"{outer_mirrors}."
|
| 243 |
+
)
|
| 244 |
+
kwargs.setdefault("architectures", ["TimeBraid"])
|
| 245 |
+
# PretrainedConfig applies dtype and attention overrides during its
|
| 246 |
+
# initializer. Register the nested config first so those composite
|
| 247 |
+
# controls reach Qwen instead of remaining only on the outer wrapper.
|
| 248 |
+
self.llm_config = resolved_llm_config
|
| 249 |
+
super().__init__(**kwargs)
|
| 250 |
+
self._sync_llm_surface()
|
| 251 |
+
|
| 252 |
+
@classmethod
|
| 253 |
+
def from_llm_config(
|
| 254 |
+
cls, llm_config: PretrainedConfig, **kwargs: Any
|
| 255 |
+
) -> "TimeBraidConfig":
|
| 256 |
+
return cls(llm_config=llm_config, **kwargs)
|
| 257 |
+
|
| 258 |
+
def _sync_llm_surface(self) -> None:
|
| 259 |
+
"""Derive the non-serialized wrapper surface generic HF helpers consume."""
|
| 260 |
+
for field in _LLM_MIRROR_FIELDS:
|
| 261 |
+
if hasattr(self.llm_config, field):
|
| 262 |
+
setattr(self, field, getattr(self.llm_config, field))
|
| 263 |
+
|
| 264 |
+
def to_dict(self) -> dict[str, Any]:
|
| 265 |
+
"""Serialize only inference restore state, never training objectives."""
|
| 266 |
+
payload = super().to_dict()
|
| 267 |
+
forbidden_fields = _present_forbidden_config_fields(payload)
|
| 268 |
+
if forbidden_fields:
|
| 269 |
+
raise RuntimeError(
|
| 270 |
+
"TimeBraidConfig contains forbidden training-only or external "
|
| 271 |
+
f"TimesFM fields: {forbidden_fields}."
|
| 272 |
+
)
|
| 273 |
+
for field in _LLM_SERIALIZED_MIRROR_FIELDS:
|
| 274 |
+
payload.pop(field, None)
|
| 275 |
+
nested = payload.get("llm_config")
|
| 276 |
+
if isinstance(nested, dict):
|
| 277 |
+
nested_forbidden_fields = _present_forbidden_config_fields(nested)
|
| 278 |
+
if nested_forbidden_fields:
|
| 279 |
+
raise RuntimeError(
|
| 280 |
+
"TimeBraidConfig llm_config contains forbidden training-only, "
|
| 281 |
+
f"or external TimesFM fields: {nested_forbidden_fields}."
|
| 282 |
+
)
|
| 283 |
+
return payload
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def _labels_with_language_supervision(
|
| 287 |
+
labels: Optional[torch.LongTensor],
|
| 288 |
+
) -> Optional[torch.LongTensor]:
|
| 289 |
+
if isinstance(labels, torch.Tensor) and (
|
| 290 |
+
labels.numel() == 0 or not bool(torch.any(labels.ne(IGNORE_INDEX)).item())
|
| 291 |
+
):
|
| 292 |
+
return None
|
| 293 |
+
return labels
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def _require_binary_plain_attention_mask(attention_mask: object) -> None:
|
| 297 |
+
"""Reject packed segment ids before delegating a plain call to native Qwen."""
|
| 298 |
+
# Rank-4 prepared masks remain part of Qwen's public ABI. TimeBraid packing
|
| 299 |
+
# uses positive segment ids only in the rank-2 mask produced by its
|
| 300 |
+
# processor/collator, so the plain route must accept exactly binary values
|
| 301 |
+
# there instead of letting Qwen collapse every nonzero id to one segment.
|
| 302 |
+
if not isinstance(attention_mask, torch.Tensor) or attention_mask.ndim != 2:
|
| 303 |
+
return
|
| 304 |
+
binary_values = torch.logical_or(attention_mask.eq(0), attention_mask.eq(1))
|
| 305 |
+
if not bool(torch.all(binary_values).item()):
|
| 306 |
+
raise RuntimeError(
|
| 307 |
+
"Plain Qwen fallback requires a binary 2D attention_mask with values "
|
| 308 |
+
"in {0, 1}; packed segment ids require the TimeBraid processor/collator "
|
| 309 |
+
f"payload, got dtype={attention_mask.dtype}, shape={tuple(attention_mask.shape)}."
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
_TIMEBRAID_KWARG_PREFIXES = ("mot_", "ts_")
|
| 314 |
+
_KNOWN_TIMEBRAID_KWARGS = frozenset(
|
| 315 |
+
TIMEBRAID_TS_PAYLOAD_FIELDS + _MOT_GENERATION_CONTROL_FIELDS
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def _reject_unknown_timebraid_kwargs(generation_kwargs: Mapping[str, object]) -> None:
|
| 320 |
+
"""Reject misspelled TimeBraid arguments instead of forwarding them.
|
| 321 |
+
|
| 322 |
+
`mot_` and `ts_` are TimeBraid's own argument namespace, so an unrecognized
|
| 323 |
+
name there is a mistake we can name. Unprefixed names belong to Hugging
|
| 324 |
+
Face and must keep flowing through, or this package would break whenever
|
| 325 |
+
transformers adds a generation argument.
|
| 326 |
+
|
| 327 |
+
Without this, a typo such as `mot_target_horizon` was silently forwarded,
|
| 328 |
+
the request routed to plain text, and the caller received prose with no
|
| 329 |
+
forecast and no error.
|
| 330 |
+
"""
|
| 331 |
+
unknown = sorted(
|
| 332 |
+
name
|
| 333 |
+
for name in generation_kwargs
|
| 334 |
+
if name.startswith(_TIMEBRAID_KWARG_PREFIXES)
|
| 335 |
+
and name not in _KNOWN_TIMEBRAID_KWARGS
|
| 336 |
+
)
|
| 337 |
+
if unknown:
|
| 338 |
+
raise TypeError(
|
| 339 |
+
f"Unknown TimeBraid generation argument(s) {unknown}. Supported "
|
| 340 |
+
f"names are {sorted(_KNOWN_TIMEBRAID_KWARGS)}."
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def _fixed_loss_breakdown(
|
| 345 |
+
breakdown: Optional[Mapping[str, torch.Tensor]],
|
| 346 |
+
) -> Optional[TimeBraidLossBreakdown]:
|
| 347 |
+
if breakdown is None:
|
| 348 |
+
return None
|
| 349 |
+
reference = next(
|
| 350 |
+
(value for value in breakdown.values() if isinstance(value, torch.Tensor)),
|
| 351 |
+
None,
|
| 352 |
+
)
|
| 353 |
+
if reference is None:
|
| 354 |
+
raise RuntimeError("TimeBraid loss accounting returned no tensor values.")
|
| 355 |
+
zero = reference.detach().new_zeros(())
|
| 356 |
+
values: dict[str, torch.Tensor] = {}
|
| 357 |
+
for output_field in fields(TimeBraidLossBreakdown):
|
| 358 |
+
value = breakdown.get(output_field.name)
|
| 359 |
+
if value is None:
|
| 360 |
+
value = zero.clone()
|
| 361 |
+
if not isinstance(value, torch.Tensor) or value.numel() != 1:
|
| 362 |
+
raise RuntimeError(
|
| 363 |
+
"TimeBraid loss accounting values must be scalar tensors: "
|
| 364 |
+
f"field={output_field.name!r}, value={value!r}."
|
| 365 |
+
)
|
| 366 |
+
values[output_field.name] = value.detach().reshape(())
|
| 367 |
+
return TimeBraidLossBreakdown(**values)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def _as_timebraid_output(
|
| 371 |
+
outputs: object,
|
| 372 |
+
breakdown: Optional[Mapping[str, torch.Tensor]],
|
| 373 |
+
) -> TimeBraidOutput:
|
| 374 |
+
return TimeBraidOutput(
|
| 375 |
+
loss=getattr(outputs, "loss", None),
|
| 376 |
+
logits=getattr(outputs, "logits", None),
|
| 377 |
+
past_key_values=getattr(outputs, "past_key_values", None),
|
| 378 |
+
hidden_states=getattr(outputs, "hidden_states", None),
|
| 379 |
+
attentions=getattr(outputs, "attentions", None),
|
| 380 |
+
token_accuracy=getattr(outputs, "token_accuracy", None),
|
| 381 |
+
predicted_tokens=getattr(outputs, "predicted_tokens", None),
|
| 382 |
+
loss_breakdown=_fixed_loss_breakdown(breakdown),
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def _build_lm_output(
|
| 387 |
+
*,
|
| 388 |
+
llm: PreTrainedModel,
|
| 389 |
+
hidden_states: torch.Tensor,
|
| 390 |
+
past_key_values: object,
|
| 391 |
+
decoder_hidden_states: object,
|
| 392 |
+
decoder_attentions: object,
|
| 393 |
+
labels: Optional[torch.LongTensor],
|
| 394 |
+
labels_for_loss: Optional[torch.LongTensor],
|
| 395 |
+
shift_labels: Optional[torch.LongTensor],
|
| 396 |
+
logits_to_keep: Union[int, torch.Tensor],
|
| 397 |
+
skip_logits: Optional[bool],
|
| 398 |
+
loss_kwargs: Mapping[str, object],
|
| 399 |
+
) -> TimeBraidOutput:
|
| 400 |
+
"""Project decoder hidden states and own the complete language-loss contract."""
|
| 401 |
+
if skip_logits is not None and type(skip_logits) is not bool:
|
| 402 |
+
raise TypeError(
|
| 403 |
+
f"TimeBraid forward skip_logits must be a bool or None, got {skip_logits!r}."
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
# `skip_logits=True` only ever meant "the fused Liger loss will consume the
|
| 407 |
+
# hidden states instead of logits". That path was unreachable — the flag
|
| 408 |
+
# enabling it was hardcoded False with no setter — so it is gone, and the
|
| 409 |
+
# request is refused rather than silently ignored.
|
| 410 |
+
if skip_logits:
|
| 411 |
+
raise RuntimeError(
|
| 412 |
+
"skip_logits=True is not supported: TimeBraid always projects logits "
|
| 413 |
+
"through the language head."
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
slice_indices = (
|
| 417 |
+
slice(-logits_to_keep, None)
|
| 418 |
+
if isinstance(logits_to_keep, int)
|
| 419 |
+
else logits_to_keep
|
| 420 |
+
)
|
| 421 |
+
kept_hidden_states = hidden_states[:, slice_indices, :]
|
| 422 |
+
has_effective_supervision = labels_for_loss is not None or shift_labels is not None
|
| 423 |
+
loss = None
|
| 424 |
+
# Only the removed fused-loss path produced these; the language head does
|
| 425 |
+
# not, so they stay None and keep TimeBraidOutput's shape stable.
|
| 426 |
+
token_accuracy = None
|
| 427 |
+
predicted_tokens = None
|
| 428 |
+
logits = llm.lm_head(kept_hidden_states)
|
| 429 |
+
if has_effective_supervision:
|
| 430 |
+
loss = llm.loss_function(
|
| 431 |
+
logits=logits,
|
| 432 |
+
labels=labels_for_loss,
|
| 433 |
+
shift_labels=shift_labels,
|
| 434 |
+
vocab_size=llm.config.vocab_size,
|
| 435 |
+
**loss_kwargs,
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
return TimeBraidOutput(
|
| 439 |
+
loss=loss,
|
| 440 |
+
logits=logits,
|
| 441 |
+
past_key_values=past_key_values,
|
| 442 |
+
hidden_states=decoder_hidden_states,
|
| 443 |
+
attentions=decoder_attentions,
|
| 444 |
+
token_accuracy=token_accuracy,
|
| 445 |
+
predicted_tokens=predicted_tokens,
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
class TimeBraid(PreTrainedModel):
|
| 450 |
+
r"""Top-level model with direct language, time-series, and attention components."""
|
| 451 |
+
|
| 452 |
+
# This is the complete trainable/inference component graph. Keeping the
|
| 453 |
+
# ownership list on the public model makes optional topology readable
|
| 454 |
+
# without inspecting the numeric execution helpers.
|
| 455 |
+
llm: PreTrainedModel
|
| 456 |
+
generation_tsfm: nn.Module
|
| 457 |
+
understanding_tsfm: Optional[nn.Module]
|
| 458 |
+
global_residual_attention: nn.ModuleDict
|
| 459 |
+
packed_attention: nn.Module
|
| 460 |
+
understanding_head: Optional[nn.Module]
|
| 461 |
+
|
| 462 |
+
config_class = TimeBraidConfig
|
| 463 |
+
base_model_prefix = "llm"
|
| 464 |
+
main_input_name = "input_ids"
|
| 465 |
+
_supports_flash_attn_2 = True
|
| 466 |
+
_supports_sdpa = True
|
| 467 |
+
supports_gradient_checkpointing = True
|
| 468 |
+
_tied_weights_keys = {"llm.lm_head.weight": "llm.model.embed_tokens.weight"}
|
| 469 |
+
|
| 470 |
+
def __init__(
|
| 471 |
+
self,
|
| 472 |
+
config: TimeBraidConfig,
|
| 473 |
+
llm: Optional[PreTrainedModel] = None,
|
| 474 |
+
runtime_options: Optional[dict[str, Any]] = None,
|
| 475 |
+
) -> None:
|
| 476 |
+
if not isinstance(config, TimeBraidConfig):
|
| 477 |
+
raise ValueError(
|
| 478 |
+
f"TimeBraid requires TimeBraidConfig, got {type(config).__name__}."
|
| 479 |
+
)
|
| 480 |
+
super().__init__(config)
|
| 481 |
+
self.llm = (
|
| 482 |
+
llm
|
| 483 |
+
if llm is not None
|
| 484 |
+
else AutoModelForCausalLM.from_config(config.llm_config)
|
| 485 |
+
)
|
| 486 |
+
self.config.llm_config = self.llm.config
|
| 487 |
+
self.config._sync_llm_surface()
|
| 488 |
+
self.generation_config = self.llm.generation_config
|
| 489 |
+
|
| 490 |
+
resolved_runtime_options = _runtime_options_from_timebraid_config(config)
|
| 491 |
+
if runtime_options:
|
| 492 |
+
resolved_runtime_options.update(runtime_options)
|
| 493 |
+
initialize_timebraid_components(
|
| 494 |
+
self,
|
| 495 |
+
self.llm.model.config,
|
| 496 |
+
resolved_runtime_options,
|
| 497 |
+
)
|
| 498 |
+
# Optional training integration may enable the fused hidden-to-loss tail
|
| 499 |
+
# after construction; that tail does not replace either Qwen forward method.
|
| 500 |
+
self.generation_tsfm.eval()
|
| 501 |
+
if self.understanding_tsfm is not None:
|
| 502 |
+
self.understanding_tsfm.eval()
|
| 503 |
+
self._sync_hf_loading_contracts()
|
| 504 |
+
|
| 505 |
+
def _sync_hf_loading_contracts(self) -> None:
|
| 506 |
+
"""Synchronize tied-weight and parallelism metadata without generic post-init."""
|
| 507 |
+
self._tied_weights_keys = {
|
| 508 |
+
"llm.lm_head.weight": "llm.model.embed_tokens.weight"
|
| 509 |
+
}
|
| 510 |
+
self.all_tied_weights_keys = dict(self._tied_weights_keys)
|
| 511 |
+
tied_keys = getattr(self.llm, "all_tied_weights_keys", None)
|
| 512 |
+
if tied_keys:
|
| 513 |
+
self.all_tied_weights_keys.update(
|
| 514 |
+
{f"llm.{key}": f"llm.{value}" for key, value in tied_keys.items()}
|
| 515 |
+
)
|
| 516 |
+
# Transformers expects composite wrappers to expose concrete empty
|
| 517 |
+
# parallelism plans during distributed `from_pretrained()` allocator
|
| 518 |
+
# warmup, so mirror the child plans here.
|
| 519 |
+
for plan_attr in ("_tp_plan", "_ep_plan", "_pp_plan"):
|
| 520 |
+
merged_plan = {}
|
| 521 |
+
child_plan = getattr(self.llm, plan_attr, None)
|
| 522 |
+
if child_plan:
|
| 523 |
+
merged_plan.update(
|
| 524 |
+
{f"llm.{key}": value for key, value in child_plan.copy().items()}
|
| 525 |
+
)
|
| 526 |
+
setattr(self, plan_attr, merged_plan)
|
| 527 |
+
|
| 528 |
+
def train(self, mode: bool = True) -> "TimeBraid":
|
| 529 |
+
super().train(mode)
|
| 530 |
+
# TimesFM towers stay in eval mode during full-parameter training. Module
|
| 531 |
+
# mode controls stochastic behavior independently of `requires_grad`.
|
| 532 |
+
self.generation_tsfm.eval()
|
| 533 |
+
if self.understanding_tsfm is not None:
|
| 534 |
+
self.understanding_tsfm.eval()
|
| 535 |
+
return self
|
| 536 |
+
|
| 537 |
+
def gradient_checkpointing_enable(
|
| 538 |
+
self, gradient_checkpointing_kwargs: Optional[dict[str, Any]] = None
|
| 539 |
+
) -> None:
|
| 540 |
+
super().gradient_checkpointing_enable(
|
| 541 |
+
gradient_checkpointing_kwargs=gradient_checkpointing_kwargs
|
| 542 |
+
)
|
| 543 |
+
sync_gradient_checkpointing_from_decoder_layers(self, self.llm.model.layers)
|
| 544 |
+
|
| 545 |
+
def gradient_checkpointing_disable(self) -> None:
|
| 546 |
+
super().gradient_checkpointing_disable()
|
| 547 |
+
sync_gradient_checkpointing_from_decoder_layers(self, self.llm.model.layers)
|
| 548 |
+
|
| 549 |
+
def forward(
|
| 550 |
+
self,
|
| 551 |
+
input_ids: Optional[torch.LongTensor] = None,
|
| 552 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 553 |
+
position_ids: Optional[torch.LongTensor] = None,
|
| 554 |
+
past_key_values: Optional[Cache] = None,
|
| 555 |
+
inputs_embeds: Optional[torch.FloatTensor] = None,
|
| 556 |
+
labels: Optional[torch.LongTensor] = None,
|
| 557 |
+
use_cache: Optional[bool] = None,
|
| 558 |
+
cache_position: Optional[torch.LongTensor] = None,
|
| 559 |
+
logits_to_keep: Union[int, torch.Tensor] = 0,
|
| 560 |
+
output_attentions: Optional[bool] = None,
|
| 561 |
+
output_hidden_states: Optional[bool] = None,
|
| 562 |
+
return_dict: Optional[bool] = None,
|
| 563 |
+
skip_logits: Optional[bool] = None,
|
| 564 |
+
shift_labels: Optional[torch.LongTensor] = None,
|
| 565 |
+
ts_values: object = None,
|
| 566 |
+
ts_lengths: object = None,
|
| 567 |
+
ts_loss_start_idxs: object = None,
|
| 568 |
+
ts_loss_roi_masks: object = None,
|
| 569 |
+
ts_roles: object = None,
|
| 570 |
+
ts_segment_ids: object = None,
|
| 571 |
+
ts_span_mask: object = None,
|
| 572 |
+
ts_text_start_token_idxs: object = None,
|
| 573 |
+
ts_text_end_token_idxs: object = None,
|
| 574 |
+
active_rows: Optional[torch.Tensor] = None,
|
| 575 |
+
num_items_in_batch: Optional[object] = None,
|
| 576 |
+
mot_ts_num_records_by_horizon: Optional[object] = None,
|
| 577 |
+
mot_ts_num_roi_targets_by_horizon: Optional[object] = None,
|
| 578 |
+
mot_ts_understanding_num_points: Optional[object] = None,
|
| 579 |
+
mot_ts_understanding_num_fft_elements: Optional[object] = None,
|
| 580 |
+
**kwargs: Any,
|
| 581 |
+
) -> TimeBraidOutput | tuple[Any, ...]:
|
| 582 |
+
# Generation-only controls can accompany processor batches, but never
|
| 583 |
+
# participate in the causal forward computation.
|
| 584 |
+
for key in _MOT_GENERATION_CONTROL_FIELDS:
|
| 585 |
+
kwargs.pop(key, None)
|
| 586 |
+
|
| 587 |
+
payload = TimeBraidPayload(
|
| 588 |
+
ts_values=ts_values,
|
| 589 |
+
ts_lengths=ts_lengths,
|
| 590 |
+
ts_loss_start_idxs=ts_loss_start_idxs,
|
| 591 |
+
ts_loss_roi_masks=ts_loss_roi_masks,
|
| 592 |
+
ts_roles=ts_roles,
|
| 593 |
+
ts_segment_ids=ts_segment_ids,
|
| 594 |
+
ts_span_mask=ts_span_mask,
|
| 595 |
+
ts_text_start_token_idxs=ts_text_start_token_idxs,
|
| 596 |
+
ts_text_end_token_idxs=ts_text_end_token_idxs,
|
| 597 |
+
)
|
| 598 |
+
use_timebraid_decoder = (
|
| 599 |
+
isinstance(past_key_values, MoTDynamicCache) or payload.has_runtime_inputs()
|
| 600 |
+
)
|
| 601 |
+
if not use_timebraid_decoder and isinstance(input_ids, torch.Tensor):
|
| 602 |
+
use_timebraid_decoder = bool(
|
| 603 |
+
torch.logical_or(
|
| 604 |
+
input_ids.eq(int(self.ts_open_token_id)),
|
| 605 |
+
input_ids.eq(int(self.ts_close_token_id)),
|
| 606 |
+
)
|
| 607 |
+
.any()
|
| 608 |
+
.item()
|
| 609 |
+
)
|
| 610 |
+
if active_rows is not None and not isinstance(past_key_values, MoTDynamicCache):
|
| 611 |
+
raise RuntimeError("active_rows requires an existing MoTDynamicCache.")
|
| 612 |
+
|
| 613 |
+
if return_dict is None:
|
| 614 |
+
resolved_return_dict = bool(self.llm.config.use_return_dict)
|
| 615 |
+
elif type(return_dict) is bool:
|
| 616 |
+
resolved_return_dict = return_dict
|
| 617 |
+
else:
|
| 618 |
+
raise TypeError(
|
| 619 |
+
f"TimeBraid forward return_dict must be a bool or None, got {return_dict!r}."
|
| 620 |
+
)
|
| 621 |
+
labels_for_loss = _labels_with_language_supervision(labels)
|
| 622 |
+
if not use_timebraid_decoder:
|
| 623 |
+
_require_binary_plain_attention_mask(attention_mask)
|
| 624 |
+
if skip_logits is not None and type(skip_logits) is not bool:
|
| 625 |
+
raise TypeError(
|
| 626 |
+
"TimeBraid forward skip_logits must be a bool or None, got "
|
| 627 |
+
f"{skip_logits!r}."
|
| 628 |
+
)
|
| 629 |
+
use_outer_lm_head = shift_labels is not None and labels_for_loss is None
|
| 630 |
+
if skip_logits and not use_outer_lm_head:
|
| 631 |
+
raise ValueError(
|
| 632 |
+
"skip_logits=True requires fused TimeBraid language loss."
|
| 633 |
+
)
|
| 634 |
+
plain_loss_kwargs = dict(kwargs)
|
| 635 |
+
if num_items_in_batch is not None:
|
| 636 |
+
plain_loss_kwargs["num_items_in_batch"] = num_items_in_batch
|
| 637 |
+
plain_forward_kwargs = dict(plain_loss_kwargs)
|
| 638 |
+
if shift_labels is not None:
|
| 639 |
+
plain_forward_kwargs["shift_labels"] = shift_labels
|
| 640 |
+
if use_outer_lm_head:
|
| 641 |
+
# Run Qwen's ordinary decoder, then let TimeBraid consume its
|
| 642 |
+
# hidden states. This outer tail exists for explicit
|
| 643 |
+
# shift-label-only loss, where Qwen cannot build the labels.
|
| 644 |
+
decoder_outputs = self.llm.model(
|
| 645 |
+
input_ids=input_ids,
|
| 646 |
+
attention_mask=attention_mask,
|
| 647 |
+
position_ids=position_ids,
|
| 648 |
+
past_key_values=past_key_values,
|
| 649 |
+
inputs_embeds=inputs_embeds,
|
| 650 |
+
use_cache=use_cache,
|
| 651 |
+
cache_position=cache_position,
|
| 652 |
+
output_attentions=output_attentions,
|
| 653 |
+
output_hidden_states=output_hidden_states,
|
| 654 |
+
return_dict=True,
|
| 655 |
+
**kwargs,
|
| 656 |
+
)
|
| 657 |
+
outputs = _build_lm_output(
|
| 658 |
+
llm=self.llm,
|
| 659 |
+
hidden_states=decoder_outputs.last_hidden_state,
|
| 660 |
+
past_key_values=decoder_outputs.past_key_values,
|
| 661 |
+
decoder_hidden_states=decoder_outputs.hidden_states,
|
| 662 |
+
decoder_attentions=decoder_outputs.attentions,
|
| 663 |
+
labels=labels,
|
| 664 |
+
labels_for_loss=labels_for_loss,
|
| 665 |
+
shift_labels=shift_labels,
|
| 666 |
+
logits_to_keep=logits_to_keep,
|
| 667 |
+
skip_logits=skip_logits,
|
| 668 |
+
loss_kwargs=plain_loss_kwargs,
|
| 669 |
+
)
|
| 670 |
+
else:
|
| 671 |
+
outputs = self.llm(
|
| 672 |
+
input_ids=input_ids,
|
| 673 |
+
attention_mask=attention_mask,
|
| 674 |
+
position_ids=position_ids,
|
| 675 |
+
past_key_values=past_key_values,
|
| 676 |
+
inputs_embeds=inputs_embeds,
|
| 677 |
+
labels=labels_for_loss,
|
| 678 |
+
use_cache=use_cache,
|
| 679 |
+
cache_position=cache_position,
|
| 680 |
+
logits_to_keep=logits_to_keep,
|
| 681 |
+
output_attentions=output_attentions,
|
| 682 |
+
output_hidden_states=output_hidden_states,
|
| 683 |
+
return_dict=True,
|
| 684 |
+
**plain_forward_kwargs,
|
| 685 |
+
)
|
| 686 |
+
outputs, breakdown = merge_timebraid_losses(
|
| 687 |
+
model=self,
|
| 688 |
+
mot_runtime=None,
|
| 689 |
+
outputs=outputs,
|
| 690 |
+
)
|
| 691 |
+
timebraid_output = _as_timebraid_output(outputs, breakdown)
|
| 692 |
+
if resolved_return_dict:
|
| 693 |
+
return timebraid_output
|
| 694 |
+
# Preserve Qwen's tuple prefix and ordering. Logits keeps its slot
|
| 695 |
+
# when fused execution leaves it as None, followed by optional decoder
|
| 696 |
+
# outputs; TimeBraid's token statistics append afterward.
|
| 697 |
+
tuple_output = (timebraid_output.logits,)
|
| 698 |
+
tuple_output += tuple(
|
| 699 |
+
value
|
| 700 |
+
for value in (
|
| 701 |
+
timebraid_output.past_key_values,
|
| 702 |
+
timebraid_output.hidden_states,
|
| 703 |
+
timebraid_output.attentions,
|
| 704 |
+
)
|
| 705 |
+
if value is not None
|
| 706 |
+
)
|
| 707 |
+
if timebraid_output.loss is not None:
|
| 708 |
+
tuple_output = (timebraid_output.loss,) + tuple_output
|
| 709 |
+
if timebraid_output.token_accuracy is not None:
|
| 710 |
+
tuple_output += (timebraid_output.token_accuracy,)
|
| 711 |
+
if timebraid_output.predicted_tokens is not None:
|
| 712 |
+
tuple_output += (timebraid_output.predicted_tokens,)
|
| 713 |
+
return tuple_output
|
| 714 |
+
|
| 715 |
+
if not resolved_return_dict:
|
| 716 |
+
raise RuntimeError("TS-routed TimeBraid forward requires return_dict=True.")
|
| 717 |
+
cache_implementation = kwargs.pop("cache_implementation", None)
|
| 718 |
+
if use_cache is None:
|
| 719 |
+
configured_use_cache = self.llm.model.config.use_cache
|
| 720 |
+
if type(configured_use_cache) is not bool:
|
| 721 |
+
raise TypeError(
|
| 722 |
+
"Qwen config use_cache must be a bool, got "
|
| 723 |
+
f"{configured_use_cache!r}."
|
| 724 |
+
)
|
| 725 |
+
resolved_use_cache = configured_use_cache and not self.llm.model.training
|
| 726 |
+
elif type(use_cache) is bool:
|
| 727 |
+
resolved_use_cache = use_cache
|
| 728 |
+
else:
|
| 729 |
+
raise TypeError(
|
| 730 |
+
f"TimeBraid forward use_cache must be a bool or None, got {use_cache!r}."
|
| 731 |
+
)
|
| 732 |
+
decoder_result = run_timebraid_decoder(
|
| 733 |
+
owner=self,
|
| 734 |
+
text_model=self.llm.model,
|
| 735 |
+
payload=payload,
|
| 736 |
+
input_ids=input_ids,
|
| 737 |
+
attention_mask=attention_mask,
|
| 738 |
+
position_ids=position_ids,
|
| 739 |
+
past_key_values=past_key_values,
|
| 740 |
+
inputs_embeds=inputs_embeds,
|
| 741 |
+
use_cache=resolved_use_cache,
|
| 742 |
+
cache_position=cache_position,
|
| 743 |
+
output_attentions=output_attentions,
|
| 744 |
+
output_hidden_states=output_hidden_states,
|
| 745 |
+
return_dict=True,
|
| 746 |
+
active_rows=active_rows,
|
| 747 |
+
cache_implementation=cache_implementation,
|
| 748 |
+
collect_incremental_kv_cache=resolved_use_cache,
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
loss_kwargs = dict(kwargs)
|
| 752 |
+
if num_items_in_batch is not None:
|
| 753 |
+
loss_kwargs["num_items_in_batch"] = num_items_in_batch
|
| 754 |
+
outputs = _build_lm_output(
|
| 755 |
+
llm=self.llm,
|
| 756 |
+
hidden_states=decoder_result.hidden_states,
|
| 757 |
+
past_key_values=decoder_result.past_key_values,
|
| 758 |
+
decoder_hidden_states=None,
|
| 759 |
+
decoder_attentions=None,
|
| 760 |
+
labels=labels,
|
| 761 |
+
labels_for_loss=labels_for_loss,
|
| 762 |
+
shift_labels=shift_labels,
|
| 763 |
+
logits_to_keep=logits_to_keep,
|
| 764 |
+
skip_logits=skip_logits,
|
| 765 |
+
loss_kwargs=loss_kwargs,
|
| 766 |
+
)
|
| 767 |
+
outputs, breakdown = merge_timebraid_losses(
|
| 768 |
+
model=self,
|
| 769 |
+
mot_runtime=decoder_result.mot_runtime,
|
| 770 |
+
outputs=outputs,
|
| 771 |
+
mot_ts_num_records_by_horizon=mot_ts_num_records_by_horizon,
|
| 772 |
+
mot_ts_num_roi_targets_by_horizon=mot_ts_num_roi_targets_by_horizon,
|
| 773 |
+
mot_ts_understanding_num_points=mot_ts_understanding_num_points,
|
| 774 |
+
mot_ts_understanding_num_fft_elements=(
|
| 775 |
+
mot_ts_understanding_num_fft_elements
|
| 776 |
+
),
|
| 777 |
+
)
|
| 778 |
+
return _as_timebraid_output(outputs, breakdown)
|
| 779 |
+
|
| 780 |
+
def save_pretrained(self, *args: Any, **kwargs: Any) -> None:
|
| 781 |
+
# Match Transformers' own dtype authority, but update both composite
|
| 782 |
+
# config surfaces before the base implementation serializes either one.
|
| 783 |
+
if self.config.llm_config is not self.llm.config:
|
| 784 |
+
raise RuntimeError(
|
| 785 |
+
"TimeBraid save requires config.llm_config to be the live LLM config."
|
| 786 |
+
)
|
| 787 |
+
# The checkpoint is self-contained, so machine-local source paths are
|
| 788 |
+
# neither restore semantics nor useful public provenance.
|
| 789 |
+
self.config._name_or_path = ""
|
| 790 |
+
self.config.llm_config._name_or_path = ""
|
| 791 |
+
self.config.llm_config.dtype = get_parameter_dtype(self)
|
| 792 |
+
self.config._sync_llm_surface()
|
| 793 |
+
return super().save_pretrained(*args, **kwargs)
|
| 794 |
+
|
| 795 |
+
def prepare_inputs_for_generation(
|
| 796 |
+
self, *args: Any, **kwargs: Any
|
| 797 |
+
) -> dict[str, Any]:
|
| 798 |
+
return self.llm.prepare_inputs_for_generation(*args, **kwargs)
|
| 799 |
+
|
| 800 |
+
def generate_mixed(
|
| 801 |
+
self,
|
| 802 |
+
inputs: "TimeBraidBatchFeature",
|
| 803 |
+
*,
|
| 804 |
+
max_new_tokens: Optional[int] = None,
|
| 805 |
+
max_length: Optional[int] = None,
|
| 806 |
+
mot_target_total_lengths: Optional[Sequence[int]] = None,
|
| 807 |
+
mot_forecast_head_len: Optional[int] = None,
|
| 808 |
+
mot_return_forecast_quantiles: bool = False,
|
| 809 |
+
eos_token_id: Optional[Union[int, Sequence[int]]] = None,
|
| 810 |
+
pad_token_id: Optional[int] = None,
|
| 811 |
+
use_cache: Optional[bool] = None,
|
| 812 |
+
cache_implementation: Optional[str] = None,
|
| 813 |
+
) -> TimeBraidGenerateOutput:
|
| 814 |
+
"""Generate mixed text and time series, or raise.
|
| 815 |
+
|
| 816 |
+
`inputs` is the mapping returned by
|
| 817 |
+
`TimeBraidProcessor.apply_chat_template`, which carries `input_ids`,
|
| 818 |
+
`attention_mask`, the complete TS payload, and the target horizons.
|
| 819 |
+
Everything the processor cannot know is a keyword argument here.
|
| 820 |
+
|
| 821 |
+
This is the explicit counterpart to `generate`. It states its whole
|
| 822 |
+
contract in its signature, so a misspelled argument is a `TypeError`
|
| 823 |
+
rather than a silent downgrade, and an argument this scheduler cannot
|
| 824 |
+
honour — `temperature`, `logits_processor`, anything sampling-related —
|
| 825 |
+
is simply not a parameter. Unlike `generate`, it never falls back to
|
| 826 |
+
plain text: if the request does not resolve to mixed generation it
|
| 827 |
+
raises and says which signal was missing.
|
| 828 |
+
|
| 829 |
+
Supply exactly one of `max_new_tokens` or `max_length`.
|
| 830 |
+
|
| 831 |
+
TimeBraid's own arguments keep the `mot_` prefix they carry everywhere
|
| 832 |
+
else — in `config.json`, in the processor's output, and in `generate`'s
|
| 833 |
+
keyword arguments — so one name means one thing at every boundary.
|
| 834 |
+
"""
|
| 835 |
+
require_mixed_generation_inputs(inputs)
|
| 836 |
+
input_ids = validate_mot_generation_input_ids(inputs["input_ids"])
|
| 837 |
+
batch_size = int(input_ids.shape[0])
|
| 838 |
+
|
| 839 |
+
payload = TimeBraidPayload(
|
| 840 |
+
**{name: inputs.get(name) for name in TIMEBRAID_TS_PAYLOAD_FIELDS}
|
| 841 |
+
)
|
| 842 |
+
controls = NormalizedMoTGenerationControls.normalize(
|
| 843 |
+
batch_size=batch_size,
|
| 844 |
+
# Supplied by the processor, which owns the prompt surface.
|
| 845 |
+
target_horizons=inputs.get("mot_target_horizons"),
|
| 846 |
+
target_history_span_idxs=inputs.get("mot_target_history_span_idxs"),
|
| 847 |
+
# Supplied by the caller: the processor cannot know these.
|
| 848 |
+
target_total_lengths=mot_target_total_lengths,
|
| 849 |
+
forecast_head_len=mot_forecast_head_len,
|
| 850 |
+
return_forecast_quantiles=mot_return_forecast_quantiles,
|
| 851 |
+
)
|
| 852 |
+
|
| 853 |
+
# No route resolution here: `require_mixed_generation_inputs` above has
|
| 854 |
+
# already established the one signal that matters, a complete TS
|
| 855 |
+
# payload. Inferring intent is the compatibility shim's problem.
|
| 856 |
+
if controls.target_horizons is None:
|
| 857 |
+
raise RuntimeError(
|
| 858 |
+
"generate_mixed requires `mot_target_horizons` in `inputs`. Pass "
|
| 859 |
+
"`horizon=` to TimeBraidProcessor.apply_chat_template, which "
|
| 860 |
+
"supplies it; use 0 to forbid TS rollout."
|
| 861 |
+
)
|
| 862 |
+
|
| 863 |
+
generation_kwargs: dict[str, Any] = {}
|
| 864 |
+
for name, value in (
|
| 865 |
+
("max_new_tokens", max_new_tokens),
|
| 866 |
+
("max_length", max_length),
|
| 867 |
+
("eos_token_id", eos_token_id),
|
| 868 |
+
("pad_token_id", pad_token_id),
|
| 869 |
+
("use_cache", use_cache),
|
| 870 |
+
("cache_implementation", cache_implementation),
|
| 871 |
+
):
|
| 872 |
+
if value is not None:
|
| 873 |
+
generation_kwargs[name] = value
|
| 874 |
+
|
| 875 |
+
return run_timebraid_generate(
|
| 876 |
+
model=self,
|
| 877 |
+
text_model=self.llm.model,
|
| 878 |
+
payload=payload,
|
| 879 |
+
input_ids=input_ids,
|
| 880 |
+
attention_mask=inputs.get("attention_mask"),
|
| 881 |
+
controls=controls,
|
| 882 |
+
kwargs=generation_kwargs,
|
| 883 |
+
)
|
| 884 |
+
|
| 885 |
+
def generate(
|
| 886 |
+
self, *args: Any, **kwargs: Any
|
| 887 |
+
) -> TimeBraidGenerateOutput | torch.Tensor | ModelOutput:
|
| 888 |
+
"""Hugging Face-compatible dispatch over `**kwargs`.
|
| 889 |
+
|
| 890 |
+
Kept so `model.generate(**processor_output, max_new_tokens=...)` keeps
|
| 891 |
+
working, and so plain-text requests still reach the host language
|
| 892 |
+
model. It resolves a route and delegates; the contract for mixed
|
| 893 |
+
generation lives on `generate_mixed`.
|
| 894 |
+
"""
|
| 895 |
+
generation_kwargs = dict(kwargs)
|
| 896 |
+
payload = TimeBraidPayload.pop_from_kwargs(generation_kwargs)
|
| 897 |
+
generation_controls = {
|
| 898 |
+
key: generation_kwargs.pop(key)
|
| 899 |
+
for key in _MOT_GENERATION_CONTROL_FIELDS
|
| 900 |
+
if key in generation_kwargs
|
| 901 |
+
}
|
| 902 |
+
_reject_unknown_timebraid_kwargs(generation_kwargs)
|
| 903 |
+
|
| 904 |
+
positional_input = args[0] if args else None
|
| 905 |
+
keyword_input = generation_kwargs.get("input_ids")
|
| 906 |
+
hf_input = generation_kwargs.get("inputs")
|
| 907 |
+
route_input = (
|
| 908 |
+
positional_input
|
| 909 |
+
if positional_input is not None
|
| 910 |
+
else (keyword_input if keyword_input is not None else hf_input)
|
| 911 |
+
)
|
| 912 |
+
if isinstance(route_input, torch.Tensor) and route_input.ndim == 2:
|
| 913 |
+
control_batch_size = int(route_input.shape[0])
|
| 914 |
+
elif isinstance(payload.ts_values, torch.Tensor) and payload.ts_values.ndim > 0:
|
| 915 |
+
control_batch_size = int(payload.ts_values.shape[0])
|
| 916 |
+
else:
|
| 917 |
+
control_batch_size = 1
|
| 918 |
+
controls = normalize_mot_generation_controls(
|
| 919 |
+
generation_controls,
|
| 920 |
+
batch_size=control_batch_size,
|
| 921 |
+
)
|
| 922 |
+
|
| 923 |
+
if isinstance(generation_kwargs.get("past_key_values"), MoTDynamicCache):
|
| 924 |
+
raise RuntimeError(
|
| 925 |
+
"MoT native mixed generate accepts a fresh prompt and owns its full "
|
| 926 |
+
"phase scheduler; continue a public forward cache through "
|
| 927 |
+
"model.forward instead."
|
| 928 |
+
)
|
| 929 |
+
|
| 930 |
+
decision = resolve_generation_route(
|
| 931 |
+
payload=payload,
|
| 932 |
+
route_input=route_input,
|
| 933 |
+
target_horizons=controls.target_horizons,
|
| 934 |
+
target_total_lengths=controls.target_total_lengths,
|
| 935 |
+
ts_open_token_id=int(self.ts_open_token_id),
|
| 936 |
+
ts_close_token_id=int(self.ts_close_token_id),
|
| 937 |
+
)
|
| 938 |
+
if decision.route is GenerationRoute.PLAIN_TEXT:
|
| 939 |
+
_require_binary_plain_attention_mask(
|
| 940 |
+
generation_kwargs.get("attention_mask")
|
| 941 |
+
)
|
| 942 |
+
return self.llm.generate(*args, **generation_kwargs)
|
| 943 |
+
if controls.target_horizons is None:
|
| 944 |
+
raise RuntimeError(
|
| 945 |
+
"Mixed generation requires explicit `mot_target_horizons`. Pass "
|
| 946 |
+
"`horizon=` to TimeBraidProcessor.apply_chat_template, which "
|
| 947 |
+
"supplies it; use 0 to forbid TS rollout, or a positive integer "
|
| 948 |
+
"to allow one generated TS span."
|
| 949 |
+
)
|
| 950 |
+
|
| 951 |
+
if len(args) > 1:
|
| 952 |
+
raise TypeError(
|
| 953 |
+
"TS generation accepts at most one positional input tensor."
|
| 954 |
+
)
|
| 955 |
+
provided_inputs = [
|
| 956 |
+
value
|
| 957 |
+
for value in (positional_input, keyword_input, hf_input)
|
| 958 |
+
if value is not None
|
| 959 |
+
]
|
| 960 |
+
if len(provided_inputs) != 1:
|
| 961 |
+
raise ValueError(
|
| 962 |
+
"TS generation requires exactly one of positional inputs, "
|
| 963 |
+
"input_ids, or inputs."
|
| 964 |
+
)
|
| 965 |
+
input_ids = validate_mot_generation_input_ids(provided_inputs[0])
|
| 966 |
+
generation_kwargs.pop("input_ids", None)
|
| 967 |
+
generation_kwargs.pop("inputs", None)
|
| 968 |
+
attention_mask = generation_kwargs.pop("attention_mask", None)
|
| 969 |
+
return run_timebraid_generate(
|
| 970 |
+
model=self,
|
| 971 |
+
text_model=self.llm.model,
|
| 972 |
+
payload=payload,
|
| 973 |
+
input_ids=input_ids,
|
| 974 |
+
attention_mask=attention_mask,
|
| 975 |
+
controls=controls,
|
| 976 |
+
kwargs=generation_kwargs,
|
| 977 |
+
)
|
| 978 |
+
|
| 979 |
+
def get_input_embeddings(self) -> nn.Module:
|
| 980 |
+
return self.llm.get_input_embeddings()
|
| 981 |
+
|
| 982 |
+
def set_input_embeddings(self, value: nn.Module) -> None:
|
| 983 |
+
self.llm.set_input_embeddings(value)
|
| 984 |
+
|
| 985 |
+
def get_output_embeddings(self) -> Optional[nn.Module]:
|
| 986 |
+
return self.llm.get_output_embeddings()
|
| 987 |
+
|
| 988 |
+
def set_output_embeddings(self, value: nn.Module) -> None:
|
| 989 |
+
self.llm.set_output_embeddings(value)
|
| 990 |
+
|
| 991 |
+
def tie_weights(self) -> None:
|
| 992 |
+
super().tie_weights()
|
| 993 |
+
self._sync_hf_loading_contracts()
|
| 994 |
+
|
| 995 |
+
def can_generate(self) -> bool:
|
| 996 |
+
llm = self._modules.get("llm") if hasattr(self, "_modules") else None
|
| 997 |
+
return True if llm is None else llm.can_generate()
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
+
def register_timebraid_auto_classes() -> None:
|
| 1001 |
+
"""Register TimeBraid with HF auto classes in the active Python process."""
|
| 1002 |
+
from transformers import AutoProcessor
|
| 1003 |
+
|
| 1004 |
+
from ..processing_timebraid import TimeBraidProcessor
|
| 1005 |
+
|
| 1006 |
+
AutoConfig.register(TimeBraidConfig.model_type, TimeBraidConfig, exist_ok=True)
|
| 1007 |
+
AutoProcessor.register(TimeBraidConfig, TimeBraidProcessor, exist_ok=True)
|
| 1008 |
+
# Deliberately not registered with plain `AutoModel`: that class means
|
| 1009 |
+
# "base model without a task head", and TimeBraid is a causal LM. The
|
| 1010 |
+
# published auto_map must not advertise it either — see AUTO_MAP in
|
| 1011 |
+
# scripts/build-huggingface-timebraid-model.py.
|
| 1012 |
+
AutoModelForCausalLM.register(TimeBraidConfig, TimeBraid, exist_ok=True)
|
| 1013 |
+
|
| 1014 |
+
|
| 1015 |
+
__all__ = [
|
| 1016 |
+
"TimeBraid",
|
| 1017 |
+
"TimeBraidConfig",
|
| 1018 |
+
"TimeBraidLossBreakdown",
|
| 1019 |
+
"TimeBraidOutput",
|
| 1020 |
+
"register_timebraid_auto_classes",
|
| 1021 |
+
]
|
timebraid/processing_timebraid.py
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hugging Face processor for TimeBraid text and time-series generation."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
from collections.abc import Mapping, Sequence
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from transformers import ProcessorMixin
|
| 12 |
+
from transformers.feature_extraction_utils import BatchFeature
|
| 13 |
+
|
| 14 |
+
from .data.mot_utils import (
|
| 15 |
+
ROLE_TO_ID,
|
| 16 |
+
build_mot_batch_timeseries_tensors,
|
| 17 |
+
normalize_timeseries_spans,
|
| 18 |
+
)
|
| 19 |
+
from .model.mot.generation_route import FinishReason
|
| 20 |
+
|
| 21 |
+
_ASSISTANT_PREFIX = "<|im_start|>assistant\n<think>\n\n</think>\n\n"
|
| 22 |
+
_ASSISTANT_SUFFIX = "<|im_end|>"
|
| 23 |
+
_NO_THINK_SUFFIX = "/no_think"
|
| 24 |
+
_STAT_SIGNIFICANT_DIGITS = 6
|
| 25 |
+
_STAT_DECIMAL_LOWER_BOUND = 1.0e-4
|
| 26 |
+
_STAT_DECIMAL_UPPER_BOUND = 1.0e6
|
| 27 |
+
_QWEN_STOP_TOKENS = ("<|im_end|>", "<|endoftext|>")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _coerce_finite_float(value: float, *, name: str) -> float:
|
| 31 |
+
coerced = float(value)
|
| 32 |
+
if not math.isfinite(coerced):
|
| 33 |
+
raise ValueError(f"{name} must be finite, got {value!r}")
|
| 34 |
+
return coerced
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _normalize_scientific_notation(text: str) -> str:
|
| 38 |
+
mantissa, exponent = text.split("e")
|
| 39 |
+
mantissa = mantissa.rstrip("0").rstrip(".")
|
| 40 |
+
if mantissa in {"", "-0"}:
|
| 41 |
+
return "0"
|
| 42 |
+
sign = ""
|
| 43 |
+
if exponent.startswith("-"):
|
| 44 |
+
sign = "-"
|
| 45 |
+
digits = exponent.lstrip("+-0") or "0"
|
| 46 |
+
return f"{mantissa}e{sign}{digits}"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _strip_fixed_decimal(text: str) -> str:
|
| 50 |
+
stripped = text.rstrip("0").rstrip(".") if "." in text else text
|
| 51 |
+
return "0" if stripped in {"", "-0"} else stripped
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def format_stat(
|
| 55 |
+
value: float,
|
| 56 |
+
significant_digits: int = _STAT_SIGNIFICANT_DIGITS,
|
| 57 |
+
decimal_lower_bound: float = _STAT_DECIMAL_LOWER_BOUND,
|
| 58 |
+
decimal_upper_bound: float = _STAT_DECIMAL_UPPER_BOUND,
|
| 59 |
+
) -> str:
|
| 60 |
+
"""Format one prompt-visible normalization statistic."""
|
| 61 |
+
if significant_digits <= 0:
|
| 62 |
+
raise ValueError(
|
| 63 |
+
f"significant_digits must be positive, got {significant_digits}"
|
| 64 |
+
)
|
| 65 |
+
if decimal_lower_bound <= 0:
|
| 66 |
+
raise ValueError(
|
| 67 |
+
f"decimal_lower_bound must be positive, got {decimal_lower_bound}"
|
| 68 |
+
)
|
| 69 |
+
if decimal_upper_bound <= decimal_lower_bound:
|
| 70 |
+
raise ValueError(
|
| 71 |
+
"decimal_upper_bound must be greater than decimal_lower_bound, got "
|
| 72 |
+
f"{decimal_upper_bound} <= {decimal_lower_bound}"
|
| 73 |
+
)
|
| 74 |
+
coerced = _coerce_finite_float(value, name="value")
|
| 75 |
+
if coerced == 0.0:
|
| 76 |
+
return "0"
|
| 77 |
+
magnitude = abs(coerced)
|
| 78 |
+
if decimal_lower_bound <= magnitude < decimal_upper_bound:
|
| 79 |
+
exponent = math.floor(math.log10(magnitude))
|
| 80 |
+
decimals = max(0, significant_digits - 1 - exponent)
|
| 81 |
+
rendered = _strip_fixed_decimal(f"{coerced:.{decimals}f}")
|
| 82 |
+
else:
|
| 83 |
+
rendered = _normalize_scientific_notation(
|
| 84 |
+
f"{coerced:.{significant_digits - 1}e}"
|
| 85 |
+
)
|
| 86 |
+
parsed = float(rendered)
|
| 87 |
+
if rendered in {"", "-0"} or (coerced != 0.0 and parsed == 0.0):
|
| 88 |
+
raise ValueError(
|
| 89 |
+
"Formatted normalization statistic lost its nonzero magnitude: "
|
| 90 |
+
f"value={coerced!r}, rendered={rendered!r}."
|
| 91 |
+
)
|
| 92 |
+
relative_error = abs(parsed - coerced) / abs(coerced)
|
| 93 |
+
if relative_error > 10.0 ** (1 - significant_digits):
|
| 94 |
+
raise ValueError(
|
| 95 |
+
"Formatted normalization statistic lost too much precision: "
|
| 96 |
+
f"value={coerced!r}, rendered={rendered!r}."
|
| 97 |
+
)
|
| 98 |
+
return rendered
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def compute_zscore_stats(
|
| 102 |
+
values: Sequence[float], eps: float = 1.0e-6
|
| 103 |
+
) -> tuple[float, float]:
|
| 104 |
+
"""Return population mean and epsilon-floored population standard deviation."""
|
| 105 |
+
if not values:
|
| 106 |
+
raise ValueError("Time-series values must be non-empty.")
|
| 107 |
+
mean = sum(values) / float(len(values))
|
| 108 |
+
variance = sum((value - mean) ** 2 for value in values) / float(len(values))
|
| 109 |
+
std = max(variance**0.5, float(eps))
|
| 110 |
+
if not math.isfinite(mean) or not math.isfinite(std):
|
| 111 |
+
raise ValueError("Time-series z-score statistics must be finite.")
|
| 112 |
+
return float(mean), float(std)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def zscore_with_stats(values: Sequence[float], mean: float, std: float) -> list[float]:
|
| 116 |
+
"""Normalize finite values with explicit statistics."""
|
| 117 |
+
if not math.isfinite(std) or std <= 0.0:
|
| 118 |
+
raise ValueError(f"std must be finite and positive, got {std!r}.")
|
| 119 |
+
normalized = [(float(value) - mean) / std for value in values]
|
| 120 |
+
if any(not math.isfinite(value) for value in normalized):
|
| 121 |
+
raise ValueError("Time-series z-score values must be finite.")
|
| 122 |
+
return normalized
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def build_inline_named_zscore_span_reference(
|
| 126 |
+
*, length_tag: int, mean: float, std: float, precision: int | None = None
|
| 127 |
+
) -> str:
|
| 128 |
+
"""Render the canonical stats block and generic TS delimiter pair."""
|
| 129 |
+
del precision
|
| 130 |
+
if type(length_tag) is not int or length_tag <= 0:
|
| 131 |
+
raise ValueError(f"length_tag must be a positive integer, got {length_tag!r}.")
|
| 132 |
+
return (
|
| 133 |
+
f"<stats>len={length_tag}, mean={format_stat(mean)}, "
|
| 134 |
+
f"std={format_stat(std)}</stats> <ts></ts>"
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def format_qwen_chat_turns(turns: Sequence[tuple[str, str]]) -> str:
|
| 139 |
+
"""Render the canonical Qwen transcript used by TimeBraid."""
|
| 140 |
+
if not turns:
|
| 141 |
+
raise ValueError("Chat turns must be non-empty.")
|
| 142 |
+
rendered: list[str] = []
|
| 143 |
+
for index, (role, content) in enumerate(turns):
|
| 144 |
+
if role not in {"system", "user", "assistant"}:
|
| 145 |
+
raise ValueError(f"Unsupported chat role {role!r} at index {index}.")
|
| 146 |
+
allow_empty_scaffold = role == "assistant" and index == len(turns) - 1
|
| 147 |
+
if not content and not allow_empty_scaffold:
|
| 148 |
+
raise ValueError(f"Chat content must be non-empty at index {index}.")
|
| 149 |
+
if role == "assistant":
|
| 150 |
+
rendered.append(
|
| 151 |
+
f"<|im_start|>assistant\n<think>\n\n</think>\n\n{content}<|im_end|>\n"
|
| 152 |
+
)
|
| 153 |
+
else:
|
| 154 |
+
rendered.append(f"<|im_start|>{role}\n{content}<|im_end|>\n")
|
| 155 |
+
return "".join(rendered)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _qwen_stop_token_ids(tokenizer: Any) -> list[int]:
|
| 159 |
+
"""Resolve only the canonical single-token Qwen generation stops."""
|
| 160 |
+
resolved: list[int] = []
|
| 161 |
+
eos_token_id = getattr(tokenizer, "eos_token_id", None)
|
| 162 |
+
if type(eos_token_id) is int and eos_token_id >= 0:
|
| 163 |
+
resolved.append(eos_token_id)
|
| 164 |
+
for token in _QWEN_STOP_TOKENS:
|
| 165 |
+
token_ids = tokenizer(token, add_special_tokens=False)["input_ids"]
|
| 166 |
+
if not isinstance(token_ids, list) or len(token_ids) != 1:
|
| 167 |
+
continue
|
| 168 |
+
token_id = int(token_ids[0])
|
| 169 |
+
if tokenizer.unk_token_id is not None and token_id == int(
|
| 170 |
+
tokenizer.unk_token_id
|
| 171 |
+
):
|
| 172 |
+
continue
|
| 173 |
+
if token_id not in resolved:
|
| 174 |
+
resolved.append(token_id)
|
| 175 |
+
return resolved
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
@dataclass(frozen=True, slots=True)
|
| 179 |
+
class TimeBraidRequestContext:
|
| 180 |
+
"""What `post_process_generation` needs that the tensors do not carry.
|
| 181 |
+
|
| 182 |
+
Typed rather than a bare dict because it travels beside the batch: a
|
| 183 |
+
serving worker that hands the tensors to another process needs to know
|
| 184 |
+
exactly what else must go with them, and a 9-key dict documented nowhere
|
| 185 |
+
could not tell it.
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
prompt_text: str
|
| 189 |
+
horizon: int | None
|
| 190 |
+
target_series_index: int
|
| 191 |
+
normalization: Any
|
| 192 |
+
prompt_width: int
|
| 193 |
+
prompt_tokens: int
|
| 194 |
+
num_timeseries_spans: int
|
| 195 |
+
eos_token_ids: Sequence[int]
|
| 196 |
+
pad_token_id: int | None
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class TimeBraidBatchFeature(BatchFeature):
|
| 200 |
+
"""Tensor inputs plus the non-mapping context needed for decoding.
|
| 201 |
+
|
| 202 |
+
The context rides as an attribute rather than as batch data because it is
|
| 203 |
+
not a tensor. `BatchFeature.to()` returns self, so the documented
|
| 204 |
+
processor -> generate -> post_process flow keeps it; `{**inputs}` and any
|
| 205 |
+
cross-process hand-off do not. Pass `context=` explicitly there.
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
def __init__(
|
| 209 |
+
self, data: dict[str, Any], *, postprocess_context: TimeBraidRequestContext
|
| 210 |
+
):
|
| 211 |
+
if not isinstance(postprocess_context, TimeBraidRequestContext):
|
| 212 |
+
raise TypeError(
|
| 213 |
+
"postprocess_context must be a TimeBraidRequestContext, got "
|
| 214 |
+
f"{type(postprocess_context).__name__}."
|
| 215 |
+
)
|
| 216 |
+
super().__init__(data=data)
|
| 217 |
+
self.postprocess_context = postprocess_context
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
class TimeBraidProcessor(ProcessorMixin):
|
| 221 |
+
"""Prepare one TimeBraid completion and decode its mixed generation output."""
|
| 222 |
+
|
| 223 |
+
attributes = ["tokenizer"]
|
| 224 |
+
tokenizer_class = "AutoTokenizer"
|
| 225 |
+
|
| 226 |
+
def __init__(
|
| 227 |
+
self,
|
| 228 |
+
tokenizer,
|
| 229 |
+
max_spans_per_sample: int = 64,
|
| 230 |
+
normalization_epsilon: float = 1.0e-6,
|
| 231 |
+
) -> None:
|
| 232 |
+
if type(max_spans_per_sample) is not int or max_spans_per_sample <= 0:
|
| 233 |
+
raise ValueError(
|
| 234 |
+
"max_spans_per_sample must be a positive integer, got "
|
| 235 |
+
f"{max_spans_per_sample!r}."
|
| 236 |
+
)
|
| 237 |
+
if (
|
| 238 |
+
isinstance(normalization_epsilon, bool)
|
| 239 |
+
or not isinstance(normalization_epsilon, (int, float))
|
| 240 |
+
or not math.isfinite(float(normalization_epsilon))
|
| 241 |
+
or float(normalization_epsilon) <= 0.0
|
| 242 |
+
):
|
| 243 |
+
raise ValueError(
|
| 244 |
+
"normalization_epsilon must be finite and positive, got "
|
| 245 |
+
f"{normalization_epsilon!r}."
|
| 246 |
+
)
|
| 247 |
+
self.max_spans_per_sample = max_spans_per_sample
|
| 248 |
+
self.normalization_epsilon = float(normalization_epsilon)
|
| 249 |
+
super().__init__(tokenizer)
|
| 250 |
+
self._validate_delimiters()
|
| 251 |
+
|
| 252 |
+
@property
|
| 253 |
+
def model_input_names(self) -> list[str]:
|
| 254 |
+
return [
|
| 255 |
+
"input_ids",
|
| 256 |
+
"attention_mask",
|
| 257 |
+
"ts_values",
|
| 258 |
+
"ts_lengths",
|
| 259 |
+
"ts_loss_start_idxs",
|
| 260 |
+
"ts_loss_roi_masks",
|
| 261 |
+
"ts_roles",
|
| 262 |
+
"ts_segment_ids",
|
| 263 |
+
"ts_span_mask",
|
| 264 |
+
"ts_text_start_token_idxs",
|
| 265 |
+
"ts_text_end_token_idxs",
|
| 266 |
+
"mot_target_horizons",
|
| 267 |
+
"mot_target_history_span_idxs",
|
| 268 |
+
]
|
| 269 |
+
|
| 270 |
+
def _validate_delimiters(self) -> None:
|
| 271 |
+
resolved: list[int] = []
|
| 272 |
+
for delimiter in ("<ts>", "</ts>"):
|
| 273 |
+
token_ids = self.tokenizer(delimiter, add_special_tokens=False)["input_ids"]
|
| 274 |
+
if not isinstance(token_ids, list) or len(token_ids) != 1:
|
| 275 |
+
raise ValueError(
|
| 276 |
+
f"TimeBraid tokenizer must encode {delimiter!r} as one token, got {token_ids!r}."
|
| 277 |
+
)
|
| 278 |
+
token_id = int(token_ids[0])
|
| 279 |
+
if self.tokenizer.unk_token_id is not None and token_id == int(
|
| 280 |
+
self.tokenizer.unk_token_id
|
| 281 |
+
):
|
| 282 |
+
raise ValueError(
|
| 283 |
+
f"TimeBraid tokenizer resolves {delimiter!r} to unk_token_id={token_id}."
|
| 284 |
+
)
|
| 285 |
+
resolved.append(token_id)
|
| 286 |
+
if resolved[0] == resolved[1]:
|
| 287 |
+
raise ValueError("TimeBraid TS delimiters must use distinct token IDs.")
|
| 288 |
+
|
| 289 |
+
def __call__(
|
| 290 |
+
self,
|
| 291 |
+
*,
|
| 292 |
+
messages: Sequence[Mapping[str, str]],
|
| 293 |
+
timeseries: Sequence[Sequence[float]] | None = None,
|
| 294 |
+
horizon: int | None = None,
|
| 295 |
+
target_series_index: int | None = None,
|
| 296 |
+
return_tensors: str = "pt",
|
| 297 |
+
) -> TimeBraidBatchFeature:
|
| 298 |
+
return self.apply_chat_template(
|
| 299 |
+
messages,
|
| 300 |
+
timeseries=timeseries,
|
| 301 |
+
horizon=horizon,
|
| 302 |
+
target_series_index=target_series_index,
|
| 303 |
+
add_generation_prompt=True,
|
| 304 |
+
tokenize=True,
|
| 305 |
+
return_dict=True,
|
| 306 |
+
return_tensors=return_tensors,
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
def apply_chat_template(
|
| 310 |
+
self,
|
| 311 |
+
messages: Sequence[Mapping[str, str]],
|
| 312 |
+
*,
|
| 313 |
+
timeseries: Sequence[Sequence[float]] | None = None,
|
| 314 |
+
horizon: int | None = None,
|
| 315 |
+
target_series_index: int | None = None,
|
| 316 |
+
add_generation_prompt: bool = True,
|
| 317 |
+
tokenize: bool = True,
|
| 318 |
+
return_dict: bool = True,
|
| 319 |
+
return_tensors: str = "pt",
|
| 320 |
+
) -> TimeBraidBatchFeature:
|
| 321 |
+
"""Prepare one request; a positive horizon starts numeric forecasting."""
|
| 322 |
+
if add_generation_prompt is not True:
|
| 323 |
+
raise ValueError(
|
| 324 |
+
"add_generation_prompt must be true for TimeBraid inference."
|
| 325 |
+
)
|
| 326 |
+
if tokenize is not True:
|
| 327 |
+
raise ValueError("tokenize must be true for TimeBraid inference.")
|
| 328 |
+
if return_dict is not True:
|
| 329 |
+
raise ValueError("return_dict must be true for TimeBraid inference.")
|
| 330 |
+
if return_tensors != "pt":
|
| 331 |
+
raise ValueError(f"return_tensors must be 'pt', got {return_tensors!r}.")
|
| 332 |
+
normalized_messages = self._normalize_messages(messages)
|
| 333 |
+
raw_series = self._normalize_raw_timeseries(
|
| 334 |
+
[] if timeseries is None else timeseries
|
| 335 |
+
)
|
| 336 |
+
if horizon is not None and (type(horizon) is not int or horizon <= 0):
|
| 337 |
+
raise ValueError(f"horizon must be a positive integer, got {horizon!r}.")
|
| 338 |
+
resolved_target_series_index: int | None = None
|
| 339 |
+
if horizon is None:
|
| 340 |
+
if target_series_index is not None:
|
| 341 |
+
raise ValueError("target_series_index requires a forecast horizon.")
|
| 342 |
+
else:
|
| 343 |
+
if not raw_series:
|
| 344 |
+
raise ValueError("horizon requires at least one input time series.")
|
| 345 |
+
if target_series_index is None:
|
| 346 |
+
if len(raw_series) > 1:
|
| 347 |
+
raise ValueError(
|
| 348 |
+
"target_series_index is required when forecasting from "
|
| 349 |
+
"multiple input time series."
|
| 350 |
+
)
|
| 351 |
+
resolved_target_series_index = 0
|
| 352 |
+
elif type(target_series_index) is not int:
|
| 353 |
+
raise ValueError(
|
| 354 |
+
"target_series_index must be an integer, got "
|
| 355 |
+
f"{target_series_index!r}."
|
| 356 |
+
)
|
| 357 |
+
elif not 0 <= target_series_index < len(raw_series):
|
| 358 |
+
raise ValueError(
|
| 359 |
+
"target_series_index must select an input time series, got "
|
| 360 |
+
f"{target_series_index!r} for {len(raw_series)} series."
|
| 361 |
+
)
|
| 362 |
+
else:
|
| 363 |
+
resolved_target_series_index = target_series_index
|
| 364 |
+
|
| 365 |
+
spans: list[dict[str, Any]] = []
|
| 366 |
+
normalization: list[dict[str, float | str]] = []
|
| 367 |
+
references: list[str] = []
|
| 368 |
+
for values in raw_series:
|
| 369 |
+
try:
|
| 370 |
+
mean, std = compute_zscore_stats(values, self.normalization_epsilon)
|
| 371 |
+
except (OverflowError, ValueError) as exc:
|
| 372 |
+
raise ValueError(
|
| 373 |
+
"Time-series values cannot be z-score normalized."
|
| 374 |
+
) from exc
|
| 375 |
+
normalized_values = zscore_with_stats(values, mean=mean, std=std)
|
| 376 |
+
role = "context" if horizon is not None else "observed"
|
| 377 |
+
spans.append(
|
| 378 |
+
{
|
| 379 |
+
"len": len(normalized_values),
|
| 380 |
+
"role": role,
|
| 381 |
+
"values": normalized_values,
|
| 382 |
+
"loss_start": len(normalized_values),
|
| 383 |
+
}
|
| 384 |
+
)
|
| 385 |
+
references.append(
|
| 386 |
+
build_inline_named_zscore_span_reference(
|
| 387 |
+
length_tag=len(values), mean=mean, std=std
|
| 388 |
+
)
|
| 389 |
+
)
|
| 390 |
+
normalization.append(
|
| 391 |
+
{
|
| 392 |
+
"method": "history_population_zscore",
|
| 393 |
+
"mean": mean,
|
| 394 |
+
"std": std,
|
| 395 |
+
"epsilon": self.normalization_epsilon,
|
| 396 |
+
}
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
if references:
|
| 400 |
+
references_text = "\n".join(
|
| 401 |
+
f"Series {index + 1}: {reference}"
|
| 402 |
+
for index, reference in enumerate(references)
|
| 403 |
+
)
|
| 404 |
+
target_text = ""
|
| 405 |
+
if horizon is not None and len(references) > 1:
|
| 406 |
+
target_text = (
|
| 407 |
+
"\nForecast target: "
|
| 408 |
+
f"Series {int(resolved_target_series_index) + 1}."
|
| 409 |
+
)
|
| 410 |
+
normalized_messages[-1] = (
|
| 411 |
+
"user",
|
| 412 |
+
normalized_messages[-1][1]
|
| 413 |
+
+ "\n\nTime series inputs:\n"
|
| 414 |
+
+ references_text
|
| 415 |
+
+ target_text,
|
| 416 |
+
)
|
| 417 |
+
last_role, last_content = normalized_messages[-1]
|
| 418 |
+
normalized_messages[-1] = (
|
| 419 |
+
last_role,
|
| 420 |
+
last_content.rstrip() + "\n" + _NO_THINK_SUFFIX,
|
| 421 |
+
)
|
| 422 |
+
transcript = format_qwen_chat_turns([*normalized_messages, ("assistant", "")])
|
| 423 |
+
if not transcript.endswith(_ASSISTANT_PREFIX + _ASSISTANT_SUFFIX + "\n"):
|
| 424 |
+
raise RuntimeError("TimeBraid assistant scaffold rendering drifted.")
|
| 425 |
+
prompt = transcript[: -len(_ASSISTANT_SUFFIX + "\n")]
|
| 426 |
+
|
| 427 |
+
previous_padding_side = getattr(self.tokenizer, "padding_side", None)
|
| 428 |
+
self.tokenizer.padding_side = "left"
|
| 429 |
+
try:
|
| 430 |
+
tokenized = self.tokenizer(
|
| 431 |
+
[prompt],
|
| 432 |
+
add_special_tokens=False,
|
| 433 |
+
padding=True,
|
| 434 |
+
return_tensors="pt",
|
| 435 |
+
)
|
| 436 |
+
finally:
|
| 437 |
+
if previous_padding_side is not None:
|
| 438 |
+
self.tokenizer.padding_side = previous_padding_side
|
| 439 |
+
input_ids = tokenized["input_ids"]
|
| 440 |
+
attention_mask = tokenized["attention_mask"]
|
| 441 |
+
data: dict[str, Any] = {
|
| 442 |
+
"input_ids": input_ids,
|
| 443 |
+
"attention_mask": attention_mask,
|
| 444 |
+
}
|
| 445 |
+
normalized_spans = normalize_timeseries_spans(
|
| 446 |
+
spans,
|
| 447 |
+
sample_idx=0,
|
| 448 |
+
require_segment_id=False,
|
| 449 |
+
default_segment_id=1,
|
| 450 |
+
max_spans_per_sample=self.max_spans_per_sample,
|
| 451 |
+
)
|
| 452 |
+
if normalized_spans:
|
| 453 |
+
data.update(
|
| 454 |
+
build_mot_batch_timeseries_tensors(
|
| 455 |
+
tokenizer=self.tokenizer,
|
| 456 |
+
input_ids_by_sample=input_ids.tolist(),
|
| 457 |
+
spans_by_sample=[normalized_spans],
|
| 458 |
+
max_spans_per_sample=self.max_spans_per_sample,
|
| 459 |
+
)
|
| 460 |
+
)
|
| 461 |
+
data["mot_target_horizons"] = torch.tensor(
|
| 462 |
+
[0 if horizon is None else horizon], dtype=torch.long
|
| 463 |
+
)
|
| 464 |
+
if horizon is not None:
|
| 465 |
+
data["mot_target_history_span_idxs"] = torch.tensor(
|
| 466 |
+
[int(resolved_target_series_index)], dtype=torch.long
|
| 467 |
+
)
|
| 468 |
+
if len(normalized_spans) >= self.max_spans_per_sample:
|
| 469 |
+
raise ValueError(
|
| 470 |
+
"Forecasting requires one additional output span; provide "
|
| 471 |
+
f"fewer than {self.max_spans_per_sample} input time series."
|
| 472 |
+
)
|
| 473 |
+
# Forecasting is an explicit output request, so use the existing
|
| 474 |
+
# open-target protocol instead of asking the LM to emit <ts>.
|
| 475 |
+
# All input series and text remain visible; only the selected
|
| 476 |
+
# series supplies the numeric target's history and scale.
|
| 477 |
+
self._open_forecast_target(data, int(resolved_target_series_index))
|
| 478 |
+
prompt += "<ts>"
|
| 479 |
+
input_ids = data["input_ids"]
|
| 480 |
+
attention_mask = data["attention_mask"]
|
| 481 |
+
return TimeBraidBatchFeature(
|
| 482 |
+
data,
|
| 483 |
+
postprocess_context=TimeBraidRequestContext(
|
| 484 |
+
prompt_text=prompt,
|
| 485 |
+
horizon=horizon,
|
| 486 |
+
target_series_index=resolved_target_series_index,
|
| 487 |
+
normalization=normalization,
|
| 488 |
+
prompt_width=int(input_ids.shape[1]),
|
| 489 |
+
prompt_tokens=int(attention_mask[0].sum().item()),
|
| 490 |
+
num_timeseries_spans=len(normalized_spans)
|
| 491 |
+
+ (1 if horizon is not None else 0),
|
| 492 |
+
eos_token_ids=_qwen_stop_token_ids(self.tokenizer),
|
| 493 |
+
pad_token_id=self.tokenizer.pad_token_id,
|
| 494 |
+
),
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
def _open_forecast_target(
|
| 498 |
+
self, data: dict[str, Any], target_series_index: int
|
| 499 |
+
) -> None:
|
| 500 |
+
"""Seed a separate assistant target from the selected observed history."""
|
| 501 |
+
input_ids = data["input_ids"]
|
| 502 |
+
open_position = int(input_ids.shape[1])
|
| 503 |
+
open_id = self.tokenizer.convert_tokens_to_ids("<ts>")
|
| 504 |
+
data["input_ids"] = torch.cat(
|
| 505 |
+
[input_ids, input_ids.new_tensor([[open_id]])], dim=1
|
| 506 |
+
)
|
| 507 |
+
mask = data["attention_mask"]
|
| 508 |
+
data["attention_mask"] = torch.cat([mask, mask.new_ones((1, 1))], dim=1)
|
| 509 |
+
for field, value in list(data.items()):
|
| 510 |
+
if field.startswith("ts_"):
|
| 511 |
+
data[field] = torch.cat(
|
| 512 |
+
[value, value[:, target_series_index : target_series_index + 1]],
|
| 513 |
+
dim=1,
|
| 514 |
+
)
|
| 515 |
+
data["ts_roles"][0, -1] = ROLE_TO_ID["target"]
|
| 516 |
+
data["ts_text_start_token_idxs"][0, -1] = open_position
|
| 517 |
+
data["ts_text_end_token_idxs"][0, -1] = -1
|
| 518 |
+
|
| 519 |
+
def _normalize_messages(
|
| 520 |
+
self, messages: Sequence[Mapping[str, str]]
|
| 521 |
+
) -> list[tuple[str, str]]:
|
| 522 |
+
if not isinstance(messages, Sequence) or isinstance(messages, (str, bytes)):
|
| 523 |
+
raise ValueError("messages must be a non-empty sequence.")
|
| 524 |
+
normalized: list[tuple[str, str]] = []
|
| 525 |
+
for index, message in enumerate(messages):
|
| 526 |
+
if not isinstance(message, Mapping):
|
| 527 |
+
raise ValueError(f"messages[{index}] must be an object.")
|
| 528 |
+
role = message.get("role")
|
| 529 |
+
content = message.get("content")
|
| 530 |
+
if role not in {"system", "user", "assistant"}:
|
| 531 |
+
raise ValueError(f"messages[{index}] has unsupported role {role!r}.")
|
| 532 |
+
if not isinstance(content, str) or not content.strip():
|
| 533 |
+
raise ValueError(f"messages[{index}].content must be non-empty text.")
|
| 534 |
+
try:
|
| 535 |
+
content.encode("utf-8", errors="strict")
|
| 536 |
+
except UnicodeEncodeError as exc:
|
| 537 |
+
raise ValueError(
|
| 538 |
+
f"messages[{index}].content must be valid UTF-8 text."
|
| 539 |
+
) from exc
|
| 540 |
+
normalized.append((str(role), content.strip()))
|
| 541 |
+
if not normalized or normalized[-1][0] != "user":
|
| 542 |
+
raise ValueError("messages must end with a user turn.")
|
| 543 |
+
return normalized
|
| 544 |
+
|
| 545 |
+
def _normalize_raw_timeseries(
|
| 546 |
+
self, timeseries: Sequence[Sequence[float]]
|
| 547 |
+
) -> list[list[float]]:
|
| 548 |
+
if not isinstance(timeseries, Sequence) or isinstance(timeseries, (str, bytes)):
|
| 549 |
+
raise ValueError("timeseries must be a sequence of numeric sequences.")
|
| 550 |
+
normalized: list[list[float]] = []
|
| 551 |
+
for series_index, series in enumerate(timeseries):
|
| 552 |
+
if not isinstance(series, Sequence) or isinstance(series, (str, bytes)):
|
| 553 |
+
raise ValueError(
|
| 554 |
+
f"timeseries[{series_index}] must be a numeric sequence."
|
| 555 |
+
)
|
| 556 |
+
values: list[float] = []
|
| 557 |
+
for value_index, value in enumerate(series):
|
| 558 |
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
| 559 |
+
raise ValueError(
|
| 560 |
+
f"timeseries[{series_index}][{value_index}] must be finite."
|
| 561 |
+
)
|
| 562 |
+
try:
|
| 563 |
+
casted = float(value)
|
| 564 |
+
except (OverflowError, ValueError) as exc:
|
| 565 |
+
raise ValueError(
|
| 566 |
+
f"timeseries[{series_index}][{value_index}] must be finite."
|
| 567 |
+
) from exc
|
| 568 |
+
if not math.isfinite(casted):
|
| 569 |
+
raise ValueError(
|
| 570 |
+
f"timeseries[{series_index}][{value_index}] must be finite."
|
| 571 |
+
)
|
| 572 |
+
values.append(casted)
|
| 573 |
+
if not values:
|
| 574 |
+
raise ValueError(f"timeseries[{series_index}] must be non-empty.")
|
| 575 |
+
normalized.append(values)
|
| 576 |
+
return normalized
|
| 577 |
+
|
| 578 |
+
def post_process_generation(
|
| 579 |
+
self,
|
| 580 |
+
output: Any,
|
| 581 |
+
*,
|
| 582 |
+
model_inputs: TimeBraidBatchFeature | None = None,
|
| 583 |
+
context: TimeBraidRequestContext | None = None,
|
| 584 |
+
) -> dict[str, Any]:
|
| 585 |
+
"""Decode one mixed generation result.
|
| 586 |
+
|
| 587 |
+
Supply either `model_inputs` — the object `apply_chat_template`
|
| 588 |
+
returned, which carries its own context — or `context` directly. The
|
| 589 |
+
second form exists because the context is an attribute rather than
|
| 590 |
+
batch data, so `{**inputs}` and any cross-process hand-off drop it.
|
| 591 |
+
"""
|
| 592 |
+
if model_inputs is not None and context is not None:
|
| 593 |
+
raise TypeError("Pass either model_inputs or context, not both.")
|
| 594 |
+
if context is None:
|
| 595 |
+
if model_inputs is None:
|
| 596 |
+
raise TypeError(
|
| 597 |
+
"post_process_generation needs the request context: pass "
|
| 598 |
+
"`model_inputs=` with the object returned by "
|
| 599 |
+
"apply_chat_template, or `context=` with its "
|
| 600 |
+
"`.postprocess_context` if the batch was rebuilt or moved "
|
| 601 |
+
"between processes."
|
| 602 |
+
)
|
| 603 |
+
if not isinstance(model_inputs, TimeBraidBatchFeature):
|
| 604 |
+
raise TypeError("model_inputs must be returned by TimeBraidProcessor.")
|
| 605 |
+
context = model_inputs.postprocess_context
|
| 606 |
+
if not isinstance(context, TimeBraidRequestContext):
|
| 607 |
+
raise TypeError(
|
| 608 |
+
"context must be a TimeBraidRequestContext, got "
|
| 609 |
+
f"{type(context).__name__}."
|
| 610 |
+
)
|
| 611 |
+
generated_ids = getattr(output, "sequences", output)
|
| 612 |
+
if not isinstance(generated_ids, torch.Tensor) or generated_ids.ndim != 2:
|
| 613 |
+
raise TypeError("generation output must expose sequences shaped [1, L].")
|
| 614 |
+
if int(generated_ids.shape[0]) != 1:
|
| 615 |
+
raise ValueError("TimeBraid postprocessing supports one request at a time.")
|
| 616 |
+
prompt_width = int(context.prompt_width)
|
| 617 |
+
suffix_ids = [
|
| 618 |
+
int(token_id)
|
| 619 |
+
for token_id in generated_ids[0, prompt_width:].detach().cpu().tolist()
|
| 620 |
+
]
|
| 621 |
+
completion_tokens = len(suffix_ids)
|
| 622 |
+
eos_hit = False
|
| 623 |
+
eos_token_ids = set(context.eos_token_ids)
|
| 624 |
+
for index, token_id in enumerate(suffix_ids):
|
| 625 |
+
if token_id in eos_token_ids:
|
| 626 |
+
completion_tokens = index + 1
|
| 627 |
+
suffix_ids = suffix_ids[:index]
|
| 628 |
+
eos_hit = True
|
| 629 |
+
break
|
| 630 |
+
if not eos_hit:
|
| 631 |
+
pad_token_id = context.pad_token_id
|
| 632 |
+
while (
|
| 633 |
+
suffix_ids
|
| 634 |
+
and pad_token_id is not None
|
| 635 |
+
and suffix_ids[-1] == int(pad_token_id)
|
| 636 |
+
):
|
| 637 |
+
suffix_ids.pop()
|
| 638 |
+
completion_tokens = len(suffix_ids)
|
| 639 |
+
decoded = self.tokenizer.decode(suffix_ids, skip_special_tokens=False)
|
| 640 |
+
text = decoded.split(_ASSISTANT_SUFFIX, 1)[0].strip()
|
| 641 |
+
if "</think>" in text:
|
| 642 |
+
text = text.split("</think>", 1)[1].strip()
|
| 643 |
+
text = text.replace("<ts>", "").replace("</ts>", "").strip()
|
| 644 |
+
|
| 645 |
+
horizon = context.horizon
|
| 646 |
+
generated_ts = getattr(output, "generated_ts_values", None)
|
| 647 |
+
normalized_values: list[float] = []
|
| 648 |
+
if generated_ts is not None:
|
| 649 |
+
if generated_ts == []:
|
| 650 |
+
generated_ts = [[]]
|
| 651 |
+
if not isinstance(generated_ts, list) or len(generated_ts) != 1:
|
| 652 |
+
raise ValueError(
|
| 653 |
+
"Generated time-series values are not request-aligned."
|
| 654 |
+
)
|
| 655 |
+
normalized_values = [float(value) for value in generated_ts[0]]
|
| 656 |
+
if horizon is None and normalized_values:
|
| 657 |
+
raise ValueError("A horizon-free completion emitted time-series values.")
|
| 658 |
+
if horizon is not None and len(normalized_values) != int(horizon):
|
| 659 |
+
raise ValueError(
|
| 660 |
+
"Generated time-series horizon mismatch: "
|
| 661 |
+
f"expected={horizon}, got={len(normalized_values)}."
|
| 662 |
+
)
|
| 663 |
+
if any(not math.isfinite(value) for value in normalized_values):
|
| 664 |
+
raise ValueError("Generated time-series values must be finite.")
|
| 665 |
+
|
| 666 |
+
rollout_records = getattr(output, "rollout_records", None)
|
| 667 |
+
record = None
|
| 668 |
+
if rollout_records is not None:
|
| 669 |
+
if rollout_records == []:
|
| 670 |
+
rollout_records = None
|
| 671 |
+
if rollout_records is not None:
|
| 672 |
+
if not isinstance(rollout_records, list) or len(rollout_records) != 1:
|
| 673 |
+
raise ValueError("Generation rollout records are not request-aligned.")
|
| 674 |
+
record = rollout_records[0]
|
| 675 |
+
if not isinstance(record, Mapping):
|
| 676 |
+
raise TypeError("Generation rollout record must be an object.")
|
| 677 |
+
timeseries_result = None
|
| 678 |
+
if normalized_values:
|
| 679 |
+
normalization = context.normalization
|
| 680 |
+
target_series_index = context.target_series_index
|
| 681 |
+
if type(
|
| 682 |
+
target_series_index
|
| 683 |
+
) is not int or not 0 <= target_series_index < len(normalization):
|
| 684 |
+
raise ValueError(
|
| 685 |
+
"Numeric generation requires a valid target normalization record."
|
| 686 |
+
)
|
| 687 |
+
mean = float(normalization[target_series_index]["mean"])
|
| 688 |
+
std = float(normalization[target_series_index]["std"])
|
| 689 |
+
values = [value * std + mean for value in normalized_values]
|
| 690 |
+
if any(not math.isfinite(value) for value in values):
|
| 691 |
+
raise ValueError("Generated time-series denormalization overflowed.")
|
| 692 |
+
timeseries_result = {
|
| 693 |
+
"values": values,
|
| 694 |
+
"normalized_values": normalized_values,
|
| 695 |
+
"target_series_index": target_series_index,
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
decode_impl = "hf_generate"
|
| 699 |
+
finish_reason = "stop" if eos_hit else "length"
|
| 700 |
+
if record is not None:
|
| 701 |
+
decode_impl = str(record.get("decode_impl") or "")
|
| 702 |
+
scheduler_finish = str(record.get("finish_reason") or "")
|
| 703 |
+
if scheduler_finish == FinishReason.EOS_OR_PROTOCOL_STOP:
|
| 704 |
+
finish_reason = "stop"
|
| 705 |
+
elif scheduler_finish == FinishReason.TEXT_BUDGET:
|
| 706 |
+
finish_reason = "length"
|
| 707 |
+
else:
|
| 708 |
+
raise ValueError(
|
| 709 |
+
f"Unknown TimeBraid scheduler finish reason {scheduler_finish!r}."
|
| 710 |
+
)
|
| 711 |
+
return {
|
| 712 |
+
"content": text,
|
| 713 |
+
"timeseries": timeseries_result,
|
| 714 |
+
"target_series_index": context.target_series_index,
|
| 715 |
+
"normalization": context.normalization or None,
|
| 716 |
+
"finish_reason": finish_reason,
|
| 717 |
+
"prompt_tokens": int(context.prompt_tokens),
|
| 718 |
+
"completion_tokens": completion_tokens,
|
| 719 |
+
"decode_impl": decode_impl,
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
__all__ = [
|
| 724 |
+
"TimeBraidBatchFeature",
|
| 725 |
+
"TimeBraidProcessor",
|
| 726 |
+
]
|