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
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.pyreproduit 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
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.