Timestral-209M — LiteRT

Forecast one time series on a phone with Timestral-209M: 21 quantiles for each of up to 1,024 future steps, read from up to 8,192 past steps, on the CPU, the GPU or a Qualcomm NPU through the LiteRT CompiledModel API. The graphs were converted with litert-torch; on a Galaxy S26 (LiteRT 2.2.0, measured 2026-10-22), one forecast's graph call takes 15.6 ms on the NPU and 27.0 ms on the GPU with timestral-209m-c2048-h256.tflite (medians; conditions under Speed on phones).

A 72-hour forecast of a synthetic hourly temperature series: the last week of history, the median forecast and its 10–90 % and 1–99 % bands

The series is the last 2,048 hours of a synthetic hourly temperature series (made with NumPy, no outside data). The forecast is the Kotlin code below with timestral-209m-c2048-h1024.tflite on the GPU of a Galaxy S26 (FP16, LiteRT 2.2.0).

Timestral is Mistral AI's family of time-series foundation models (5M to 209M parameters). This repository holds the 209M checkpoint converted to LiteRT: graphs that run the patch embedding, the 24 transformer layers and the quantile head in one call, and host code in Python and Kotlin that runs the official forecaster's steps around them; they are the files the Android sample timestral in the LiteRT-Models zoo loads. The forecasts follow the official TimestralForecaster.forecast() defaults for a single series without covariates: the series and its negation are forecast together and averaged (symmetric averaging), the quantiles are capped at the context's range widened by 10⁴ times the scale of its last patch, and a series with no negative values gets no negative quantiles.

Files

File Context Horizon per call Weights Accelerators Bytes SHA-256
timestral-209m-c2048-h256.tflite up to 2,048 steps up to 256 steps float16 GPU, NPU 405,041,792 c016500177c84c6f03cf746d59d690baf35c9fafb9a4c720cba99e669fb66fca
timestral-209m-c2048-h1024.tflite up to 2,048 steps up to 1,024 steps float16 GPU, NPU 405,041,792 5e825e884bf96f6acb5ee40553cbac453d85cfb53f9eae9a0630c31b6e0c1ae5
timestral-209m-c8192-h256.tflite up to 8,192 steps up to 256 steps float16 GPU 405,041,792 62a92115fb81e16f7b235d402c74df6175c568540b6c0ac14f354d92567763d7
timestral-209m-c8192-h1024.tflite up to 8,192 steps up to 1,024 steps float16 GPU 405,041,792 95f327839f76cf78ded74e022a5a598b017dee83ceac50719e3d24cb0bdc522d
timestral-209m-c2048-h256-int8.tflite up to 2,048 steps up to 256 steps int8 in the fully connected layers of the first 23 of the 24 blocks, float16 elsewhere CPU 222,336,912 c32773bc277b6dafb4e34561b6f900fa8a6c63ec4dc4ed603d4d563d1f2f5fc5
timestral-209m-c2048-h1024-int8.tflite up to 2,048 steps up to 1,024 steps int8 in the fully connected layers of the first 23 of the 24 blocks, float16 elsewhere CPU 222,336,912 790935a69c9f1a4cd41f5678d59cda0e2dfde73ef139f0e76d2d2d3cba637264
timestral-209m-c8192-h256-int8.tflite up to 8,192 steps up to 256 steps int8 in the fully connected layers of the first 23 of the 24 blocks, float16 elsewhere CPU 222,336,912 cd4ef83f6104cb381c5dad741063b131da30b8fa10bbf39cdc60358ee8e0b1e6
timestral-209m-c8192-h1024-int8.tflite up to 8,192 steps up to 1,024 steps int8 in the fully connected layers of the first 23 of the 24 blocks, float16 elsewhere CPU 222,336,912 97bd95c154ef25671dd3dc9732b217690f3413b6ee72dc9503dc4c873523a03a
forecast_query.f32 the decoder token (768 float32) 3,072 fb8bf83c6788fac02c8bac975235921135b234d96e3c948da4d654b589b167bd
rope_cos.f32 RoPE cos rows of positions −1,023 to 1,055 (2,079 × 64 float32) 532,224 b15457cc67269e6c65fdfc2a77e96af24cbd4b9c03f45e0e379f5c8f0afb6abc
rope_sin.f32 RoPE sin rows of positions −1,023 to 1,055 (2,079 × 64 float32) 532,224 47f2ab89a9d6fb487ca22d6e5483f358321808051787707132d72ae916044247

