Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,21 @@ jobs:
- name: Test
run: dotnet test tests/Tsfm.Forecasting.Kronos.Tests -c Release --nologo --no-build

# Vacuous here by design: the TimesFM checkpoint is ~1.2 GB and non-commercially
# licensed, so it is never fetched on a runner. Running them anyway proves they
# compile and start; the assertions only bite on a developer machine.
- name: TimesFM tests (self-skipping without weights)
# The checkpoint tests are vacuous here by design: the TimesFM checkpoint is ~1.2 GB
# and non-commercially licensed, so it is never fetched on a runner. The batch tests
# run on a random-init model and do bite.
- name: TimesFM tests (checkpoint tests self-skip)
run: dotnet test tests/Tsfm.Forecasting.TimesFm.IntegrationTests -c Release --nologo

# The invariant tests run on a tiny random-init model and need no weights; the
# checkpoint tests self-skip, as the ~1.4 GB of t0 weights are not fetched here.
- name: t0 tests
run: dotnet test tests/Tsfm.Forecasting.T0.Tests -c Release --nologo

# Random-init model, no weights needed.
- name: Timestral tests
run: dotnet test tests/Tsfm.Forecasting.Timestral.Tests -c Release --nologo

# Packing embeds the checkpoints, so this job needs them. Platform-neutral.
pack:
runs-on: ubuntu-latest
Expand All @@ -66,23 +75,27 @@ jobs:
key: nuget-pack-${{ hashFiles('**/*.csproj', 'Directory.Build.props') }}
restore-keys: nuget-pack-

# Key on the fetch script: revisions are pinned there and change deliberately.
# Key on the fetch scripts: revisions are pinned there and change deliberately.
- name: Cache checkpoints
uses: actions/cache@v4
with:
path: checkpoints
key: checkpoints-${{ hashFiles('scripts/fetch-checkpoints.sh') }}
key: checkpoints-${{ hashFiles('scripts/fetch-checkpoints.sh', 'scripts/fetch-timestral-checkpoint.sh') }}

- name: Fetch checkpoints
run: ./scripts/fetch-checkpoints.sh
run: |
./scripts/fetch-checkpoints.sh
./scripts/fetch-timestral-checkpoint.sh # 5M and 28M, the embedded pair (~132 MB)

- name: Pack
run: dotnet pack -c Release --nologo -o artifacts

# Weights-backed, so they belong here rather than in the fast matrix, which fetches
# no checkpoints. The example is built to prove it still compiles against the API.
- name: Integration tests
run: dotnet test tests/Tsfm.Forecasting.Kronos.IntegrationTests -c Release --nologo
run: |
dotnet test tests/Tsfm.Forecasting.Kronos.IntegrationTests -c Release --nologo
dotnet test tests/Tsfm.Forecasting.Timestral.IntegrationTests -c Release --nologo

- name: Build samples
run: |
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ jobs:
global-json-file: global.json

- name: Fetch checkpoints
run: ./scripts/fetch-checkpoints.sh
run: |
./scripts/fetch-checkpoints.sh
./scripts/fetch-timestral-checkpoint.sh

- name: Verify the tag matches the declared version
if: startsWith(github.ref, 'refs/tags/v')
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ reference/parity.safetensors

# Python environment for the TimesFM parity dump.
.venv/

