TAKT-Showcase: Zeitreihenprognose in Rust (DLinear und PatchTST auf ETT)
Zwei Standardmodelle für Langzeitprognosen, DLinear (LTSF-Linear) und PatchTST, mit den offiziellen Skripten auf den ETT-Datensätzen trainiert, aus PyTorch in reines Rust übersetzt (kein BLAS, keine ML-Laufzeitumgebung) und zweimal gebaut: der ursprüngliche Build dieses Rust-Codes und der TAKT-Build desselben Codes. Die Vorhersagen beider Builds sind bis aufs Bit identisch. Das Paket enthält fertige Programme für beide Builds, den Quellcode der ursprünglichen Version, ein Skript, das die Eingabefenster aus den offiziellen Daten neu erzeugt, ein Benchmark-Skript und unsere Messungen.
Was die Programme berechnen
Jedes Fenster des Test-Splits eines ETT-Datensatzes (offizielle Grenzen, Features M, StandardScaler
auf dem Train-Split angepasst): 336 vergangene Schritte x 7 Kanäle als Eingabe, die nächsten
96 Schritte x 7 Kanäle als Ausgabe, float32. ETTh1/ETTh2: 2.785 Fenster; ETTm1/ETTm2: 11.425 Fenster.
Die Gewichte des Checkpoints für den gewählten Datensatz sind in das Programm einkompiliert (--data).
| Modell | Datensatz | Test-MSE / -MAE (alle Fenster, tools/score.py) |
Offizieller Trainingslauf, Test |
|---|---|---|---|
| DLinear | ETTh1 | 0,384144 / 0,404713 | 0,384144 / 0,404713 |
| DLinear | ETTh2 | 0,290098 / 0,353328 | 0,290098 / 0,353328 |
| DLinear | ETTm1 | 0,301222 / 0,344619 | 0,301222 / 0,344619 |
| DLinear | ETTm2 | 0,171852 / 0,267122 | 0,171852 / 0,267122 |
| PatchTST | ETTh1 | 0,385126 / 0,405953 (erste 2.688: 0,381618 / 0,405088) | 0,381618 / 0,405088 |
| PatchTST | ETTh2 | 0,274616 / 0,337234 (erste 2.688: 0,274118 / 0,336000) | 0,274117 / 0,336000 |
Die offizielle Testschleife von PatchTST verwirft den letzten unvollständigen Batch von 128 Fenstern, daher die Angabe „erste 2.688“. Die Rust-Übersetzung ist nicht bitidentisch mit PyTorch (andere Summationsreihenfolge; die größte absolute Abweichung gegenüber PyTorch eager beträgt 2e-6 bis 2e-5); der TAKT-Build ist bitidentisch mit dem Rust-Original.
Messung (TAKT-Messstand)
Gemessen am 03.10.2026. AMD Threadripper PRO 5975WX (Zen 3), Linux. Ein CCX mit 8 Kernen reserviert, deren zweite SMT-Threads im Leerlauf;
Läufe mit einem Thread an einen Kern gebunden, Läufe mit acht Threads an die 8 Kerne des CCX. Für
jeden Datensatz: ein Aufwärmpaar, dann 9 Runden in abwechselnder Reihenfolge, Median. Zeit: die vom
Programm selbst gemessene Vorhersageschleife (Lesen der Fenster und Schreiben der Vorhersagen sind
ausgenommen; sie kosten in beiden Builds gleich viel). Takte: Takte derselben Schleife im User-Modus
(perf stat, summiert über alle Threads). Beide Builds nutzen denselben Treiber, der freigegebenen
Heap-Speicher im Prozess behält (glibc mallopt), wie es ein langlaufender Dienst täte, damit
temporäre Puffer pro Fenster nicht zu Page Faults werden. Gemessen wurden genau die Dateien in
bin/, mit tools/run_bench.sh.
| Modell | Datensatz | Fenster | Threads | Original, ms | TAKT, ms | Beschleunigung | Original, Mio. Takte | TAKT, Mio. Takte | Beschleunigung nach Takten | Identische Runden |
|---|---|---|---|---|---|---|---|---|---|---|
| DLinear | ETTh1 | 2.785 | 1 | 788,4 | 111,0 | 7,10× | 3.518 | 486 | 7,23× | 9/9 |
| DLinear | ETTh2 | 2.785 | 1 | 785,0 | 108,4 | 7,24× | 3.512 | 476 | 7,38× | 9/9 |
| DLinear | ETTm1 | 11.425 | 1 | 3.223,9 | 447,8 | 7,20× | 14.421 | 1.978 | 7,29× | 9/9 |
| DLinear | ETTm2 | 11.425 | 1 | 3.228,4 | 442,6 | 7,29× | 14.405 | 1.984 | 7,26× | 9/9 |
| DLinear | ETTh1 | 2.785 | 8 | 103,3 | 14,5 | 7,11× | 3.515 | 486 | 7,23× | 9/9 |
| DLinear | ETTh2 | 2.785 | 8 | 103,3 | 14,2 | 7,26× | 3.514 | 477 | 7,37× | 9/9 |
| DLinear | ETTm1 | 11.425 | 8 | 423,3 | 58,6 | 7,22× | 14.426 | 1.988 | 7,26× | 9/9 |
| DLinear | ETTm2 | 11.425 | 8 | 422,1 | 58,9 | 7,17× | 14.407 | 1.998 | 7,21× | 9/9 |
| PatchTST | ETTh1 | 2.785 | 1 | 13.440,8 | 5.733,4 | 2,34× | 59.973 | 25.507 | 2,35× | 9/9 |
| PatchTST | ETTh2 | 2.785 | 1 | 13.484,3 | 5.749,2 | 2,35× | 60.068 | 25.645 | 2,34× | 9/9 |
| PatchTST | ETTh1 | 2.785 | 8 | 1.777,5 | 765,7 | 2,32× | 60.424 | 25.955 | 2,33× | 9/9 |
| PatchTST | ETTh2 | 2.785 | 8 | 1.785,3 | 769,3 | 2,32× | 60.677 | 26.070 | 2,33× | 9/9 |
DLinear: 7,1–7,3× nach Zeit und 7,2–7,4× nach Takten, mit einem und mit acht Threads. PatchTST:
2,3× nach Zeit und nach Takten. Mit acht Threads verbrauchen beide Builds insgesamt etwa gleich viele
Takte wie mit einem. Der gesamte Prozess, einschließlich des Lesens von 26–107 MB Fenstern und des
Schreibens der Vorhersagen, ist bei DLinear mit einem Thread 5,7–6,0× schneller, mit acht 3,0–3,1×
und bei PatchTST 2,3×. Details, Rohmessungen und Zeiten des Gesamtprozesses stehen in
measurements.json.
Ausführen
python3 tools/make_windows.py --out data --targets # lädt die offiziellen ETT-CSVs herunter (mit sha256-Prüfung),
# schreibt data/windows_*_test.f32 (und die Zielwerte)
mkdir -p out
for d in ETTh1 ETTh2 ETTm1 ETTm2; do
bin/ltsf-dlinear-takt --data $d --windows data/windows_${d}_test.f32 --threads 1 --out out/dlinear_$d.f32
done
for d in ETTh1 ETTh2; do
bin/ltsf-patchtst-takt --data $d --windows data/windows_${d}_test.f32 --threads 1 --out out/patchtst_$d.f32
done
sha256sum -c expected.sha256 # Fenster, Zielwerte und Vorhersagen wie in unserer Messung
python3 tools/score.py data/targets_ETTh1_test.f32 out/dlinear_ETTh1.f32
tools/run_bench.sh -m dlinear -n 9 -c 2 # beide Builds, abwechselnd, Prüfung Byte für Byte, Kern 2
tools/run_bench.sh -m dlinear -n 9 -t 8 -c 2-9 # acht Threads auf den Kernen 2-9
tools/run_bench.sh -m patchtst -n 9 -c 2
--threads T akzeptiert jedes T >= 1; die Vorhersagen hängen nicht von T ab. Ein- und Ausgabedateien
sind rohe Little-Endian-float32-Daten (n x 336 x 7 bzw. n x 96 x 7); tools/make_windows.py benötigt
nur numpy.
Linux x86-64 (glibc). Die Programme sind für x86-64-v3 gebaut (AVX2, FMA, BMI2: Intel Haswell und
neuer, AMD Zen und neuer); FMA-Instruktionen werden nicht verwendet. PatchTST ruft erff aus der
C-Bibliothek des Systems auf: Auf einer glibc, deren erff sich von glibc 2.39 (Ubuntu 24.04)
unterscheidet, können seine Vorhersagen von expected.sha256 abweichen, während Original und
TAKT-Build weiterhin Byte für Byte übereinstimmen.
Äquivalenz
- In jeder gemessenen Runde waren die Vorhersagen beider Builds byte-identisch (Anzahl in der Tabelle).
- Beide Builds reproduzieren Byte für Byte die in unserem früheren Lauf gespeicherten Vorhersagen: DLinear auf vier Datensätzen und PatchTST auf zwei, Test- und Validierungs-Split, 1, 8 und 32 Threads: 72 von 72 Dateien.
tools/make_windows.pyreproduziert die Fenster der offiziellen Data Loader von LTSF-Linear / PatchTST Byte für Byte (alle vier Datensätze, Test und Validierung; dafür wurde der Standard-Float-Parser von pandas nachimplementiert).
Original neu bauen
cd source
RUSTFLAGS="-C target-cpu=x86-64-v3 --remap-path-prefix=$HOME/.rustup=. \
--remap-path-prefix=$HOME/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f=." \
cargo +1.96.0 build --release --locked
strip --strip-all -o ltsf-dlinear-original target/release/ltsf-dlinear
Mit Rust 1.96.0 aus rustup (mit der Komponente rust-src) reproduziert dies auf unserer Maschine
bin/ltsf-dlinear-original und bin/ltsf-patchtst-original Bit für Bit; bei einem anderen Setup
unterscheiden sich die eingebetteten Pfade, die Vorhersagen nicht.
Früherer Vergleich mit PyTorch (mit diesem Paket nicht reproduzierbar)
Am 02.10.2026 haben wir dieselben Modelle auf derselben Maschine unter anderen Bedingungen gemessen:
ohne Reservierung des Messstands (39 logische CPUs, mit anderen Jobs geteilt); beide Rust-Versionen
als Shared Libraries für die native CPU gebaut und im selben Prozess aufgerufen; PyTorch 2.14 auf der
CPU, eager und torch.compile (Inductor), Batches von 64 oder 512. Für jede Implementierung wurden
die Thread-Anzahl (1, 8 oder 32) und die Batch-Größe nach der niedrigsten Latenz auf dem
Validierungs-Split gewählt, dann wurde der Test-Split gemessen (Median aus 5).
| Modell | Datensatz | Original-Rust | TAKT-Build | Schnellstes PyTorch (Inductor) | PyTorch / TAKT |
|---|---|---|---|---|---|
| DLinear | ETTh1 | 28,7 ms (32 Thr.) | 5,1 ms (32) | 18,5 ms (32, Batch 512) | 3,63× |
| DLinear | ETTh2 | 30,1 ms (32) | 5,3 ms (32) | 12,5 ms (32, Batch 512) | 2,34× |
| DLinear | ETTm1 | 115,4 ms (32) | 19,7 ms (32) | 65,1 ms (8, Batch 512) | 3,31× |
| DLinear | ETTm2 | 114,9 ms (32) | 20,4 ms (32) | 49,5 ms (32, Batch 512) | 2,43× |
| PatchTST | ETTh1 | 593 ms (32) | 236 ms (32) | 634 ms (32, Batch 64) | 2,69× |
| PatchTST | ETTh2 | 519 ms (32) | 234 ms (32) | 551 ms (32, Batch 64) | 2,35× |
Mit einem Thread war die Reihenfolge anders: PyTorch mit Inductor und Batches war schneller als der
TAKT-Build (DLinear ETTh1: 37,9 ms gegenüber 111,4 ms; PatchTST ETTh1: 2,03 s gegenüber 5,78 s). In
jenem Lauf war der TAKT-Build bei den gewählten 32 Threads 5,6–5,9× (DLinear) und 2,2–2,5× (PatchTST)
schneller als das ursprüngliche Rust, mit einem Thread 7,2–7,3× bzw. 2,3×. Alle Zahlen jenes Laufs
stehen in measurements.json (earlier_measurement_2026_10_02).
Inhalt
bin/:ltsf-dlinear-original,ltsf-dlinear-takt,ltsf-patchtst-original,ltsf-patchtst-takt(statisch gelinkter Rust-Code, Gewichte einkompiliert, gestrippt);source/: die ursprüngliche Version: Kommandozeilen-Treiber, generierter Modellcode und Gewichte;tools/:make_windows.py(Eingabefenster),score.py(MSE/MAE),run_bench.sh(Benchmark);expected.sha256,measurements.json,SHA256SUMS,LICENSE.
Die ETT-Daten sind nicht enthalten (CC BY-ND 4.0, sie werden von tools/make_windows.py
heruntergeladen). Der Quellcode des TAKT-Builds wird nicht veröffentlicht: Wir liefern Builds.