Introduction
zsfm-rs is a Rust workspace for running zero-shot forecasting and tabular foundation models locally, without Python. It has two halves — plus a cleanup helper:
- Convert — download a model’s real weights from HuggingFace and turn them into a single self-contained GGUF file.
- Infer — load that GGUF file and run zero-shot inference (a forecast, or a tabular classification/regression) via candle, Hugging Face’s Rust tensor library. No PyTorch, no Python runtime, one native binary.
- Delete — remove a model’s cached
model-f32.gguf+config.json(and optionally an output GGUF) to free disk (zsfm chronos delete).
Everything is exposed through one CLI binary, zsfm, with a subcommand per model:
zsfm chronos convert # download + convert Chronos-2 to GGUF
zsfm chronos infer --gguf ... # run a forecast
zsfm chronos delete # remove the cached files for Chronos-2
What’s included
11 time-series forecasters — point or quantile forecasts from a numeric context window:
Toto-2, Chronos-2, TimesFM 2.5, Sundial, TTM, Lag-Llama, MOMENT, Moirai 1.0, Moirai 2.0, FlowState-R1, TiRex.
5 tabular foundation models — zero-shot classification/regression from a small labeled support set, no fine-tuning:
Mitra, TabDPT, TabICL, TabPFN-3, TabFM.
See Time-series forecasters and Tabular foundation models for the exact request/response JSON for each.
Correctness
Every model in this workspace was verified bit-exact (or within pure F32 rounding, typically ~1e-6 to ~1e-7) against a reference Python implementation running the real downloaded checkpoint, before being considered done. This isn’t a from-scratch reimplementation guessing at architecture — each port was checked tensor-by-tensor against the original. Every model’s page links back to its original HuggingFace weights, the original authors’ source repo, and the paper it came from — this project converts and runs those exact weights, it doesn’t retrain or approximate them.
Where to go next
- New to the project? Start with Installation and Quick start.
- Want the exact JSON shapes for a specific model, or a link to its original repo/paper? Jump straight to Time-series forecasters or Tabular foundation models.
- Three of the 16 models (Moirai, TabPFN-3, TabFM) are non-commercial only — see Licensing before using them beyond research/internal evaluation.
- Looking for the generated Rust API reference (types, function signatures) rather than a usage guide? See the API docs.
Installation
Option 1: cargo install (like cargo install ripgrep)
From crates.io (requires a recent stable Rust toolchain from rustup.rs):
cargo install zsfm --locked
zsfm --help
From git (latest main):
cargo install --git https://github.com/amaye15/zsfm-rs zsfm --locked
# or pin a tag:
cargo install --git https://github.com/amaye15/zsfm-rs --tag v0.2.2 zsfm --locked
From a local checkout:
git clone https://github.com/amaye15/zsfm-rs.git
cargo install --path zsfm-rs/crates/zsfm --locked
# or, build without installing:
cargo build --release -p zsfm --manifest-path zsfm-rs/Cargo.toml
./zsfm-rs/target/release/zsfm --help
--lockeduses theCargo.locktested in CI for reproducible builds. Omit it if you want the latest compatible dependencies.
Option 2: download a release binary
Pre-built binaries are published on the GitHub Releases page for:
- Linux (
x86_64-unknown-linux-gnu) - macOS Apple Silicon (
aarch64-apple-darwin)
macOS Intel (
x86_64-apple-darwin) isn’t currently published — GitHub’s free Intel Mac runner capacity has been cut to the point of being unusable for CI. Usecargo installabove instead; the workspace builds fine on Intel Macs.
Download the tarball for your platform, extract it, and put zsfm on your PATH:
tar xzf zsfm-v0.2.2-aarch64-apple-darwin.tar.gz
sudo mv zsfm-v0.2.2-aarch64-apple-darwin/zsfm /usr/local/bin/
zsfm --help
Option 3: build from source
Requires a recent stable Rust toolchain (rustup.rs).
git clone https://github.com/amaye15/zsfm-rs.git
cd zsfm-rs
cargo build --release -p zsfm
./target/release/zsfm --help
# or via the inner workspace:
cargo build --release -p zsfm --manifest-path zsfm-rs/Cargo.toml
./zsfm-rs/target/release/zsfm --help
On macOS, the release profile links against Apple’s Accelerate framework for faster BLAS operations automatically — no extra setup needed. On Linux/Windows it falls back to candle’s portable default backend.
Verifying it works
zsfm --help # top-level: one subcommand per model
zsfm chronos --help # per-model: convert / infer / upload / inspect-tensors / delete
zsfm chronos delete --help # per-model cache deletion
If both print usage text without errors, you’re ready for the Quick start.
Quick start
This walks through converting a small forecasting model and running one forecast, end to end. It uses Moirai-2.0-R-small since it’s one of the smaller downloads (~45MB converted).
1. Convert
zsfm moirai2 convert
This downloads Salesforce/moirai-2.0-R-small from HuggingFace, converts it to a GGUF file at gguf/moirai2-f32.gguf, and caches a canonical F32 copy under models/Salesforce__moirai-2.0-R-small/model-f32.gguf — see The zsfm CLI for what that cache buys you on future conversions.
2. Build a request
Every time-series model reads a JSON request from stdin with a numeric context array and a horizon (how many steps to forecast). Moirai batches multiple series:
cat > request.json <<'EOF'
{
"context": [[10.0, 10.5, 11.0, 10.8, 11.2, 11.5, 11.3, 11.8, 12.0, 12.2]],
"horizon": 4
}
EOF
3. Infer
zsfm moirai2 infer --gguf gguf/moirai2-f32.gguf < request.json
You’ll get back an OpenAI-compatible forecast object:
{
"id": "forecast-...",
"object": "forecast",
"model": "moirai-2",
"choices": [
{
"index": 0,
"forecast": {
"point": [12.35, 12.51, 12.64, 12.72]
}
}
]
}
Trying a tabular model instead
Tabular models take a small labeled support set plus rows to predict, rather than a time series. Mitra is a good first one to try:
zsfm mitra convert --model autogluon/mitra-classifier --task classification
cat > request.json <<'EOF'
{
"x_support": [[0.1, 1.2], [0.9, -0.3], [-1.1, 0.4], [1.5, 1.1]],
"y_support": [0, 1, 1, 0],
"x_query": [[0.2, 0.9], [-0.8, 0.1]],
"n_classes": 2
}
EOF
zsfm mitra infer --gguf gguf/mitra-classification-f32.gguf < request.json
{
"task": "classification",
"logits": [[...]],
"probabilities": [[0.83, 0.17], [0.21, 0.79]]
}
For every model’s exact request/response shape, scope, and any caveats, see Time-series forecasters and Tabular foundation models.
4. Clean up (optional)
To free disk, remove a model’s cache (and optionally its output GGUF):
zsfm moirai2 delete
zsfm moirai2 delete --output gguf/moirai2-f32.gguf # also remove the converted file
zsfm mitra delete --task classification --output gguf/mitra-classification-f32.gguf
delete mirrors convert’s --model and --model-dir flags, so zsfm <model> delete --help shows the exact cache it will remove. See The zsfm CLI for details.
The zsfm CLI
Every model gets its own subcommand: zsfm <model> <action>. Most models support five actions:
| Action | What it does |
|---|---|
convert | Download the model from HuggingFace and produce a GGUF file |
infer | Load a GGUF file and run zero-shot inference on a JSON request from stdin |
upload | Push the source + GGUF files to a HuggingFace repo you control |
inspect-tensors | Print every tensor name/shape/dtype in a local checkpoint file |
delete | Remove the cached canonical GGUF + config for this model (and optionally an output GGUF) |
Run zsfm <model> --help or zsfm <model> convert --help for the full flag list of any specific model. Every convert command takes --model-dir (default models/, resolved relative to wherever you run zsfm from) for where downloaded weights and the canonical F32 GGUF cache are stored. delete mirrors convert’s --model and --model-dir flags: zsfm <model> delete --help shows the same cache location it will remove, plus an optional --output <path> to also delete a converted GGUF.
Model caching
The first time you convert a model, zsfm does three things:
- Downloads the raw weights from HuggingFace.
- Converts them once to a canonical F32 GGUF, cached at
<model_dir>/<owner>__<repo-name>/model-f32.gguf(defaultmodel_dirismodels/). - Deletes the downloaded raw weight file(s) — they’re fully redundant once the canonical GGUF exists. (
config.json, where a model’sinfercommand reads it separately, is kept — it’s tiny.)
Every later convert for that same model — at any --dtype — recasts straight from that cached F32 GGUF instead of touching the network again:
zsfm chronos convert --dtype f32 # downloads, ~10s
zsfm chronos convert --dtype f16 # recasts from cache, <1s, no network
zsfm chronos convert --dtype q8 # recasts from cache, <1s, no network
To free disk, remove the cache (and optionally an output file):
zsfm chronos delete # removes models/amazon__chronos-2/
zsfm chronos delete --output gguf/chronos-f16.gguf # also removes the converted file
Pass --redownload to force a fresh download anyway (e.g. the upstream repo was updated):
zsfm chronos convert --redownload
This applies to every model except TabICL and TabPFN-3, which don’t have their own convert subcommand — see below.
Output dtype
Most convert commands accept --dtype f32|f16|q8. q8 (Q8_0 block quantization) roughly quarters file size; tensors too small for a 32-element block automatically fall back to F32 for that tensor rather than erroring. BF16 is available through the generic converter (next section) for interop with other GGUF consumers, but not through the per-model --dtype flag — candle’s GGUF reader in this workspace can’t load ggml dtype 30 (BF16) back in, so a BF16-converted file couldn’t be used with this same crate’s own infer.
The generic converter
TabICL and TabPFN-3 ship a raw checkpoint whose tensor names can be used unchanged (no HuggingFace-specific renaming needed), so they’re converted through a separate, model-agnostic command instead of a dedicated subcommand:
zsfm convert --repo jingang/TabICL --file classifier-v2 --format ckpt -o gguf/tabicl-v2-f32.gguf
zsfm convert --repo Prior-Labs/tabpfn_3 --file classifier-v3_default --format ckpt -o gguf/tabpfn-v3-f32.gguf
--file is a case-insensitive substring used to disambiguate when a repo publishes multiple checkpoints of the same format (both of the repos above do — see Tabular foundation models for the exact substrings to use).
This same generic command works for any HuggingFace repo, not just the two above — zsfm convert --repo <owner>/<name> downloads whatever checkpoint format is published (safetensors, PyTorch pickle, ONNX, HDF5/Keras, npz/npy, or an existing GGUF) and converts it with tensor names passed through unchanged. It’s also how you’d re-quantize an existing GGUF file: zsfm convert existing.gguf -o smaller.gguf --dtype q8. It does not get the F32-caching behavior described above, since it’s meant to work with arbitrary repos, not just this workspace’s known model list.
Request/response format
infer always reads a single JSON object from stdin and writes a single JSON object to stdout. The shape depends on whether the model is a time-series forecaster or a tabular model — see the next two chapters for exact examples.
Time-series forecasters
All 11 models here read a numeric context (past values) and a horizon (how many future steps to predict), and write back an OpenAI-compatible forecast object. zsfm <model> infer --help always shows the exact request shape for that model.
Two request shapes are used, depending on the model:
- Univariate:
{"context": [...], "horizon": N}— one flat array of numbers. - Batch (Toto, Moirai, Moirai-2):
{"context": [[...], [...]], "horizon": N}— a list of series, forecast independently. These three also accept a multivariate form ([[[v0_t0, ...], [v1_t0, ...]], ...]) for genuinely multi-channel input.
Response shape is always:
{
"id": "forecast-...",
"object": "forecast",
"model": "<model-name>",
"choices": [
{"index": 0, "forecast": {"point": [...], "quantiles": {...}}}
]
}
Not every model produces quantiles — point-forecast-only models (noted below) only fill in "point".
Each row below is the original model, not a reimplementation with a different architecture — zsfm convert downloads the exact published weights and this workspace’s inference code is verified bit-exact (or numerically equivalent within float tolerance) against the original PyTorch implementation. Links go to the original HuggingFace weights, the original authors’ source repo, and the paper.
| Model | HF weights | Original code | License | Output |
|---|---|---|---|---|
| Toto-2 | Datadog/Toto-2.0-2.5B | DataDog/toto | Apache-2.0 | quantiles |
| Chronos-2 | amazon/chronos-2 | amazon-science/chronos-forecasting | Apache-2.0 | quantiles |
| TimesFM 2.5 | google/timesfm-2.5-200m-pytorch | google-research/timesfm | Apache-2.0 | quantiles |
| Sundial | thuml/sundial-base-128m | thuml/Sundial | Apache-2.0 | point only |
| TTM | ibm-granite/granite-timeseries-ttm-r2 | ibm-granite/granite-tsfm | Apache-2.0 | point only |
| Lag-Llama | time-series-foundation-models/Lag-Llama | time-series-foundation-models/lag-llama | Apache-2.0 | point only |
| MOMENT | AutonLab/MOMENT-1-large | moment-timeseries-foundation-model/moment | MIT | point only |
| Moirai 1.0 | Salesforce/moirai-1.0-R-large | SalesforceAIResearch/uni2ts | CC-BY-NC-4.0 | point only |
| Moirai 2.0 | Salesforce/moirai-2.0-R-small | SalesforceAIResearch/uni2ts | CC-BY-NC-4.0 | point only |
| FlowState-R1 | ibm-granite/granite-timeseries-flowstate-r1 | ibm-granite/granite-tsfm | Apache-2.0 | quantiles |
| TiRex | NX-AI/TiRex | NX-AI/tirex | NXAI Community | quantiles |
Bold licenses have real usage restrictions beyond plain permissive — see Licensing before relying on Moirai or TiRex weights for anything beyond research/internal use.
Toto-2
Paper: Toto 2.0: Time Series Forecasting Enters the Scaling Era.
zsfm toto convert
echo '{"context": [[1,2,3,4,5,6,7,8]], "horizon": 4}' | zsfm toto infer --gguf gguf/toto-2.5b-f16.gguf
Batch and multivariate input supported (see table above). --context-length on infer overrides how much of the context window is fed to the model (default: last 4096 steps, must be divisible by the patch size, 32). --f64 runs the forward pass in double precision to match PyTorch’s numerical accuracy more closely, at ~2x memory.
Chronos-2
Paper: Chronos-2: From Univariate to Universal Forecasting.
zsfm chronos convert
echo '{"context": [1,2,3,4,5,6,7,8], "horizon": 4}' | zsfm chronos infer --gguf gguf/chronos-f16.gguf
Univariate only. Full quantile levels in the response; "point" is the median (q0.5).
TimesFM 2.5
Paper: A decoder-only foundation model for time-series forecasting (ICML 2024).
zsfm timesfm convert
echo '{"context": [1,2,3,4,5,6,7,8], "horizon": 4}' | zsfm timesfm infer --gguf gguf/timesfm.gguf
Univariate only. Fixed architecture — no --config flag needed at inference time (everything’s embedded in the GGUF).
Sundial
Paper: Sundial: A Family of Highly Capable Time Series Foundation Models (ICML 2025 Oral).
zsfm sundial convert
echo '{"context": [1,2,3,4,5,6,7,8], "horizon": 4}' | zsfm sundial infer --gguf gguf/sundial-f16.gguf
Flow-matching model, point-forecast only. --steps on infer overrides the ODE solver’s step count (default: from GGUF metadata, typically 50); 10-20 is usually enough and latency scales linearly with this value.
TTM
Paper: TinyTimeMixers (NeurIPS 2024).
zsfm ttm convert
echo '{"context": [1,2,3,4,5,6,7,8], "horizon": 4}' | zsfm ttm infer --gguf gguf/ttm-f32.gguf
Univariate, point-forecast only. Small and fast (~3MB as F32).
Lag-Llama
Paper: Lag-Llama: Towards Foundation Models for Probabilistic Time Series Forecasting.
zsfm lag-llama convert
echo '{"context": [1,2,3,4,5,6,7,8], "horizon": 4}' | zsfm lag-llama infer --gguf gguf/lag_llama-f32.gguf
Univariate, point-forecast only. Downloads a raw PyTorch Lightning .ckpt and reads it directly — no Python needed for conversion.
MOMENT
Paper: MOMENT: A Family of Open Time-series Foundation Models (ICML 2024).
zsfm moment convert
echo '{"context": [1,2,3,4,5,6,7,8], "horizon": 4}' | zsfm moment infer --gguf gguf/moment-f32.gguf
Univariate, point-forecast only.
Moirai 1.0 / Moirai 2.0
Papers: Unified Training of Universal Time Series Forecasting Transformers (Moirai 1.0), Moirai 2.0: When Less Is More for Time Series Forecasting (Moirai 2.0).
zsfm moirai convert # or: zsfm moirai2 convert
echo '{"context": [[1,2,3,4,5,6,7,8]], "horizon": 4}' | zsfm moirai infer --gguf gguf/moirai-f32.gguf
Point-forecast only, channel-independent across variates (each variate forecast independently, computed in parallel via rayon). Batch and multivariate input supported. Moirai-2 is the newer, smaller (R-small) checkpoint.
Both checkpoints are CC-BY-NC-4.0 — non-commercial use only. See Licensing.
FlowState-R1
Paper: FlowState: Sampling Rate Invariant Time Series Forecasting.
zsfm flowstate convert
echo '{"context": [1,2,3,4,5,6,7,8], "horizon": 4}' | zsfm flowstate infer --gguf gguf/flowstate-r1-f16.gguf
Univariate. Full quantile levels; "point" is the median.
TiRex
Paper: TiRex: Zero-Shot Forecasting Across Long and Short Horizons with Enhanced In-Context Learning. Built on xLSTM.
zsfm tirex convert
echo '{"context": [1,2,3,4,5,6,7,8], "horizon": 4}' | zsfm tirex infer --gguf gguf/tirex-f32.gguf
Univariate. Full quantile levels; "point" is the median. Downloads a raw .ckpt directly, like Lag-Llama.
Licensed under the NXAI Community License (modeled on Meta’s Llama community license): free to use and redistribute, including commercially, unless your organization’s consolidated annual revenue exceeds €100M and you’re incorporating TiRex into a commercial product or service — in which case NXAI requires a separate commercial license. See Licensing.
Tabular foundation models
These models do in-context classification or regression: you give them a small labeled support set (training rows) and a set of query rows to predict, and they run a single zero-shot forward pass — no fine-tuning, no training loop.
Four of the five (Mitra, TabDPT, TabICL, TabPFN-3) share one request format:
{
"x_support": [[0.1, 1.2], [0.9, -0.3], [-1.1, 0.4]],
"y_support": [0, 1, 1],
"x_query": [[0.2, 0.9], [-0.8, 0.1]],
"n_classes": 2
}
n_classes is only used for classification (ignored — may be omitted — for regression). Classification responses look like:
{"task": "classification", "probabilities": [[0.83, 0.17], [0.21, 0.79]]}
Regression responses:
{"task": "regression", "predictions": [1.53, 0.22]}
TabFM uses a different shape — see its own section below.
Each row below is the original model, not a reimplementation — zsfm convert downloads the exact published weights and this workspace’s inference code is verified bit-exact against the original PyTorch implementation. Links go to the original HuggingFace weights, the original authors’ source repo, and the paper.
| Model | HF weights | Original code | License | Class. | Regr. |
|---|---|---|---|---|---|
| Mitra | autogluon/mitra-classifier / -regressor | autogluon/autogluon | Apache-2.0 | ✅ | ✅ |
| TabDPT | Layer6/TabDPT | layer6ai-labs/TabDPT-inference | Apache-2.0 | ✅ | ✅ |
| TabICL | jingang/TabICL | soda-inria/tabicl | BSD-3-Clause | ✅ | ❌ |
| TabPFN-3 | Prior-Labs/tabpfn_3 | PriorLabs/TabPFN | Non-commercial | ✅ | ❌ |
| TabFM | google/tabfm-1.0.0-pytorch | google-research/tabfm | Non-commercial | ✅ | ✅ |
Bold licenses restrict use to research/internal evaluation — see Licensing before relying on TabPFN-3 or TabFM for anything else.
Mitra
Paper: Mitra: Mixed Synthetic Priors for Enhancing Tabular Foundation Models.
zsfm mitra convert --model autogluon/mitra-classifier --task classification
zsfm mitra convert --model autogluon/mitra-regressor --task regression
zsfm mitra infer --gguf gguf/mitra-classification-f32.gguf --task classification < request.json
zsfm mitra infer --gguf gguf/mitra-regression-f32.gguf --task regression < request.json
--task on infer must match which checkpoint you loaded — the GGUF doesn’t self-describe which head it has. Zero-shot only (no fine-tuning path), no random-mirror augmentations (both scope decisions made deliberately to keep the port a single deterministic forward pass).
TabDPT
Paper: TabDPT: Scaling Tabular Foundation Models on Real Data.
zsfm tabdpt convert
zsfm tabdpt infer --gguf gguf/tabdpt-f32.gguf --task classification < request.json
zsfm tabdpt infer --gguf gguf/tabdpt-f32.gguf --task regression < request.json
One checkpoint serves both tasks — --task on infer just picks which output head to read. Single forward pass, no class-permutation ensembling.
TabICL
zsfm convert --repo jingang/TabICL --file classifier-v2 --format ckpt -o gguf/tabicl-v2-f32.gguf
zsfm tabicl infer --gguf gguf/tabicl-v2-f32.gguf < request.json
Classification only, n_classes must be ≤ 10 (the >10-class mixed-radix/hierarchical path from the original model isn’t implemented). No dedicated convert subcommand — see the generic converter. The jingang/TabICL repo publishes 4 classifier checkpoints; classifier-v2 picks the one this port was verified against.
TabPFN-3
Paper: TabPFN-3: Technical Report.
zsfm convert --repo Prior-Labs/tabpfn_3 --file classifier-v3_default --format ckpt -o gguf/tabpfn-v3-f32.gguf
zsfm tabpfn infer --gguf gguf/tabpfn-v3-f32.gguf < request.json
Classification only (the regression bar-distribution head isn’t implemented). No dedicated convert subcommand. The repo publishes several checkpoint variants; classifier-v3_default is the one this port was verified against.
Licensed under
tabpfn-3-license-v1.0— non-commercial: research, testing, and internal benchmarking are explicitly fine, but the model, its derivatives, and its outputs can’t be used for any commercial or production purpose. See Licensing.
TabFM
Source: Google Research blog post.
TabFM’s request shape is a single combined table rather than separate support/query arrays:
zsfm tabfm convert # classification by default; --task regression for the other variant
cat > request.json <<'EOF'
{
"x": [[0.1, 1.2], [0.9, -0.3], [-1.1, 0.4], [0.2, 0.9]],
"y": [0, 1, 1, 0],
"train_size": 3
}
EOF
zsfm tabfm infer --gguf gguf/tabfm-classification-f16.gguf < request.json
x is [rows][columns], y is one label per row (any finite placeholder value at query-row positions is fine — it’s ignored), train_size is how many leading rows are the training set. Optional fields: cat_mask (marks categorical columns, default all-false) and d (actual unpadded feature count, default = number of columns).
TabFM also has a second, heavier command, ensemble-predict, that reproduces the full sklearn-wrapper pipeline (feature scaling, categorical encoding, n_estimators-member ensembling, calibration) bit-compatible with the original TabFMClassifier/TabFMRegressor’s default RNG — see zsfm tabfm ensemble-predict --help for its (considerably larger) request shape.
Licensed under the TabFM Non-Commercial License v1.0 — testing, evaluation, and internal benchmarking are fine; any commercial or production use (including client deliverables or revenue-generating decisions) requires a separate license from Google. See Licensing.
Python bindings (uv + pyo3 + maturin)
The same 16 models are available from Python via zsfm — installed with uv and built with maturin + pyo3. The API mirrors the CLI (convert / infer / delete) but as Python classes/functions, with numpy arrays for inputs/outputs where natural.
Installation
Requires Python ≥3.8 and a recent Rust toolchain (rustup.rs).
# from crates.io (once published)
uv pip install zsfm
# or: pip install zsfm
# from git (latest main)
uv pip install "zsfm @ git+https://github.com/amaye15/zsfm-rs"
# from a local checkout (editable, fastest for development)
git clone https://github.com/amaye15/zsfm-rs.git
cd zsfm-rs # repo root is zero-shot-forecasters-gguf
uv sync # creates .venv, installs deps + zsfm as editable
uv run maturin develop # rebuild after Rust changes (or: maturin develop)
# alternative without uv: pip install -e .
The Rust code lives under
zsfm-rs/but the Python project root is the repo root (wherepyproject.tomllives).uv sync+uv run maturin developis theuvanalogue ofcargo install zsfmfor Python.
Verify:
uv run python -c "import zsfm; print(zsfm.__version__); print(zsfm.list_models())"
# ['toto', 'chronos', 'timesfm', 'sundial', 'ttm', 'lag_llama', 'moment', 'moirai', 'moirai2', 'flowstate', 'tirex', 'mitra', 'tabdpt', 'tabicl', 'tabpfn', 'tabfm']
Quick start
Forecasting (time series)
import zsfm
# list all models
print(zsfm.list_forecasters())
# ['toto', 'chronos', 'timesfm', 'sundial', 'ttm', 'lag_llama', 'moment', 'moirai', 'moirai2', 'flowstate', 'tirex']
# download + convert (like `zsfm ttm convert`)
zsfm.convert("ttm", output="gguf/ttm-f32.gguf", dtype="f32") # also: model_dir, token, redownload, task/filename
# or per-model: already handled by the generic `convert` dispatcher
# load and forecast (like `zsfm ttm infer --gguf gguf/ttm-f32.gguf`)
model = zsfm.TtmModel("gguf/ttm-f32.gguf", config="models/ibm-granite__granite-timeseries-ttm-r2/config.json")
context = [10.0, 10.5, 11.0, 10.8, 11.2, 11.5, 11.3, 11.8, 12.0, 12.2]
point = model.forecast(context, horizon=4)
print(point) # [12.3, 12.5, 12.6, 12.7]
# Chronos-2 exposes quantiles as well
chronos = zsfm.ChronosModel("gguf/chronos-f16.gguf")
qmat = chronos.forecast_quantiles([1,2,3,4,5,6,7,8], horizon=4) # [n_quantiles][horizon]
print(chronos.quantiles()) # [0.1, 0.2, ..., 0.9]
# Toto / Moirai support batch + multivariate (mirrors CLI JSON shapes)
# For now, Python's TotoModel exposes `forecast` (single series) and `forecast_batch`
# (List[List[float]] -> List[List[float]]).
# delete cache (like `zsfm ttm delete`)
zsfm.delete("ttm") # removes models/ibm-granite__granite-timeseries-ttm-r2/
zsfm.delete("ttm", output="gguf/ttm-f32.gguf") # also remove the converted file
Tabular (zero-shot classification / regression)
import zsfm
# Mitra (needs task)
mitra_clf = zsfm.MitraModel("gguf/mitra-classification-f32.gguf", task="classification")
logits = mitra_clf.predict_classification(
x_support=[[0.1, 1.2], [0.9, -0.3]],
y_support=[0, 1],
x_query=[[0.2, 0.9]],
n_classes=2,
)
print(logits) # [[...], [...]]
mitra_reg = zsfm.MitraModel("gguf/mitra-regression-f32.gguf", task="regression")
preds = mitra_reg.predict_regression(
x_support=[[0.1], [0.9]],
y_support=[0.5, 1.5],
x_query=[[0.2]],
)
print(preds)
# TabDPT (single checkpoint, task at predict time)
tabdpt = zsfm.TabDptModel("gguf/tabdpt-f32.gguf")
probs = tabdpt.predict_classification(x_support, y_support, x_query, n_classes=2)
# TabICL / TabPFN-3 (classification only)
tabicl = zsfm.TabIclModel("gguf/tabicl-v2-f32.gguf")
tabpfn = zsfm.TabPfnModel("gguf/tabpfn-v3-f32.gguf")
# TabFM (needs config, like the CLI)
tabfm = zsfm.TabFmModel("gguf/tabfm-classification-f16.gguf")
# single predict (like `zsfm tabfm infer`)
out = tabfm.predict(x=[[0.1, 1.2], [0.9, -0.3]], y=[0, 1], train_size=1)
# ensemble-predict is not yet exposed in Python; use the CLI for now:
# zsfm tabfm ensemble-predict --gguf ... < request.json
For exact per-model convert defaults and GGUF paths, see zsfm <model> convert --help — the Python convert(model, ...) dispatcher accepts the same model, model_dir, output, dtype, token, redownload, task, filename kwargs.
API reference
Top-level:
| Symbol | Kind | Description |
|---|---|---|
zsfm.__version__ | str | Crate version (matches Cargo.toml workspace version) |
zsfm.list_models() | fn -> List[str] | All 16 model ids |
zsfm.list_forecasters() | fn -> List[str] | 11 forecasters |
zsfm.list_tabular() | fn -> List[str] | 5 tabular |
zsfm.convert(model, output?, dtype?, model_dir?, token?, redownload?, task?, filename?) | fn | Download + convert (dispatches by model id, like zsfm <model> convert) |
zsfm.delete(model, model_dir?, output?) | fn | Remove cache (like zsfm <model> delete) |
Forecasters (each Model(gguf, config?) with forecast(context, horizon)):
TotoModel, ChronosModel, TimesFmModel, SundialModel, TtmModel, LagLlamaModel, MomentModel, MoiraiModel, Moirai2Model, FlowStateModel, TirexModel
TotoModel(gguf, config?, context_length?, use_f64?)— alsoforecast_batchChronosModel(gguf, config?)— alsoforecast_quantiles,quantiles()FlowStateModel(gguf, config?)/TtmModel(gguf, config?)/ChronosModel—configismodels/<owner>__<name>/config.jsonfromconvert- Others with fixed configs:
TimesFmModel(gguf),SundialModel(gguf),LagLlamaModel(gguf),MomentModel(gguf),MoiraiModel(gguf),Moirai2Model(gguf),TirexModel(gguf)
Tabular:
MitraModel(gguf, task?) — predict_classification / predict_regressionTabDptModel(gguf) — predict_classification / predict_regressionTabIclModel(gguf) — predict_classificationTabPfnModel(gguf) — predict_classificationTabFmModel(gguf, config?) — predict(x, y, train_size) (single forward pass)
All forecast/predict methods accept Python list or numpy.ndarray and return list (convert to numpy via np.array(...) if you prefer).
Development
# Rust + Python together
uv sync # (re)create .venv with deps
cargo build --release -p zsfm-python # check Rust alone
uv run maturin develop # build + install as editable (fastest)
# or: maturin develop --manifest-path zsfm-rs/crates/zsfm-python/Cargo.toml
# run Python tests
uv run pytest tests/python -v
# or: .venv/bin/python -m pytest
# from crates.io (once published)
cargo publish -p zsfm-python # last, after the other 22 crates
pyproject.toml at the repo root is the uv/maturin project ( tool.maturin.manifest-path = "zsfm-rs/crates/zsfm-python/Cargo.toml", module-name = "zsfm" ). The Rust workspace at zsfm-rs/Cargo.toml and the root Cargo.toml both include zsfm-python as a member so cargo install --git and cargo build --workspace keep working.
See also the zsfm API docs at https://amaye15.github.io/zsfm-rs/api/zsfm_python/ (once cargo doc includes the pyo3 crate).
Licensing
This project
The code in this repository (converters, GGUF writer/reader, CLI, inference kernels) is MIT licensed — see LICENSE in the repo root.
Model weights
Converting a model with zsfm <model> convert downloads the original weights from HuggingFace and re-encodes them as GGUF — it does not change who owns them or what license applies. Each model keeps the license its original authors published it under, and licenses vary quite a bit across this workspace’s 16 models. Check the table below before using a GGUF for anything beyond local experimentation.
| Model | License | Commercial use |
|---|---|---|
| Toto-2, Chronos-2, TimesFM 2.5, Sundial, TTM, Lag-Llama, Mitra, TabDPT | Apache-2.0 | ✅ unrestricted |
| MOMENT | MIT | ✅ unrestricted |
| TabICL | BSD-3-Clause | ✅ unrestricted |
| FlowState-R1 | Apache-2.0 | ✅ unrestricted |
| TiRex | NXAI Community License | ✅ unless your org’s annual revenue exceeds €100M and you ship TiRex in a commercial product/service (then a separate commercial license from NXAI is required) |
| Moirai 1.0, Moirai 2.0 | CC-BY-NC-4.0 | ❌ non-commercial only |
| TabPFN-3 | tabpfn-3-license-v1.0 | ❌ non-commercial only |
| TabFM | TabFM Non-Commercial License v1.0 | ❌ non-commercial only |
The non-commercial models
Three models — Moirai (both versions), TabPFN-3, and TabFM — are licensed for research, testing, and internal evaluation only. None of them permit production deployment, revenue-generating use, or offering the model (or its outputs) as part of a paid product or service. If you need any of those for a commercial use case, the license text for each (linked above) explains how to request a commercial license from the original publisher — zsfm has no involvement in that process.
TiRex’s revenue threshold
TiRex’s NXAI Community License is modeled on Meta’s Llama community license: free to use, modify, and redistribute — including commercially — for everyone except organizations whose consolidated annual revenue exceeds €100M and who are incorporating TiRex into a commercial product or service, who need to request a license from NXAI directly.
Everything else
The remaining 11 models ship under standard permissive open-source licenses (Apache-2.0, MIT, or BSD-3-Clause) with no commercial-use restriction.
If you’re unsure whether your use case qualifies under any of these, read the actual license file in the model’s HuggingFace repo (linked from each model’s page in Time-series forecasters / Tabular foundation models) — zsfm doesn’t attempt to interpret or enforce license terms for you.
Development
Workspace layout
.
├── Cargo.toml # root workspace (so `cargo install --git https://github.com/amaye15/zsfm-rs zsfm` works)
├── pyproject.toml # Python project (uv + maturin + pyo3, module `zsfm`)
└── zsfm-rs/
crates/
zsfm/ # the `zsfm` binary — one subcommand module per model (`cargo install zsfm`)
zsfm-python/ # `zsfm` Python extension (pyo3, `import zsfm`; see ./python.md)
zsfm-gguf/ # GGUF reader/writer
zsfm-checkpoint/ # loads safetensors/pickle/onnx/hdf5/npz/ckpt/gguf, dtype casting, recast()
zsfm-hub/ # HuggingFace download + the canonical-F32-cache helpers
zsfm-nn/ # shared candle tensor primitives
zsfm-bench/ # in-process rolling-window accuracy/latency benchmark + ensembling
models/
chronos/ flowstate/ moirai/ moirai2/ moment/ sundial/ timesfm/ toto/ ttm/
lag_llama/ tirex/ # time-series forecasters
mitra/ tabdpt/ tabicl/ tabpfn/ tabfm/ # tabular foundation models
Each model crate under models/ owns its architecture, weight-name mapping, and inference kernel; zsfm (crates/zsfm/) just wires them up to convert/infer/upload/inspect-tensors/delete subcommands.
Building and testing
# Rust — from the repo root (uses the root workspace, which re-exports zsfm-rs):
cargo build --release --workspace
cargo test --release --workspace
# or, from inside zsfm-rs (same result, uses zsfm-rs/Cargo.toml):
cd zsfm-rs
cargo build --release --workspace
cargo test --release --workspace
# also: cargo install from crates.io or git, like ripgrep:
cargo install zsfm --locked
cargo install --git https://github.com/amaye15/zsfm-rs zsfm --locked
# Python — uv + pyo3 + maturin (see ./python.md)
uv sync
cargo build --release -p zsfm-python # check Rust alone
uv run maturin develop # build + install as editable (fastest)
uv run pytest tests/python -v # or: .venv/bin/python -m pytest
uv run python -c "import zsfm; print(zsfm.list_models())"
The baseline is 130 passing tests across the workspace (unit tests for tensor casting, GGUF round-tripping, per-model architecture/shape checks, and zsfm-bench’s window-generation/metrics/ensembling logic) plus 6 Python tests (tests/python/test_zsfm.py). CI (.github/workflows/ci.yml) runs both on every push to main and every PR, on Linux and macOS — Rust (cargo build/cargo test) and Python (uv run maturin develop + pytest).
Verifying a model port is correct
Every model in this workspace was verified bit-exact (or numerically equivalent within float tolerance) against its original PyTorch implementation before being considered done — same input, same output, checkpoint tensor-for-tensor. If you’re modifying a model crate, re-run that model’s convert + infer against a known input/output pair before assuming a change is safe; there isn’t a single workspace-wide golden-output test harness, so this is a manual step per model.
Benchmarking
zsfm-bench (crates/zsfm-bench) runs the rolling-window accuracy/latency benchmark across the 11 time-series forecasters and writes benchmark.md. It links the model crates in-process through the shared zsfm_core::Forecaster interface — each model’s GGUF is loaded exactly once and reused across every dataset/window/context/horizon combination, and independent models run concurrently — rather than the old Python driver’s one-subprocess-and-reload-the-model-every-time approach.
# convert whichever models you want to benchmark first, e.g.:
zsfm ttm convert && zsfm moirai2 convert
# one dataset, quick look:
zsfm-bench run --dataset ETTh1 --models ttm,moirai2 --windows 30
# full 21-dataset × horizon/context sweep, writes ../benchmark.md:
zsfm-bench report
report caches each (dataset, context, horizon, windows) config’s results in benchmark/bench_cache.json (gitignored) — re-running after an interruption, or after adding a model, only computes what’s missing.
Adding a new model
Roughly the shape to follow, based on the existing crates under models/:
- New crate under
crates/models/<name>/with a weight-name mapping from the original checkpoint to whatever internal names you want, an inference module built on candle, and a request/response type. - A
zsfm/src/<name>.rs(zsfm-rs/crates/zsfm/src/<name>.rs) withconvert/infer/deletesubcommands, following the caching pattern described in ThezsfmCLI — computezsfm_hub::canonical_gguf_path, check it before downloading, callzsfm_checkpoint::recastfor cache hits and for the final requested dtype after a fresh download, and implementdeleteviacommon::delete_cached_model. - Wire the subcommand into
zsfm’s top-levelCommandsenum (zsfm-rs/crates/zsfm/src/main.rs). - A page in this guide (
docs-guide/src/models/) and a row in the relevant summary table.
Releasing
Pushing a v* tag triggers .github/workflows/release.yml: cross-platform binary builds (cargo build --release -p zsfm), a GitHub Release with --generate-notes, and (gated behind the PUBLISH_CRATES_IO repository variable) publishing all 22 crates to crates.io in dependency order via zsfm-rs/scripts/publish-crates.sh (cargo publish -p zsfm is last).