# 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 ```sh 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.py` reproduziert 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 ```sh 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.