Time Series Forecasting
MLX
Safetensors
apple-silicon
timestral
time-series
forecasting
quantile-regression
Instructions to use jbilcke/timestral-mlx with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use jbilcke/timestral-mlx with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download jbilcke/timestral-mlx --local-dir timestral-mlx
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Download scripts/validate.py from jbilcke/timestral-mlx: direct link, hf CLI and curl.
- Browser
- Download file 5.08 kB
-
https://huggingface.co/jbilcke/timestral-mlx/resolve/main/scripts/validate.py
- Command line
-
hf download hf://jbilcke/timestral-mlx/scripts/validate.py
-
curl -L -o validate.py https://huggingface.co/jbilcke/timestral-mlx/resolve/main/scripts/validate.py
5.08 kB
| """Compare converted checkpoints against pinned Mistral CPU FP32 inference.""" | |
| import argparse | |
| import gc | |
| import importlib.metadata | |
| import json | |
| import platform | |
| import sys | |
| import subprocess | |
| import time | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(ROOT)) | |
| sys.path.insert(0, str(ROOT / "upstream" / "src")) | |
| import numpy as np | |
| import torch | |
| import mlx.core as mx | |
| from timestral import TimestralForecaster as SourceForecaster | |
| from timestral_mlx import TimestralForecaster | |
| from timestral_mlx.loading import sha256 | |
| VARIANTS = ("5M", "28M", "123M", "209M") | |
| def fixtures(): | |
| rng = np.random.default_rng(20261020) | |
| def signal(b, v, t): | |
| x = (3*np.sin(np.arange(t)*2*np.pi/24) + rng.normal(0,.2,(b,v,t)).cumsum(-1) + 10).astype(np.float32) | |
| return x | |
| x = signal(1, 1, 257) | |
| x[..., 10:16] = np.nan | |
| yield "univariate_missing_96", x, 96, {} | |
| x = signal(2, 2, 129); x[0,0,:40] = np.nan; x[1,1,60:70] = np.nan | |
| yield "multivariate_batched_65", x, 65, {} | |
| x = signal(1, 1, 193) | |
| past,known = signal(1,2,193),signal(1,2,193+97) | |
| past[0,0,20:28] = np.nan | |
| known[:,1] = np.arange(193+97)%2 | |
| known[0,0,210:215] = np.nan | |
| yield "numeric_covariates_97", x, 97, dict(past_covariates=past,known_covariates=known) | |
| codes = (np.arange(193+97)%4 + 2**26).astype(np.float64)[None,None] | |
| codes[...,5:9] = np.nan | |
| yield "categorical_covariates_97", x, 97, dict(categorical_past_covariates=codes[...,:193],categorical_known_covariates=codes) | |
| x = signal(1,1,257) | |
| yield "autoregressive_1057", x, 1057, dict(symmetric_averaging=False) | |
| yield "constant_65", np.full(65, 5, np.float32), 65, {} | |
| yield "all_missing_65", np.full(65, np.nan, np.float32), 65, {} | |
| def validate(size): | |
| print(f"Validating {size} against upstream...", flush=True) | |
| src = SourceForecaster.from_pretrained(ROOT / "models" / "source" / size) | |
| dst = TimestralForecaster.from_pretrained(ROOT / "models" / size) | |
| records=[] | |
| for name,x,h,kw in fixtures(): | |
| start=time.perf_counter(); expected=src.forecast(x,h,**kw).quantiles.numpy(); source_s=time.perf_counter()-start | |
| start=time.perf_counter(); actual=dst.forecast(x,h,**kw).quantiles; mlx_s=time.perf_counter()-start | |
| delta=actual.astype(np.float64)-expected | |
| rms=float(np.sqrt(np.mean(expected.astype(np.float64)**2))) | |
| err=float(np.sqrt(np.mean(delta**2))) | |
| normalized_rmse=err/max(rms,1e-6) | |
| normalized_max=float(np.max(np.abs(delta)))/max(rms,1e-6) | |
| # Fixed, declared acceptance criteria; not adjusted per model. | |
| passed=bool(np.isfinite(actual).all() and (np.diff(actual,axis=-1)>=0).all() | |
| and normalized_rmse <= 1e-4 and normalized_max <= 2e-3) | |
| record=dict(fixture=name,shape=list(actual.shape),source_seconds=source_s,mlx_seconds=mlx_s, | |
| max_absolute_error=float(np.max(np.abs(delta))),rmse=err,reference_rms=rms, | |
| normalized_rmse=normalized_rmse,normalized_max_error=normalized_max, | |
| finite=bool(np.isfinite(actual).all()),monotone=bool((np.diff(actual,axis=-1)>=0).all()),passed=passed) | |
| records.append(record) | |
| print(f" {name}: normalized RMSE={normalized_rmse:.3g}, max error={record['max_absolute_error']:.3g}, pass={passed}",flush=True) | |
| report=dict(variant=size,passed=all(r['passed'] for r in records), | |
| acceptance=dict(normalized_rmse_max=1e-4,normalized_max_error_max=2e-3), | |
| bundle_manifest_sha256=sha256(ROOT/'models'/size/'manifest.json'), | |
| runtime_sha256={p.name:sha256(p) for p in sorted((ROOT/'timestral_mlx').glob('*.py')) if not p.name.startswith('._')}, | |
| environment=dict(python=platform.python_version(),platform=platform.platform(), | |
| torch=torch.__version__,mlx=importlib.metadata.version('mlx'),numpy=np.__version__, | |
| source_device='cpu',mlx_device=str(mx.default_device()),torch_threads=torch.get_num_threads()), | |
| notes='Deterministic synthetic parity fixtures; timings include first-call overhead and are not a controlled benchmark. No GIFT-Eval/TIME/fev-bench evaluation was run.',fixtures=records) | |
| (ROOT/'reports'/f'{size}.json').write_text(json.dumps(report,indent=2)+'\n') | |
| del src,dst; gc.collect(); mx.clear_cache() | |
| if not report['passed']: | |
| raise RuntimeError(f'{size} did not pass numerical validation') | |
| if __name__=='__main__': | |
| p=argparse.ArgumentParser(description=__doc__) | |
| p.add_argument('variants',nargs='*',choices=VARIANTS,default=list(VARIANTS)) | |
| revision=subprocess.check_output(['git','-C',str(ROOT/'upstream'),'rev-parse','HEAD'],text=True).strip() | |
| if revision!='a534bdbd244d64f3d9d743f46c7bf6fc330ed4ff': | |
| raise ValueError('upstream source revision mismatch') | |
| subprocess.run(['git','-C',str(ROOT/'upstream'),'diff','--exit-code','HEAD','--','src'],check=True) | |
| torch.set_num_threads(1) | |
| for size in p.parse_args().variants: | |
| validate(size) | |