The zoo’s first time-series forecasting foundation model. google/timesfm-2.5-200m-transformers
(Apache-2.0). Archetype = stateless single graph + host DSP (like Kokoro / Parakeet), but the
graph is a decoder-only LLM-shaped transformer and the “tokens” are numeric patches.
Decoder-only transformer over patches of the input series (patch_length 32 → one token). Given
a context of up to 16384 points it emits, at each patch position, a 128-step horizon of 10
quantiles (deciles 0.1…0.9 + mean). We forecast from the last patch. Config: hidden 1280, 20
layers, 16 heads, head_dim 80, intermediate 1280, rms_norm_eps 1e-6, RoPE θ 10000,
force_flip_invariance=True, infer_is_positive=True, decode_index=5,
use_continuous_quantile_head=True, output_quantile_len=1024.
Needs transformers ≥ 5.x for TimesFm2_5ModelForPrediction (config says 5.3.0.dev0); 4.57 only
has TimesFM 2.0 (TimesFmModelForPrediction). Keep a separate oracle venv on transformers-main.
The transformer core is familiar LLM machinery the export stack already handles:
input, post_attention, pre_feedforward,
post_feedforward; residual adds after the post-norms (x = post_norm(sub(x)) + residual).scale = softplus(scaling) · (log2(e)/√head_dim),
multiplied into Q; the attention interface is then called with scaling=1.0. (log2(e)=1.442695
— folds a base-2 softmax convention; replicate exactly.)fc1 → swish → fc2, intermediate == hidden == 1280. No gate_proj.input_ff_layer.TimesFm2_5ResidualBlock: Linear(64→1280)→swish→Linear(1280→1280) + Linear(64→1280)
skip. Input = concat([normed_patch(32), mask_bool_float(32)]) = 64-dim.output_projection_point (→ 128·10) and
output_projection_quantiles (→ 1024·10), both bias-free.The novel part is I/O, and it’s all host DSP (below).
Pure feed-forward transformer over patch tokens — no KV cache, no data-dependent control flow, static shapes:
tok_in[1,N,64] , cos[1,N,80] , sin[1,N,80] , attn_bias[1,1,N,N]
→ input_ff_layer → 20 decoder layers → output_projection_point[1,N,1280] , output_projection_quantiles[1,N,10240]
N = ctx_len / 32 (ship bundle N=64, ctx 2048). RoPE cos/sin and the additive mask are computed host-side and passed in, so the graph never sees position_ids or padding logic. Everything else is host arithmetic.
host_forecast.py)Two nested RevIN, run twice for flip-invariance:
ModelForPrediction.forward): truncate to ctx, front-pad short series to ctx
(mask=1 on pad). mu_g = input.mean(), sigma_g = input.std() (unbiased, ddof=1);
normalized = (input − mu_g)/sigma_g.Model.forward): patch the normalized series; compute running
(causal) mean/std per patch with Welford over valid points → ctx_mu, ctx_sigma [B,N];
normed_patch = (patch − ctx_mu)/ctx_sigma, masked→0. tok_in = concat([normed_patch, mask]).pos = arange(N) − num_masked_patches; RoPE from that. Mask = causal AND
key-not-padded, as one additive tensor.ctx_mu/ctx_sigma (reverse),
reshape, take the last patch → pf[B,128,10], qs[B,1024,10].−normalized; combine pf = (pf − flipq(pf⁻))/2 where
flipq reverses the quantile axis except the median (cat([x[...,:1], flip(x[...,1:])])).full[...,idx] = qs[...,idx] − qs[...,median] + full[...,median]
for each non-median quantile (median index = decode_index = 5).full_pred = full·sigma_g + mu_g. mean_pred = full_pred[...,5].The flip pass means 2 graph calls per forecast. All of 1–8 are deterministic → identical on Mac and device; only the transformer runs on the Core AI engine.
TimesFmCore vs HF captured projections (fp32): cos 1.0000000, MAE ~1e-6.fp16 bundle 462.9 MB. Mac GPU ~7 ms/graph → ~14 ms per 128-step forecast. iOS h18p AOT: clean.
model.safetensors stores MLP as mlp.ff0/mlp.ff1; transformers
remaps to fc1/fc2 on load. Load from safetensors → map ff0→fc1, ff1→fc2 (order matters).torch.finfo(float32).min (−3.4e38) becomes −inf in fp16, and
adding causal + key-padding fills overflows to −inf too → an all-masked query row → softmax NaN →
0 · NaN propagates into valid positions. Fix: build one additive mask
(allowed = causal ∧ key_valid) with a finite fill (−1e4). A finite fill makes an all-masked
row a uniform (harmless) average; the padded query positions are discarded anyway (we read the
last patch). This is invisible until you feed a short (front-padded) series._decode_and_project), the global one denormalizes the final forecast. Don’t collapse them.std is unbiased (ddof=1) for the global stats; Welford uses population variance per patch.conversion/timesfm/: timesfm_core.py (export-clean graph + safetensors loader + EngineCore),
host_forecast.py (host DSP), oracle.py (HF dump, --ctx/--short), gate_core.py (ladder 1),
export_timesfm.py (export + ladder 3, --ctx/--dtype), e2e_engine_gate.py (E2E). HF DL used
HF_HUB_DISABLE_XET=1. Oracle env = fresh venv, transformers-main; export env = coreai-models/.venv
(loads weights from safetensors, no transformers dep).