# Vitrine TAKT : prévision de séries temporelles en Rust (DLinear et PatchTST sur ETT) Deux modèles standard de prévision à long terme, DLinear (LTSF-Linear) et PatchTST, entraînés avec les scripts officiels sur les jeux de données ETT, traduits de PyTorch en Rust pur (sans BLAS, sans runtime ML) et compilés deux fois : le build d’origine de ce code Rust et le build TAKT du même code. Les prédictions des deux builds sont identiques au bit près. Vous trouverez ici les programmes prêts à l’emploi pour les deux builds, le code source de la version d’origine, un script qui recrée les fenêtres d’entrée à partir des données officielles, un script de benchmark et nos mesures. ## Ce que calculent les programmes Chaque fenêtre de la partition de test d’un jeu ETT (bornes officielles, features M, StandardScaler ajusté sur la partition d’entraînement) : 336 pas passés × 7 canaux en entrée, les 96 pas suivants × 7 canaux en sortie, en float32. ETTh1/ETTh2 : 2 785 fenêtres ; ETTm1/ETTm2 : 11 425 fenêtres. Les poids du checkpoint du jeu sélectionné sont compilés dans le programme (`--data`). | Modèle | Jeu de données | MSE / MAE de test (toutes les fenêtres, `tools/score.py`) | Entraînement officiel, 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 (2 688 premières : 0,381618 / 0,405088) | 0,381618 / 0,405088 | | PatchTST | ETTh2 | 0,274616 / 0,337234 (2 688 premières : 0,274118 / 0,336000) | 0,274117 / 0,336000 | La boucle de test officielle de PatchTST écarte le dernier lot incomplet de 128 fenêtres, d’où la mention « 2 688 premières ». La traduction en Rust n’est pas identique bit à bit à PyTorch (ordre de sommation différent ; l’écart absolu maximal avec PyTorch eager va de 2·10⁻⁶ à 2·10⁻⁵) ; le build TAKT est identique bit à bit à l’original en Rust. ## Mesure (banc TAKT) Mesuré le 3 octobre 2026. AMD Threadripper PRO 5975WX (Zen 3), Linux. Un CCX de 8 cœurs réservé, leurs jumeaux SMT inactifs ; les exécutions monothread sont épinglées sur un cœur, les exécutions à huit threads sur les 8 cœurs du CCX. Pour chaque jeu de données : une paire de préchauffage, puis 9 tours en ordre alterné, médiane. Temps : la boucle de prédiction, chronométrée par le programme lui-même (la lecture des fenêtres et l’écriture des prédictions sont exclues ; elles coûtent autant dans les deux builds). Cycles : cycles en mode utilisateur de la même boucle (`perf stat`, additionnés sur tous les threads). Les deux builds partagent le même programme pilote, qui conserve dans le processus la mémoire du tas libérée (`mallopt` de glibc), comme le ferait un service de longue durée, afin que les tampons temporaires par fenêtre ne se transforment pas en défauts de page. Ce sont exactement les fichiers de `bin/` qui ont été mesurés, avec `tools/run_bench.sh`. | Modèle | Jeu de données | Fenêtres | Threads | Original, ms | TAKT, ms | Accélération | Original, Mcycles | TAKT, Mcycles | Accélération en cycles | Tours identiques | |---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:| | 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× en temps et 7,2–7,4× en cycles, avec un comme avec huit threads. PatchTST : 2,3× en temps comme en cycles. Avec huit threads, les deux builds consomment à peu près le même total de cycles qu’avec un seul. Le processus complet, y compris la lecture de 26–107 Mo de fenêtres et l’écriture des prédictions, est 5,7–6,0× plus rapide pour DLinear avec un thread, 3,0–3,1× avec huit, et 2,3× pour PatchTST. Les détails, les échantillons bruts et les temps du processus complet figurent dans `measurements.json`. ## Exécution ```sh python3 tools/make_windows.py --out data --targets # télécharge les CSV ETT officiels (vérifiés par sha256), # écrit data/windows_*_test.f32 (et les cibles) 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 # fenêtres, cibles et prédictions identiques à notre mesure python3 tools/score.py data/targets_ETTh1_test.f32 out/dlinear_ETTh1.f32 tools/run_bench.sh -m dlinear -n 9 -c 2 # les deux builds, entrelacés, vérification octet par octet, cœur 2 tools/run_bench.sh -m dlinear -n 9 -t 8 -c 2-9 # huit threads sur les cœurs 2-9 tools/run_bench.sh -m patchtst -n 9 -c 2 ``` `--threads T` accepte tout T ≥ 1 ; les prédictions ne dépendent pas de T. Les fichiers d’entrée et de sortie sont des float32 bruts little-endian (n × 336 × 7 et n × 96 × 7) ; `tools/make_windows.py` ne nécessite que numpy. Linux x86-64 (glibc). Les programmes sont compilés pour x86-64-v3 (AVX2, FMA, BMI2 : Intel Haswell et plus récents, AMD Zen et plus récents) ; les instructions FMA ne sont pas utilisées. PatchTST appelle `erff` de la bibliothèque C du système : avec une glibc dont `erff` diffère de celui de glibc 2.39 (Ubuntu 24.04), ses prédictions peuvent différer de `expected.sha256`, tandis que l’original et le build TAKT restent identiques octet par octet. ## Équivalence - À chaque tour mesuré, les prédictions des deux builds étaient identiques à l’octet près (décomptes dans le tableau). - Les deux builds reproduisent, octet par octet, les prédictions enregistrées lors de notre exécution précédente : DLinear sur quatre jeux de données et PatchTST sur deux, partitions de test et de validation, 1, 8 et 32 threads : 72 fichiers sur 72. - `tools/make_windows.py` reproduit octet par octet les fenêtres des chargeurs de données officiels LTSF-Linear / PatchTST (les quatre jeux de données, test et validation ; le parseur de flottants par défaut de pandas est réimplémenté à cet effet). ## Recompiler l’original ```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 ``` Avec Rust 1.96.0 installé via rustup (avec le composant `rust-src`), cela reproduit `bin/ltsf-dlinear-original` et `bin/ltsf-patchtst-original` bit à bit sur notre machine ; avec une autre configuration, les chemins embarqués diffèrent, mais pas les prédictions. ## Comparaison antérieure avec PyTorch (non reproductible avec cette archive) Le 2 octobre 2026, nous avons mesuré les mêmes modèles sur la même machine dans d’autres conditions : sans réservation du banc (39 CPU logiques partagés avec d’autres tâches) ; les deux versions Rust compilées en bibliothèques partagées pour le CPU natif et appelées dans le processus ; PyTorch 2.14 sur CPU, en mode eager et avec `torch.compile` (Inductor), par lots de 64 ou 512. Pour chaque implémentation, le nombre de threads (1, 8 ou 32) et la taille de lot ont été choisis selon la latence la plus faible sur la partition de validation, puis la partition de test a été chronométrée (médiane de 5). | Modèle | Jeu de données | Rust d’origine | Build TAKT | PyTorch le plus rapide (Inductor) | PyTorch / TAKT | |---|---|---:|---:|---:|---:| | DLinear | ETTh1 | 28,7 ms (32 threads) | 5,1 ms (32) | 18,5 ms (32, lot de 512) | 3,63× | | DLinear | ETTh2 | 30,1 ms (32) | 5,3 ms (32) | 12,5 ms (32, lot de 512) | 2,34× | | DLinear | ETTm1 | 115,4 ms (32) | 19,7 ms (32) | 65,1 ms (8, lot de 512) | 3,31× | | DLinear | ETTm2 | 114,9 ms (32) | 20,4 ms (32) | 49,5 ms (32, lot de 512) | 2,43× | | PatchTST | ETTh1 | 593 ms (32) | 236 ms (32) | 634 ms (32, lot de 64) | 2,69× | | PatchTST | ETTh2 | 519 ms (32) | 234 ms (32) | 551 ms (32, lot de 64) | 2,35× | En monothread, l’ordre était différent : PyTorch avec Inductor et des lots était plus rapide que le build TAKT (DLinear ETTh1 : 37,9 ms contre 111,4 ms ; PatchTST ETTh1 : 2,03 s contre 5,78 s). Lors de cette exécution, le build TAKT était 5,6–5,9× (DLinear) et 2,2–2,5× (PatchTST) plus rapide que le Rust d’origine avec les 32 threads retenus, et 7,2–7,3× et 2,3× en monothread. Tous les chiffres de cette exécution figurent dans `measurements.json` (`earlier_measurement_2026_10_02`). ## Contenu - `bin/` : `ltsf-dlinear-original`, `ltsf-dlinear-takt`, `ltsf-patchtst-original`, `ltsf-patchtst-takt` (code Rust lié statiquement, poids compilés dans le binaire, symboles supprimés) ; - `source/` : la version d’origine : programme pilote en ligne de commande, code du modèle généré et poids ; - `tools/` : `make_windows.py` (fenêtres d’entrée), `score.py` (MSE/MAE), `run_bench.sh` (benchmark) ; - `expected.sha256`, `measurements.json`, `SHA256SUMS`, `LICENSE`. Les données ETT ne sont pas incluses (CC BY-ND 4.0, téléchargées par `tools/make_windows.py`). Le code source du build TAKT n’est pas publié : nous livrons des builds.