# t0 parity fixtures and the public datasets they are built from.
reference/*-parity.safetensors
reference/data/
96 changes: 96 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,101 @@
# Changelog

## 0.2.0

### Tsfm.Forecasting.T0 (Apache-2.0) — new

- Inference for The Forecasting Company's **t0-alpha** (102M) and **t0-beta** (256M), ported
from `tfc-t0` 0.6.0: causal scaler, patch encoder, alternating time/group transformer
with xPos rotary embedding, residual decoder, monotone quantile head.
- `T0Forecaster` takes the TimesFM shape: `ForecastJoint` over variate-major series with
known-future flags and a target index, and `Forecast` over OHLCVA for forward returns.
Unlike TimesFM, any context length is accepted (left-padded onto the patch grid and
masked), and up to 1024 steps are decoded in one pass.
- Each checkpoint's scaler convention is read from its `config.json`. t0-beta's differs
from alpha's, and running it under alpha's loads cleanly and forecasts worse.
- Verified stage by stage against the reference, both checkpoints, including the
quickstart notebook's electricity-price and M4 examples (`tests/Tsfm.Forecasting.T0.Parity`,
`reference/dump_t0_parity.py`).
- Weights are Apache-2.0 but not packaged: each exceeds nuget.org's 250 MB limit.
`scripts/fetch-t0-checkpoint.sh` fetches pinned revisions and checks their SHA-256.
- Not ported: autoregressive rollout past 1024 steps, quantile interpolation and tail
extrapolation to levels off the trained grid, and the optional symmetric averaging,
clipping and integer snapping.

- Weights are not bundled. The README gives pinned download commands with checksums for
projects that only reference the package, and `Load` names them when files are missing.

### Tsfm.Forecasting.Timestral (Apache-2.0) — new

- Inference for Mistral AI's Timestral family — 5M, 28M, 123M, 209M — ported from
`mistralai/timestral` 1.0.0: causal per-patch scaler, gated time/variate transformer with
partial RoPE, Decoupled Horizon Forecasting decoder tokens, monotone 21-quantile head.
- `TimestralForecaster` takes the shared shape (`ForecastJoint`, `Forecast`, both batches)
and applies the reference forecaster's defaults — symmetric averaging, non-negative
clipping, advancing refine frame, quantile cap — so published numbers are reproduced.
The mirrored input of symmetric averaging rides in the same forward pass.
- Verified stage by stage against the reference on all four sizes, with and without the
defaults, including the covariate path and the shared notebook examples. Faster than the
reference on CPU at every size.
- Context up to 32,768 steps, refused beyond; up to 1,024 steps forecast per pass.

### Tsfm.Forecasting.Timestral.Weights.5M and .28M (Mistral Modified MIT) — new

- Timestral-5M (21 MB) and -28M (111 MB) embedded at pinned revisions; the build checks
each payload's SHA-256 and refuses one that differs.
- **Revenue-restricted, not open source**: the licence withholds all rights from companies
above USD 20M monthly revenue. It ships verbatim as the package licence file, is
readable at run time (`Timestral5M.License`), and must be accepted on install.
- 123M and 209M are fetched by `scripts/fetch-timestral-checkpoint.sh`, not packaged.

### Tsfm.Forecasting

- `IModelWeights`, `DirectoryWeights`, `EmbeddedWeights`: a tokenizer-free checkpoint
abstraction beside `ICheckpoint`, used by the Timestral weights packages.

### Batching, across all the models

- `IQuantileForecaster` (new, in `Tsfm.Forecasting`): the shared `ForecastJoint` /
`Forecast` / `Quantiles` shape, plus `ForecastJointBatch` and `ForecastBatch` over many
entries sharing a horizon, written `[entry][step][quantile]` into a caller-sized span
(`BatchOutputLength`). Entries are `JointSeries` / `ReadOnlyMemory<float>`, so anchors
sliced from one history copy nothing. `TimesFmForecaster` and `T0Forecaster` implement it.
- t0 batches any entries in one pass — differing variates, contexts and known-future
patterns — each its own attention group, shorter contexts left-padded and masked. Entries
are ordered by context before being cut into passes of `MaxRowsPerPass` series. Verified
against the reference in the parity harness.
- TimesFM stacks entries of one shape on the model's batch axis, `MaxBatchPerPass` at a
time. The single calls now run as a batch of one; outputs are bit-identical to 0.1.3.
- `KronosForecaster.InferBatch` forecasts the final window of many series in one pass of
`batch × rollouts` rows; row `i` equals the last row `InferPath` gives for series `i`
alone, sampling included. `Infer` and `InferPath` share the refactored loop and are
bit-identical to 0.1.3.
- `KronosForecaster.Seed` makes the sampling seed a host policy over the anchor bar and a
series key (`KronosSeries.SeriesKey`, or `seriesKey:` on `Infer`/`InferPath`).
`SamplingSeeds.AnchorTime`, the default, is the 0.1.x behaviour: series anchored on one bar
share draws (common random numbers). `PerSeries` gives each key its own stream, and
`Salted` derives a fresh reproducible run from any policy. Batch invariance holds under
all of them; the default is bit-identical to 0.1.3.

### Packaging

`scripts/pack.sh` builds the whole set in one command — fetch, pack, verify — and prints
the order to upload in by hand.

All packages move to 0.2.0: the core gains the interface the model packages implement,
and a project reference packs an exact-version dependency.

### Samples and testing

- `samples/GroceryCovariates` runs on TimesFM, t0 or Timestral (`-- t0-beta`,
`-- Timestral-28M`) through the interface.
- Batch tests for TimesFM and t0 run on small random-init models, so they bite in CI
without weights; Kronos's run with the integration tests.
- `Tsfm.Forecasting.T0.Tests` checks structural invariants on a tiny random-init model, so
they run in CI without weights: a target never reads its own future, known-future rows
never read targets, time attention is causal, any context length is accepted. Behaviour
on the published checkpoints is covered too, gated on their presence.

## 0.1.3 — 2026-09-02

### Joint forecasting with known-future covariates
Expand Down
5 changes: 3 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>0.1.3</Version>
<Version>0.2.0</Version>
<Authors>Mark Hammond</Authors>
<Copyright>Copyright (c) 2026 Mark Hammond</Copyright>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand All @@ -40,7 +40,8 @@
<!-- Every package carries the licence and readme from the repository root, so the
third-party notices travel with the binaries rather than living only here. -->
<ItemGroup Condition="'$(IsPackable)' == 'true'">
<None Include="$(MSBuildThisFileDirectory)LICENSE" Pack="true" PackagePath="\" Visible="false" />
<None Include="$(MSBuildThisFileDirectory)LICENSE" Pack="true" PackagePath="\" Visible="false"
Condition="'$(PackRepositoryLicense)' != 'false'" />
<None Include="$(MSBuildThisFileDirectory)NOTICE" Pack="true" PackagePath="\" Visible="false" />
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="\" Visible="false" />
</ItemGroup>
Expand Down
73 changes: 73 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,76 @@ TimesFM WEIGHTS ARE NOT DISTRIBUTED AND CANNOT BE. The published checkpoints are
under the TimesFM Non-Commercial License v1.0 — research and evaluation only, no
production or revenue-generating use. Obtaining them is the user's own act under
Google's terms, and nothing here grants any right in them.

────────────────────────────────────────────────────────────────────────────────
Tsfm.Forecasting.T0 — Apache-2.0, NOT MIT
────────────────────────────────────────────────────────────────────────────────

That package contains software derived from tfc-t0
(https://github.com/theforecastingcompany/tfc-t0), Copyright 2026 The Forecasting
Company, licensed under the Apache License, Version 2.0. Its NOTICE carries these
attributions, reproduced here:

This product includes software developed at Datadog (https://www.datadoghq.com/),
Copyright 2025 Datadog, Inc., licensed under the Apache License 2.0.
Source: https://github.com/DataDog/toto

This product includes software derived from Chronos-2 by Amazon.com, Inc. or
its affiliates, Copyright Amazon.com, Inc. All Rights Reserved, licensed under
the Apache License 2.0.
Source: https://github.com/amazon-science/chronos-forecasting

Changes to the derived work, as required by Apache-2.0 section 4(b):

- The inference path is translated from Python/PyTorch to C#/TorchSharp: the
causal scaler, patch alignment, attention-mask construction, the patch
encoder, the time/group transformer stack with xPos rotary embedding, the
residual decoder and the monotone quantile head.
- Rotary embedding is computed directly rather than through
rotary-embedding-torch, with the same interleaved pairing and xPos scale.
- Attention returns zero for a query with no permitted key, explicitly rather
than through the reference's safe softmax.
- Training, fine-tuning, the MLX runtime, autoregressive rollout beyond one
pass, quantile interpolation and tail extrapolation, and the optional
symmetric-averaging, clipping and snapping adjustments are not ported.

No upstream source files and no weights are redistributed. The t0-alpha and
t0-beta checkpoints are Apache-2.0, published by The Forecasting Company, and
are fetched by scripts/fetch-t0-checkpoint.sh at pinned revisions.

────────────────────────────────────────────────────────────────────────────────
Tsfm.Forecasting.Timestral — Apache-2.0, NOT MIT
────────────────────────────────────────────────────────────────────────────────

That package contains software derived from Timestral
(https://github.com/mistralai/timestral), Copyright Mistral AI, licensed under the
Apache License, Version 2.0.

Changes to the derived work, as required by Apache-2.0 section 4(b):

- The inference path is translated from Python/PyTorch to C#/TorchSharp: the
causal per-patch scaler, instance scaling of known-future covariates, patch
representation, the time/variate transformer with gated attention and
partial rotary embedding, the monotone quantile head, the attention masks
and positions, and the forecaster's refine frame, quantile cap, symmetric
averaging and non-negative clipping.
- Decoder tokens are built for the horizon patches only; the reference also
builds, then discards, one for every earlier patch.
- Symmetric averaging runs the negated input in the same forward pass as the
original, as further groups, instead of in a second call.
- The advancing refine frame accumulates per-patch Welford states sequentially
rather than by a parallel prefix scan; both are float64.
- A context longer than the trained length is refused rather than truncated.
- Past-only covariates as a separate role, categorical target encoding and the
block-by-block continuation beyond one decoded block are not ported.

────────────────────────────────────────────────────────────────────────────────
Tsfm.Forecasting.Timestral.Weights.5M and .28M — Mistral AI Modified MIT
────────────────────────────────────────────────────────────────────────────────

These packages redistribute, unmodified, the Timestral-5M and Timestral-28M weights
published by Mistral AI. The weights are under Mistral AI's Modified MIT licence,
reproduced verbatim in LICENSE.txt, each package's licence file. It is NOT an
open-source licence: its condition 2 grants no rights to anyone whose company (or
employer) had global consolidated monthly revenue above USD 20 million in the
preceding month. Attribution notice: 2026 - Mistral AI.
Loading
Loading