The graphs come in four sizes, named in the files: a context of up to 2,048 or 8,192 steps (c2048, c8192) and a horizon of up to 256 or 1,024 steps per call (h256, h1024). The host pads a shorter series in front with missing steps, which the attention mask leaves out, and keeps only the requested steps of the graph's horizon; the smallest graph that holds the series and the horizon takes the least time, and the Python class and GraphSpec.choose in android/GraphSpec.kt take it. Each size has a float16 file for the GPU (the c2048 ones also for the NPU) and an -int8 file for the CPU; the CPU runs the float16 files too, more slowly:

  • On the Galaxy S26 CPU (4 threads), timestral-209m-c2048-h1024-int8.tflite takes 39.3 ms per forecast and timestral-209m-c2048-h1024.tflite 93.8 ms (Speed on phones).
Other files What they are
forms.json the graphs the Python class and GraphSpec.choose pick from (context and decoder patches, rows per call, the attention mask value, accelerators) and each accelerator's settings
host_forecast.py the host steps in Python (NumPy): input preparation and the forecast's finish around a graph call
timestral_litert.py a Python class that picks the graph, runs it with CompiledModel and calls host_forecast.py
android/TimestralHost.kt, android/GraphSpec.kt the same host steps and graph choice in Kotlin, with no Android dependency (package com.timestral)
LICENSE the Modified MIT License of the weights (Mistral AI); it covers the graph files and host tables
LICENSE-APACHE-2.0, NOTICE the licence of the code files, which port the official inference code (Apache-2.0)

Inputs and output

Each graph has one signature, serving_default, with five float32 inputs and one float32 output. A patch is 32 steps; L is the graph's number of context patches and H its number of decoder patches (the forecast's 32-step blocks), S = L + H, and R = 2 rows (the series and its negation).

Name Shape Content
patches [R, L, 64] per patch, 32 normalized values and then 32 missing flags (1 = missing); patches in front of the series are padding (values 0, all missing)
dec [R, H, 768] the checkpoint's forecast_query vector (forecast_query.f32) for every decoder token
bias [1, 1, S, S] additive attention mask: 0 where a token may attend, −3·10⁴ where it may not
cos, sin [1, 1, S, 64] RoPE rows for the positions counted from the first observed patch, cut from the two RoPE tables
output_0 [R, H, 32, 21] the normalized quantiles of every decoder step, in order of level

