Pinned Post
Hochladen, prognostizieren, plotten — TimesFM in Phoenix LiveView
- Link abrufen
- X
- Andere Apps
Zeitreihen-Forecasting ist klassisch ein Feld für ARIMA (statistisches Modell aus Autoregression, Differenzbildung und gleitendem Mittel) und Prophet (Metas additives Modell für Trend, Saisonalität und Feiertagseffekte) & Co. — Modelle, die man pro Datensatz trainiert. Foundation-Modelle drehen das um: ein einziges, vortrainiertes Modell prognostiziert ohne Training beliebige neue Reihen. Googles TimesFM ist genau so ein Modell. In diesem Post baue ich eine kleine Phoenix-LiveView-App, die eine CSV-Zeitreihe entgegennimmt, sie mit TimesFM prognostiziert und das Ergebnis mit VegaLite zeichnet — und ich erkläre, warum das Modell weder über Bumblebee noch über Ortex geladen wird, sondern über Pythonx.
Was ist TimesFM — und wozu?
TimesFM ("Time Series Foundation Model") ist ein decoder-only Transformer von Google Research — architektonisch ein naher Verwandter von GPT, nur dass die Tokens keine Wörter sind, sondern Patches einer Zeitreihe. Das Modell zerlegt den historischen Verlauf in nicht-überlappende Patches, verarbeitet sie autoregressiv und sagt die nächsten Patches voraus. Längere Horizonte entstehen durch rekursives Rolling-Window-Decoding: Die Vorhersage wird zurückgefüttert.
Der eigentliche Nutzen ist das Zero-Shot-Forecasting:
- Kein Training, kein Tuning. Reihe rein, Forecast raus. Kein Pro-Datensatz-Fitting.
- Ein Modell für viele Domänen — Absatzzahlen, Sensordaten, Lastkurven, Metriken.
- Sinnvolle Baseline in Minuten statt eines Modellierungs-Projekts.
Für ein Dashboard, das „mal eben" eine Prognose über eine hochgeladene CSV legen soll, ist das ideal: Man spart sich die komplette Trainings-Pipeline.
Der schwierige Teil: das Modell in Elixir laden
Hier wird es interessant — und hier liegt die Entscheidung, die die gesamte Architektur bestimmt. Es gibt drei naheliegende Wege, und zwei davon funktionieren nicht so, wie man hofft.
Warum nicht Bumblebee?
Bumblebee ist die offensichtliche erste Idee — „HuggingFace-Modelle in Elixir". Aber Bumblebee kann keine beliebigen HuggingFace-Repos laden. Jede Architektur braucht eine eigene Implementierung in Axon im Bumblebee-Quellcode: BERT, GPT-2, Llama, Whisper, Stable Diffusion … TimesFM ist dort schlicht nicht implementiert.
# Das hier schlägt fehl — nicht wegen falscher Konfiguration,
# sondern weil die Architektur in Bumblebee fehlt:
Bumblebee.load_model({:hf, "google/timesfm-2.5-200m-pytorch"})
Das ist kein Bug, den man umgeht — es fehlt der Architektur-Port. Bumblebee fällt also aus.
Warum nicht Ortex?
Ortex lädt ONNX-Modelle über die ONNX Runtime — elixir-nativ, kein Python. Technisch ginge das, aber es bedeutet zwei unangenehme Aufgaben:
- Einen ONNX-Export von TimesFM beschaffen. Es gibt kein offizielles Standard-Artefakt; Community-Exporte schwanken in Qualität und Signatur.
- Die Pre-/Post-Processing-Pipeline in Nx nachbauen. Patching, Normalisierung und vor allem das rekursive Rolling-Window-Decoding liegen außerhalb des ONNX-Graphen. Genau dieser Teil ist der aufwändigste — und fehleranfällig.
Für einen Production-Service mit hohem Durchsatz und ohne Python-Abhängigkeit kann sich dieser Aufwand lohnen. Für „echtes TimesFM, schnell lauffähig" ist er unverhältnismäßig.
Der pragmatische Weg: Pythonx
Pythonx (von Dashbit/Livebook) bettet einen Python-Interpreter direkt in den BEAM-Prozess ein und konvertiert Daten zwischen Elixir und Python. Damit lässt sich das offizielle timesfm-Paket verwenden — echtes Modell, keine nachgebaute Pipeline.
Pythonx verwaltet sein eigenes Python-Environment über uv. Wir deklarieren die Abhängigkeiten als pyproject.toml-String und initialisieren lazy beim ersten Aufruf (der lädt einmalig torch, timesfm und den Checkpoint — mehrere GB):
@pyproject """
[project]
name = "timesfm_forecaster"
version = "0.0.0"
requires-python = "==3.11.*"
dependencies = ["timesfm[torch]", "numpy"]
"""
@load_code """
import numpy as np
import timesfm
model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
"google/timesfm-2.5-200m-pytorch"
)
model.compile(
timesfm.ForecastConfig(
max_context=1024, max_horizon=256,
normalize_inputs=True, use_continuous_quantile_head=True,
force_flip_invariance=True, infer_is_positive=True,
fix_quantile_crossing=True,
)
)
True
"""
Der eigentliche Forecast ist dann nur noch ein paar Zeilen Python, denen wir die Reihe und den Horizont als Globals übergeben:
@forecast_code """
arr = np.asarray(series, dtype=np.float32)
point_forecast, quantile_forecast = model.forecast(horizon=int(horizon), inputs=[arr])
[float(v) for v in point_forecast[0]]
"""
Wichtig: Python hat einen GIL — zwei Threads können nicht gleichzeitig Python ausführen. Deshalb läuft der gesamte Modellzugriff durch einen einzigen GenServer. Er serialisiert die Aufrufe, lädt das Modell genau einmal und hält die Python-Globals (inklusive model) zwischen den Aufrufen:
def handle_call({:forecast, series, horizon}, _from, state) do
state = ensure_python(state) # uv_init + Modell-Load, je einmal
context = Enum.take(series, -1024) # Kontext auf max_context begrenzen
h = horizon |> min(256) |> max(1)
globals = Map.merge(state.globals, %{"series" => context, "horizon" => h})
{result, _} = Pythonx.eval(@forecast_code, globals)
{:reply, {:ok, Pythonx.decode(result)}, state}
end
Weil der erste Aufruf minutenlang dauern kann (Download!), ruft die LiveView den GenServer nicht direkt auf, sondern aus einem Task heraus — so bleibt die UI responsiv und zeigt einen „Rechne …"-Status.
Die App: Upload → Forecast → Plot
Der Rest ist klassisches Phoenix. Drei bewegliche Teile:
- CSV-Upload über
allow_upload/3undlive_file_input. Ein kleiner Parser (NimbleCSV) erkennt eine Kopfzeile automatisch und unterstützt sowohl ein- als auch zweispaltige Reihen (Wert bzw. Zeitstempel + Wert). - Forecast asynchron über den
Forecaster-GenServer (siehe oben). - Visualisierung mit VegaLite: Verlauf und Forecast werden in einem Liniendiagramm gelayert. Der Forecast beginnt am letzten historischen Punkt, damit die Linien nahtlos anschließen. Die fertige Spec wird per
push_event/3an einen kleinen JS-Hook geschickt, dervegaEmbedaufruft.
Vl.new(width: "container", height: 420, title: "TimesFM 2.5 — Zeitreihe & Forecast")
|> Vl.data_from_values(history ++ forecast_points)
|> Vl.mark(:line, point: true, tooltip: true)
|> Vl.encode_field(:x, "x", type: :quantitative, title: "Zeitschritt")
|> Vl.encode_field(:y, "y", type: :quantitative, title: "Wert")
|> Vl.encode_field(:color, "Reihe",
type: :nominal,
scale: [domain: ["Verlauf", "Forecast"], range: ["#2563eb", "#dc2626"]]
)
|> Vl.to_spec()
Und so sieht das Ergebnis aus — eine saisonale Reihe mit Aufwärtstrend, blau der hochgeladene Verlauf (120 Punkte), rot der TimesFM-Forecast über 64 Schritte:
Schön zu sehen: TimesFM setzt Saisonalität und Trend der Reihe ohne jedes Training korrekt fort.
Fazit
- TimesFM ist ein decoder-only Foundation-Model, das Zeitreihen zero-shot prognostiziert — ideal für „schnell eine Prognose über fremde Daten legen".
- Bumblebee scheidet aus (Architektur nicht portiert), Ortex wäre möglich, verlangt aber ONNX-Export plus den Nachbau der Decoding-Pipeline in Nx.
- Pythonx ist der pragmatische Mittelweg: das echte Python-Modell im BEAM-Prozess, serialisiert über einen GenServer, mit Phoenix/LiveView als Frontend und VegaLite für die Visualisierung.
Zwei ehrliche Stolpersteine: Der erste Aufruf lädt mehrere GB (Torch + Checkpoint), und die timesfm-API ist historisch wandelbar — beides ist im Code an genau einer Stelle gekapselt. Für ein internes Forecasting-Tool ist dieser Weg in überschaubarer Zeit lauffähig — und nutzt das echte Modell, nicht eine Annäherung.
- Link abrufen
- X
- Andere Apps


Kommentare