Instructions to use mlboydaisuke/Timestral-209M-LiteRT with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LiteRT
How to use mlboydaisuke/Timestral-209M-LiteRT with LiteRT:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
File size: 35,809 Bytes
11f5cc3 ff9034d 11f5cc3 ff9034d ac11baa ff9034d 11f5cc3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | ---
license: other
license_name: modified-mit
license_link: LICENSE
library_name: litert
pipeline_tag: time-series-forecasting
base_model: mistralai/Timestral-209M
base_model_relation: quantized
tags:
- litert
- tflite
- android
- on-device
- gpu
- npu
- time-series
- forecasting
- timestral
---
# Timestral-209M — LiteRT
Forecast one time series on a phone with [Timestral-209M](https://huggingface.co/mistralai/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](#speed-on-phones)).

*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](https://github.com/john-rocky/LiteRT-Models/tree/main/timestral) 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
<!-- facts: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` |
<!-- /facts:files -->
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:
<!-- facts:cpu -->
- 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).
<!-- /facts:cpu -->
| 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:
```bash
pip install ai-edge-litert==2.2.0 numpy==2.5.4 huggingface_hub==2.2.0
```
```python
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:
```python
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.
```kotlin
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:
```kotlin
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.
<!-- facts:npu_check -->
- 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.
<!-- /facts:npu_check -->
| 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/` |
<!-- facts:qairt -->
The NPU runs in this card used QAIRT 2.47.0 (the version its `libQnnHtp.so` reports).
<!-- /facts:qairt -->
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](https://github.com/john-rocky/LiteRT-Models/tree/main/timestral) 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
<video src="https://huggingface.co/mlboydaisuke/Timestral-209M-LiteRT/resolve/main/assets/demo.mp4" controls muted playsinline width="360"></video>
The Android sample on a Galaxy S26: a year of daily order counts pasted in (365 values, synthetic: the sample's bundled example), a 14-day forecast on the NPU with `timestral-209m-c2048-h256.tflite`; the chart shows the last 64 days and the forecast. The screen gives 18.0 ms for the graph call (writing the inputs, `run()`, reading the output back) and 7.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 23.6 s in that run. Paste as much history as you have (these graphs read up to 8,192 steps, the NPU graphs 2,048). With only the last 56 of these days, the official forecaster's first-day median is 51.94 instead of 58.25 (16 weeks: 52.52): 8 weeks do not cover this series' 13-week cycle, and one cycle is not enough either. The screen's 58.27 is the NPU's float16 result for the same year.
## 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.
<!-- facts:speed -->
| 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.
<!-- /facts:speed -->
## 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.
<!-- facts:accuracy -->
| 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 | | | | |
<!-- /facts:accuracy -->
## 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.
<!-- facts:conversion -->
- 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.
<!-- /facts:conversion -->
<!-- facts:graph -->
- `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`
<!-- /facts:graph -->
## Other ports
Timestral has also been converted to ONNX ([amine40/Timestral-209M-ONNX](https://huggingface.co/amine40/Timestral-209M-ONNX) and the other sizes, [mrfakename/Timestral-209M-ONNX](https://huggingface.co/mrfakename/Timestral-209M-ONNX), [onnx-community/Timestral-28M-ONNX](https://huggingface.co/onnx-community/Timestral-28M-ONNX) and the 5M size, [konfi-labs/Timestral-28M-batched-ONNX](https://huggingface.co/konfi-labs/Timestral-28M-batched-ONNX)), MLX ([jbilcke/timestral-mlx](https://huggingface.co/jbilcke/timestral-mlx), [vikiival/timestral-mlx](https://huggingface.co/vikiival/timestral-mlx)) and GGUF ([mudler/timestral-cpp-gguf](https://huggingface.co/mudler/timestral-cpp-gguf), [EvoAwaken-Workshop/Timestral-209M-gguf](https://huggingface.co/EvoAwaken-Workshop/Timestral-209M-gguf)).
## Upstream
- Model: [mistralai/Timestral-209M](https://huggingface.co/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](https://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:
> 2. 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`).
|