The host builds the inputs and finishes the forecast the way the official forecaster does:

  1. Missing steps (NaN or infinite) are marked; at most the last 8,192 steps are kept (2,048 when only the c2048 graphs are used) and the front is padded to whole patches of 32 steps.
  2. Causal scaling per row: a float64 running mean and Bessel standard deviation over the observed steps (floored at 10⁻⁶); each patch is scaled with the statistics at its last step; observed steps become asinh((x − mean) / scale) in float32.
  3. The mask lets each context patch attend to itself and to the valid patches before it, and each decoder token to every valid context patch, to the decoder tokens before it and to itself (a patch is valid from the first patch with an observed step on).
  4. After the call: the first ⌈horizon / 32⌉ decoder patches are mapped back (sinh, times the scale, plus the mean of the last context patch), moved to the official advancing refine frame when there is more than one (in float64, each decoder patch after the first is mapped back with the mean and scale of the context extended by the medians of the forecast's earlier patches), capped at the context's minimum − 10⁴ × scale and maximum + 10⁴ × scale and checked for NaN.
  5. The negation's quantiles are reversed, negated and averaged with the series' quantiles; when every observed value is ≥ 0, the quantiles are clipped at 0.

Minimal usage

Python (desktop, LiteRT CompiledModel)

Tested with Python 3.12 on macOS (Apple M4 Max) after this install line:

pip install ai-edge-litert==2.2.0 numpy==2.5.4 huggingface_hub==2.2.0
import sys
import numpy as np
from huggingface_hub import snapshot_download
from ai_edge_litert.compiled_model import CompiledModel, CpuOptions, HardwareAccelerator, Options

GRAPH = "timestral-209m-c2048-h1024.tflite"  # contexts up to 2,048 steps, horizons up to 1,024
d = snapshot_download("mlboydaisuke/Timestral-209M-LiteRT",
                      allow_patterns=["*.py", "*.json", "*.f32", GRAPH])
sys.path.insert(0, d)
import host_forecast as hf  # the official forecaster's host steps, in NumPy

f32 = lambda name: np.fromfile(f"{d}/{name}", "<f4")
inputs = hf.GraphInputs(f32("forecast_query.f32"), f32("rope_cos.f32").reshape(-1, 64),
                        f32("rope_sin.f32").reshape(-1, 64), rope_p_min=-1023)
cpu = Options(hardware_accelerators=HardwareAccelerator.CPU, cpu_options=CpuOptions(num_threads=4))
model = CompiledModel.from_file(f"{d}/{GRAPH}", options=cpu)
ins = {n: model.create_input_buffer_by_name("serving_default", n) for n in ("patches", "dec", "bias", "cos", "sin")}
out = model.create_output_buffer_by_name("serving_default", "output_0")

def run_graph(scaled, observed, h):  # normalized context [2, T] -> normalized quantiles [2, h * 32, 21]
    for n, x in zip(ins, inputs.build(scaled, observed, 64, 32, neg=-3e4)):
        ins[n].write(x)
    model.run_by_name("serving_default", ins, {"output_0": out})
    return out.read(2 * 32 * 32 * 21, np.float32).reshape(2, 1024, 21)[:, : h * 32]

t = np.arange(1500)  # an hourly series; NaN marks a missing value
series = 14 + 6 * np.sin(2 * np.pi * (t - 9) / 24) + np.random.default_rng(0).normal(0, 0.7, t.size)
q = hf.forecast(series[-2048:], 48, run_graph)  # [48, 21] quantiles of the next 48 steps
print(q[:3, [2, 10, 18]])  # the 0.1, 0.5 and 0.9 quantiles of the first three steps

timestral_litert.py wraps the same steps and picks the graph as GraphSpec.choose does: from forms.json, the graph with the fewest patches that holds the series and the horizon among the files present, the -int8 graphs on the CPU and the float16 graphs on the desktop GPU (accelerator="gpu"; Metal on macOS). It refuses a series shorter than 16 values:

from timestral_litert import TimestralLiteRT
d = snapshot_download("mlboydaisuke/Timestral-209M-LiteRT", allow_patterns=["*.py", "*.json", "*.f32", "*-int8.tflite"])
q = TimestralLiteRT(d).forecast(series, horizon=48)  # [48, 21], on the CPU with an int8 graph

Kotlin (Android, LiteRT CompiledModel)

Add implementation("com.google.ai.edge.litert:litert:2.2.0"), copy android/TimestralHost.kt into the app, and put the graph files and the three .f32 tables into the app's files directory (an app downloads them; for a test, adb push them to /data/local/tmp and copy them with adb shell run-as <package> cp). The two blocks below ran as written on a Galaxy S26 (Android 16) with each of the files and accelerators the comment in the second block names; on the phone's CPU, the float16 file and the -int8 file each gave quantiles bit-identical to those of the same file in Python on a desktop CPU for the same series.

import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.CompiledModel
import com.google.ai.edge.litert.Environment
import com.google.ai.edge.litert.TensorBuffer
import com.timestral.Bucket
import com.timestral.HostTables
import com.timestral.TimestralHost
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder

/**
 * [horizon] steps (1 to 1,024) after [series] (NaN = missing) as [horizon × 21] quantiles, step after step, from
 * [graph] in [dir]: a graph for contexts up to 2,048 steps and horizons up to 1,024 (`timestral-209m-c2048-h1024*`).
 */
fun timestralForecast(dir: File, graph: String, series: FloatArray, horizon: Int, options: CompiledModel.Options, env: Environment): FloatArray {
  fun floats(name: String) = ByteBuffer.wrap(File(dir, name).readBytes()).order(ByteOrder.LITTLE_ENDIAN)
    .asFloatBuffer().let { b -> FloatArray(b.remaining()).also { b.get(it) } }
  val tables = HostTables(floats("forecast_query.f32"), floats("rope_cos.f32"), floats("rope_sin.f32"), firstPosition = -1023)
  // Bucket = a graph's shape: context patches and horizon patches
  val bucket = Bucket("c2048-h1024", contextPatches = 64, horizonPatches = 32)
  val prepared = TimestralHost.prepare(series.copyOfRange(maxOf(0, series.size - 2048), series.size), horizon)
  val inputs = TimestralHost.buildInputs(prepared, bucket, tables, mask = -3e4f)
  val model = CompiledModel.create(File(dir, graph).absolutePath, options, env)
  val buffers = ArrayList<TensorBuffer>()
  try {
    val ins = inputs.byName().mapValues { (name, _) -> model.createInputBuffer(name, "serving_default").also { buffers += it } }
    val output = model.createOutputBuffer("output_0", "serving_default").also { buffers += it }
    for ((name, values) in inputs.byName()) ins.getValue(name).writeFloat(values)
    model.run(ins, mapOf("output_0" to output), "serving_default")
    return TimestralHost.finish(prepared, output.readFloat(), bucket)  // median of step i: [i * 21 + 10]
  } finally {
    buffers.forEach { it.close() }
    model.close()
  }
}

The accelerator is an option of the compile, and each accelerator has its file of the same shape; the host code stays the same:

val gpu = CompiledModel.Options(Accelerator.GPU).apply {
  gpuOptions = CompiledModel.GpuOptions(precision = CompiledModel.GpuOptions.Precision.FP16)
}
val cpu = CompiledModel.Options(Accelerator.CPU).apply { cpuOptions = CompiledModel.CpuOptions(numThreads = 4) }
val npu = CompiledModel.Options(Accelerator.NPU, Accelerator.CPU).apply {  // the Qualcomm NPU (HTP); needs the libraries below
  qualcommOptions = CompiledModel.QualcommOptions(htpPerformanceMode = CompiledModel.QualcommOptions.HtpPerformanceMode.BURST)
}
// One Environment for the process; with the Qualcomm libraries in the APK it names their directory for the NPU.
val libs = context.applicationInfo.nativeLibraryDir
val dirs = mapOf(Environment.Option.DispatchLibraryDir to libs, Environment.Option.CompilerPluginLibraryDir to libs)
val env = Environment.create(context, if (File(libs, "libLiteRtDispatch_Qualcomm.so").isFile) dirs else emptyMap())
// gpu or npu: this float16 file; cpu: "timestral-209m-c2048-h1024-int8.tflite".
val quantiles = timestralForecast(context.filesDir, "timestral-209m-c2048-h1024.tflite", series, 24, gpu, env)
  • The function compiles the graph on every call to keep the example short; an app keeps the CompiledModel and its buffers between forecasts. Run the compile, the calls and close() on one thread.
  • The float16 files are made for the GPU's FP16 precision (see How the graphs were made); set it explicitly, as above.
  • On the NPU, the graph is compiled on the phone at its first load; LiteRT keeps the result in the app's cache when the Environment is created with a Context, and the next process of the app loads it from there (logcat: Flatbuffer model initialized from cached model). When the compiler plugin is missing, LiteRT runs the graph on the CPU without an error: look for Replacing 1 out of 1 node(s) with delegate (DispatchDelegate) in logcat.
  • LiteRT starts the NPU runtime once per process, with the options of the first graph it compiles on any accelerator: compile the NPU graph first, or give every graph the same qualcommOptions, or a CPU or GPU graph compiled before it leaves the NPU without BURST.
  • The NPU libraries go into app/src/main/jniLibs/arm64-v8a/, with packaging { jniLibs { useLegacyPackaging = true } } so that the DSP-side library libQnnHtpV81Skel.so is extracted as a real file, which the NPU needs. They are not in this repository: the Qualcomm runtime may be distributed only inside an app.
  • In the run of these two blocks on a Galaxy S26 with timestral-209m-c2048-h1024.tflite, the first process on the NPU took 18.1 s for the compile on the phone, one forecast and close(); the next process took 0.3 s with the compiled graph from the app's cache.
Library From
libLiteRtDispatch_Qualcomm.so litert_npu_runtime_libraries.zip, an asset of the LiteRT v2.2.0 GitHub release (the v2.3.0 release carries no such asset)
libLiteRtCompilerPlugin_Qualcomm.so litert_npu_runtime_libraries_jit.zip, an asset of the LiteRT v2.2.0 GitHub release (the v2.3.0 release carries no such asset)
libQnnHtp.so, libQnnSystem.so, libQnnHtpPrepare.so, libQnnIr.so, libQnnSaver.so, libQnnHtpV81Stub.so, libQnnHtpV81CalculatorStub.so Qualcomm AI Runtime (QAIRT) SDK, lib/aarch64-android/
libQnnHtpV81Skel.so Qualcomm AI Runtime (QAIRT) SDK, lib/hexagon-v81/unsigned/

The NPU runs in this card used QAIRT 2.47.0 (the version its libQnnHtp.so reports).

V81 is the Hexagon version of the Snapdragon SM8850 (Galaxy S26); other chips take their own version (SM8550 v73, SM8650 v75, SM8750 v79).

The Android sample timestral in the LiteRT-Models zoo is a full app around the Kotlin above: paste a series, open a CSV file or pick a bundled example, choose the horizon and the accelerator, and it draws the end of the series, the forecast's median and its 10–90 % band. It loads the eight graphs and the three .f32 tables of this repository: its scripts/install_to_device.sh copies them from a directory on the computer into the app's files directory.

Demo

The Android sample on a Galaxy S26: 56 daily order counts pasted in (synthetic, the last 56 days of the sample's bundled example), a 14-day forecast on the NPU with timestral-209m-c2048-h256.tflite. The screen gives 17.7 ms for the graph call (writing the inputs, run(), reading the output back) and 4.1 ms for the host steps. It is the app's second forecast after the install; the first compiled the graph for the NPU in 16.3 s in that run. The clip has one cut, in a pause before the Forecast tap where the screen did not change.

Speed on phones

One forecast is one call of a two-row graph (the series and its negation): writing the five inputs, run() and reading the output back. The host steps before and after the call are not included. Each cell is the median of the timed calls (their minimum in brackets) with the file of that size and kind.

Phone, accelerator c2048-h256 c2048-h1024 c8192-h256 c8192-h1024
Galaxy S26 NPU (Qualcomm HTP), float16 files 15.6 ms (15.5) Not measured not used not used
Galaxy S26 GPU FP16, float16 files 27.0 ms (26.0) 28.8 ms (27.9) 68.7 ms (68.0) 73.4 ms (72.5)
Galaxy S26 CPU, 4 threads, -int8 files 30.3 ms (29.3) 39.3 ms (37.4) 128.5 ms (126.3) 144.2 ms (140.8)
Galaxy S26 CPU, 4 threads, float16 files 76.2 ms (73.6) 93.8 ms (88.2) 294.2 ms (282.0) 321.0 ms (309.9)
Pixel 8a GPU FP16, float16 files Not measured Not measured Not measured Not measured
Pixel 8a CPU, 4 threads, -int8 files Not measured Not measured Not measured Not measured
  • Galaxy S26 NPU (Qualcomm HTP), float16 files: LiteRT 2.2.0 C API in a native test program, not an app, with the QAIRT 2.47.0 libraries, compiled on the phone (JIT), HTP performance mode BURST; a fresh process, Android thermal status 0 (no throttling) at the start; 20 timed calls after 5 warm-up calls. First JIT compile 19.5 s.
  • Galaxy S26 GPU FP16, float16 files: LiteRT 2.2.0 Kotlin API (a benchmark app), GPU precision FP16; a fresh process, Android thermal status 0 (no throttling) at the start; 20 timed calls after 5 warm-up calls. Compile 1.6 s (c2048-h256), 1.4 s (c2048-h1024), 1.6 s (c8192-h256), 1.5 s (c8192-h1024); peak resident memory (VmHWM) 1.83 GiB; the GPU's clock ceiling stayed at 1,300 MHz.
  • Galaxy S26 CPU, 4 threads, -int8 files: LiteRT 2.2.0 Kotlin API (a benchmark app), 4 threads; a fresh process, Android thermal status 0 (no throttling) at the start; 20 timed calls after 5 warm-up calls. Compile 0.2 s; peak resident memory (VmHWM) 0.61 GiB (c2048-h256), 0.61 GiB (c2048-h1024), 0.63 GiB (c8192-h256), 0.64 GiB (c8192-h1024).
  • Galaxy S26 CPU, 4 threads, float16 files: LiteRT 2.2.0 Kotlin API (a benchmark app), 4 threads; a fresh process, Android thermal status 0 (no throttling) at the start; 20 timed calls after 5 warm-up calls. Compile 0.4 s; peak resident memory (VmHWM) 1.29 GiB (c2048-h256), 1.30 GiB (c2048-h1024), 1.31 GiB (c8192-h256), 1.32 GiB (c8192-h1024).
  • The c8192-h256 and c8192-h1024 files are not used on the NPU: on the Galaxy S26, other forms of these graphs (one row per call or float32 weights) took 82.0 to 96.5 ms per forecast on the NPU, against 68.7 to 73.4 ms for these files on its GPU.

Accuracy against the official forecaster

The reference is the official forecaster (PyTorch, float32, CPU, every default) on 84 windows: 72 from public datasets (UCI Air Quality, Datadog BOOM, UCI ElectricityLoadDiagrams20112014; contexts of up to 2,048 and of 8,192 steps, horizons of 24 to 720 steps) and 12 synthetic series. Each run below forecasts these windows with the graphs and the host code: on the desktop, all 84 windows, each with the smallest graph that holds it; on the phones, each file the windows of its size (the Windows column: a context of up to 2,048 steps for c2048, of 2,049 to 8,192 steps for c8192, and for h256 a horizon of up to 256 steps).

MASE is the mean absolute error of the median over the seasonal error of the context; CRPS is here the weighted quantile loss over the nine deciles 0.1 to 0.9 (the form the fev-bench and GIFT-Eval benchmarks use). Both are averaged over the 22 groups of windows (dataset, context length, horizon; MASE over 21, as the constant series has no seasonal error), and the ratios divide the run's values by the reference's. A ratio below 1 is not a better model: it is how far the float16 or int8 arithmetic moved the forecast on these windows, in either direction. The check for every run: both ratios at most 1.01, no NaN or infinite value, and the 21 quantiles in order at every step.

Run Accelerator Files Windows MASE ratio CRPS ratio NaN / inf Quantile order broken
Desktop (Apple M4 Max), LiteRT 2.2.0 CPU, 4 threads the four -int8 files 84 ×1.0006 ×0.9952 0 0
Desktop (Apple M4 Max), LiteRT 2.2.0 GPU (Metal), default precision FP16 the four float16 files 84 ×1.0008 ×0.9999 0 0
Galaxy S26, LiteRT 2.2.0 NPU (Qualcomm HTP) timestral-209m-c2048-h256.tflite 55 ×1.0049 ×0.9991 0 0
Galaxy S26 NPU (Qualcomm HTP) timestral-209m-c2048-h1024.tflite Not measured
Galaxy S26, LiteRT 2.2.0 GPU FP16 timestral-209m-c2048-h256.tflite 55 ×0.9954 ×0.9993 0 0
Galaxy S26, LiteRT 2.2.0 GPU FP16 timestral-209m-c2048-h1024.tflite 71 ×0.9924 ×0.9992 0 0
Galaxy S26, LiteRT 2.2.0 GPU FP16 timestral-209m-c8192-h256.tflite 5 ×1.0011 ×1.0015 0 0
Galaxy S26, LiteRT 2.2.0 GPU FP16 timestral-209m-c8192-h1024.tflite 13 ×0.9997 ×1.0002 0 0
Galaxy S26, LiteRT 2.2.0 CPU, 4 threads timestral-209m-c2048-h256-int8.tflite 55 ×1.0010 ×0.9920 0 0
Galaxy S26, LiteRT 2.2.0 CPU, 4 threads timestral-209m-c2048-h1024-int8.tflite 71 ×1.0007 ×0.9938 0 0
Galaxy S26, LiteRT 2.2.0 CPU, 4 threads timestral-209m-c8192-h256-int8.tflite 5 ×0.9999 ×0.9994 0 0
Galaxy S26, LiteRT 2.2.0 CPU, 4 threads timestral-209m-c8192-h1024-int8.tflite 13 ×0.9992 ×1.0001 0 0
Galaxy S26, LiteRT 2.2.0 CPU, 4 threads timestral-209m-c2048-h256.tflite 55 ×0.9999 ×1.0000 0 0
Galaxy S26, LiteRT 2.2.0 CPU, 4 threads timestral-209m-c2048-h1024.tflite 71 ×0.9999 ×1.0000 0 0
Galaxy S26, LiteRT 2.2.0 CPU, 4 threads timestral-209m-c8192-h256.tflite 5 ×1.0000 ×1.0000 0 0
Galaxy S26, LiteRT 2.2.0 CPU, 4 threads timestral-209m-c8192-h1024.tflite 13 ×1.0000 ×1.0000 0 0
Pixel 8a GPU FP16 the float16 files Not measured
Pixel 8a CPU, 4 threads the -int8 files Not measured

Limits

  • One series per forecast, without covariates: the multivariate rows, past-only and known-future covariates and categorical covariates of the official model are not part of these graphs.
  • A horizon of up to 1,024 steps (one decoder block of 32 patches). The official forecaster decodes longer horizons block by block, with each block's median appended to the context; host_forecast.py keeps that loop, but no result here covers it (the evaluation windows have horizons of up to 720 steps), and the Python class and the Kotlin host stop at 1,024 steps.
  • A context of up to 8,192 steps: of a longer series the last 8,192 steps are read, which is the official forecaster with max_context=8192 (its default is 32,768).
  • Float16 and int8: the GPU runs the float16 files at FP16, the Qualcomm HTP computes in float16 and the -int8 files compute most of their fully connected layers in int8, so their quantiles differ slightly from float32 (Accuracy above).
  • The NPU path was run on one chip, the Snapdragon SM8850 of a Galaxy S26.
  • The Python class refuses fewer than 16 values, or fewer than 16 observed ones; the Kotlin host in android/ and the official forecaster have no minimum.

How the graphs were made

The graphs are a re-implementation of the official PyTorch model (mistralai/timestral at commit a534bdbd244d) with fixed shapes, one per size, converted with litert-torch 0.9.4; the weights of the float16 files are cast to float16 with ai-edge-quantizer 0.9.0, and the inputs and output of every file stay float32. With float32 weights they give the official model's numbers (first line below).

  • The time attention takes the mask as an additive input instead of boolean masks, and both products are einsum, which keeps the batch matrix multiplications at rank 4.
  • With one series, the variate attention sees one key per query (its softmax is 1), so it is computed as the output projection of the value projection times the sigmoid gate.
  • The fused projections (wQKV, the SwiGLU's w12) are split into separate fully connected layers.
  • The decoder tokens and the RoPE rows are inputs that the host fills from the checkpoint's forecast_query and the official RoPE module, so no position arithmetic, gather or cast is in the graph.
  • The monotone quantile head (softplus, cumulative sums, flips) is one 42 × 21 fully connected layer over the softplus and raw outputs, with softplus written as relu(x) + log(1 + exp(−|x|)).
  • In the last layer only the decoder tokens go through the queries, the attention output, the variate attention and the MLP (the keys and values still come from every token): the head reads only those rows, so the function is the same.
  • Float16 range: Timestral-209M's attention logits and RMSNorm sums of squares leave the float16 range (third line below). Each RMSNorm input is pre-scaled by a power of two, the logits are computed at 1/16 with the row maximum subtracted before the scale is restored, the softmax input is clamped at −3·10⁴ and the mask value is −3·10⁴. In float32 these rewrites leave the results unchanged (first line below).
  • The -int8 files come from the float32 graphs through ai-edge-quantizer's dynamic-range int8 (second line below); their accuracy is in the CPU rows of the table above.
  • To skip the compile at the first load on the NPU, a float16 file can be compiled ahead of time for one Snapdragon chip with aot_compile (ai_edge_litert.aot.aot_compile) and the Qualcomm target of ai-edge-litert 2.2.0, on Linux x86-64 with Qualcomm's AOT package (it does not run on macOS); the result runs on that chip only. This repository holds no such file.
  • The float16 files' graphs with float32 weights on a desktop CPU (LiteRT 2.2.0), over the 84 windows: MASE ratio ×1.0000002, CRPS ratio ×1.0000001, largest relative quantile difference 7.5e-06, NaN / inf 0, quantile order broken 0.
  • The -int8 files: dynamic-range int8 from ai-edge-quantizer 0.9.0 (int8 weights per channel, inputs quantized at run time) on 199 of the 217 fully connected layers, 91.6 % of their weights: all those of the first 23 of the 24 transformer blocks. The other 18 keep float16 weights (3 in the patch embedding, 11 in the last block, 4 in the quantile head).
  • Activation ranges of the model in float32 over the same windows (each window and its negation, 168 calls): attention logits up to 242,875, RMSNorm sums of squares up to 164,602,176, the residual stream up to 9,534; the largest float16 value is 65,504.
  • timestral-209m-c2048-h256.tflite: 1,883 ops, tensors of rank 4 at most; ops the LiteRT GPU path rejects (gather, select, cast, cumsum and similar): none.
  • timestral-209m-c2048-h1024.tflite: 1,883 ops, tensors of rank 4 at most; ops the LiteRT GPU path rejects (gather, select, cast, cumsum and similar): none.
  • timestral-209m-c8192-h256.tflite: 1,883 ops, tensors of rank 4 at most; ops the LiteRT GPU path rejects (gather, select, cast, cumsum and similar): none.
  • timestral-209m-c8192-h1024.tflite: 1,883 ops, tensors of rank 4 at most; ops the LiteRT GPU path rejects (gather, select, cast, cumsum and similar): none.
  • Galaxy S26 NPU, timestral-209m-c2048-h256.tflite: Partitioned subgraph<0>, selected 1883 ops, from a total of 1883 ops. resulted in 1 partitions; Replacing 1 out of 1 node(s) with delegate (DispatchDelegate) node, yielding 1 partitions
  • Galaxy S26 GPU, timestral-209m-c2048-h256.tflite: Replacing 1883 out of 1883 node(s) with delegate (LITERT_CL) node, yielding 1 partitions
  • Galaxy S26 GPU, timestral-209m-c2048-h1024.tflite: Replacing 1883 out of 1883 node(s) with delegate (LITERT_CL) node, yielding 1 partitions
  • Galaxy S26 GPU, timestral-209m-c8192-h256.tflite: Replacing 1883 out of 1883 node(s) with delegate (LITERT_CL) node, yielding 1 partitions
  • Galaxy S26 GPU, timestral-209m-c8192-h1024.tflite: Replacing 1883 out of 1883 node(s) with delegate (LITERT_CL) node, yielding 1 partitions
  • Galaxy S26 CPU, timestral-209m-c2048-h256.tflite: Replacing 1883 out of 1883 node(s) with delegate (TfLiteXNNPackDelegate) node, yielding 1 partitions
  • Galaxy S26 CPU, timestral-209m-c2048-h1024.tflite: Replacing 1883 out of 1883 node(s) with delegate (TfLiteXNNPackDelegate) node, yielding 1 partitions
  • Galaxy S26 CPU, timestral-209m-c8192-h256.tflite: Replacing 1883 out of 1883 node(s) with delegate (TfLiteXNNPackDelegate) node, yielding 1 partitions
  • Galaxy S26 CPU, timestral-209m-c8192-h1024.tflite: Replacing 1883 out of 1883 node(s) with delegate (TfLiteXNNPackDelegate) node, yielding 1 partitions
  • Galaxy S26 CPU, timestral-209m-c2048-h256-int8.tflite: Replacing 1684 out of 1684 node(s) with delegate (TfLiteXNNPackDelegate) node, yielding 1 partitions
  • Galaxy S26 CPU, timestral-209m-c2048-h1024-int8.tflite: Replacing 1684 out of 1684 node(s) with delegate (TfLiteXNNPackDelegate) node, yielding 1 partitions
  • Galaxy S26 CPU, timestral-209m-c8192-h256-int8.tflite: Replacing 1684 out of 1684 node(s) with delegate (TfLiteXNNPackDelegate) node, yielding 1 partitions
  • Galaxy S26 CPU, timestral-209m-c8192-h1024-int8.tflite: Replacing 1684 out of 1684 node(s) with delegate (TfLiteXNNPackDelegate) node, yielding 1 partitions

Other ports

Timestral has also been converted to ONNX (amine40/Timestral-209M-ONNX and the other sizes, mrfakename/Timestral-209M-ONNX, onnx-community/Timestral-28M-ONNX and the 5M size, konfi-labs/Timestral-28M-batched-ONNX), MLX (jbilcke/timestral-mlx, vikiival/timestral-mlx) and GGUF (mudler/timestral-cpp-gguf, EvoAwaken-Workshop/Timestral-209M-gguf).

Upstream

  • Model: mistralai/Timestral-209M, revision 9874d252ab0aae16f7f7c999f98f98d6ad7d8365. Its card reports the model's zero-shot results on GIFT-Eval, TIME and fev-bench; this port does not re-run them.
  • Code: github.com/mistralai/timestral, commit a534bdbd244d64f3d9d743f46c7bf6fc330ed4ff, Apache-2.0.

License

The weights of Timestral-209M are licensed by Mistral AI under the Modified MIT License, and so are the graph files and the three .f32 tables of this repository, which are derived from them. The full text is in LICENSE. Attribution notice: 2026 - Mistral AI. Its condition 2 reads:

  1. You are not authorized to exercise any rights under this license if the global consolidated monthly revenue of your company (or that of your employer) exceeds $20 million (or its equivalent in another currency) for the preceding month. This restriction in (b) applies to the Model and any derivatives, modifications, or combined works based on it, whether provided by Mistral AI or by a third party. You may contact Mistral AI (sales@mistral.ai) to request a commercial license, which Mistral AI may grant you at its sole discretion, or choose to use the Model on Mistral AI's hosted services available at https://mistral.ai/.

Mistral AI's card adds: you must not use this model in a manner that infringes, misappropriates, or otherwise violates any third party's rights, including intellectual property rights.

The code files (host_forecast.py, timestral_litert.py, android/*.kt) port the official inference code and are licensed under the Apache License 2.0 (LICENSE-APACHE-2.0, NOTICE).

Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for mlboydaisuke/Timestral-209M-LiteRT

Quantized
(3)
this model