Vetrina TAKT: previsione di serie temporali in Rust (DLinear e PatchTST su ETT)
Due modelli standard di previsione a lungo termine, DLinear (LTSF-Linear) e PatchTST, addestrati con gli script ufficiali sui dataset ETT, tradotti da PyTorch in Rust puro (senza BLAS, senza runtime ML) e compilati due volte: la build originale di quel codice Rust e la build TAKT dello stesso codice. Le previsioni delle due build sono identiche fino all’ultimo bit. Qui si trovano i programmi pronti per entrambe le build, il sorgente della versione originale, uno script che ricrea le finestre di input dai dati ufficiali, uno script di benchmark e le nostre misure.
Che cosa calcolano i programmi
Ogni finestra dello split di test di un dataset ETT (confini ufficiali, feature M, StandardScaler
adattato sullo split di addestramento): in ingresso 336 passi precedenti x 7 canali, in uscita i 96
passi successivi x 7 canali, float32. ETTh1/ETTh2: 2.785 finestre; ETTm1/ETTm2: 11.425 finestre. I
pesi del checkpoint del dataset selezionato sono compilati nel programma (--data).
| Modello | Dataset | MSE / MAE di test (tutte le finestre, tools/score.py) |
Addestramento ufficiale, 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 (prime 2.688: 0,381618 / 0,405088) | 0,381618 / 0,405088 |
| PatchTST | ETTh2 | 0,274616 / 0,337234 (prime 2.688: 0,274118 / 0,336000) | 0,274117 / 0,336000 |
Il loop di test ufficiale di PatchTST scarta l’ultimo batch incompleto di 128 finestre, da cui il dato «prime 2.688». La traduzione in Rust non è identica bit per bit a PyTorch (diverso ordine di sommatoria; la massima differenza assoluta rispetto a PyTorch eager va da 2e-6 a 2e-5); la build TAKT è identica bit per bit all’originale Rust.
Misura (banco TAKT)
Misure del 3 ottobre 2026. AMD Threadripper PRO 5975WX (Zen 3), Linux. Un CCX di 8 core riservato,
con i relativi gemelli SMT inattivi; le esecuzioni a un thread sono fissate su un core, quelle a otto
thread sugli 8 core del CCX. Per ogni dataset: una coppia di riscaldamento, poi 9 round in ordine
alternato, mediana. Tempo: il loop di previsione cronometrato dal programma stesso (sono escluse la
lettura delle finestre e la scrittura delle previsioni, che costano lo stesso in entrambe le build).
Cicli: i cicli in modalità utente dello stesso loop (perf stat, sommati su tutti i thread).
Entrambe le build condividono lo stesso driver, che mantiene nel processo la memoria heap liberata
(mallopt di glibc) come farebbe un servizio di lunga durata, così che i buffer temporanei per
finestra non si trasformino in page fault. Sono stati misurati esattamente i file in bin/, con
tools/run_bench.sh.
| Modello | Dataset | Finestre | Thread | Originale, ms | TAKT, ms | Accelerazione | Originale, mln di cicli | TAKT, mln di cicli | Accelerazione in cicli | Round identici |
|---|---|---|---|---|---|---|---|---|---|---|
| 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× in tempo e 7,2–7,4× in cicli, sia con uno sia con otto thread. PatchTST: 2,3× in
tempo e in cicli. Con otto thread entrambe le build spendono in totale circa gli stessi cicli che con
uno. L’intero processo, compresa la lettura di 26–107 MB di finestre e la scrittura delle previsioni,
è 5,7–6,0× più veloce per DLinear con un thread, 3,0–3,1× con otto, e 2,3× per PatchTST. Dettagli,
campioni grezzi e tempi dell’intero processo sono in measurements.json.
Esecuzione
python3 tools/make_windows.py --out data --targets # scarica i CSV ufficiali di ETT (verificati con sha256),
# scrive data/windows_*_test.f32 (e i target)
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 # finestre, target e previsioni come nella nostra misura
python3 tools/score.py data/targets_ETTh1_test.f32 out/dlinear_ETTh1.f32
tools/run_bench.sh -m dlinear -n 9 -c 2 # entrambe le build, alternate, verifica byte per byte, core 2
tools/run_bench.sh -m dlinear -n 9 -t 8 -c 2-9 # otto thread sui core 2-9
tools/run_bench.sh -m patchtst -n 9 -c 2
--threads T accetta qualsiasi T >= 1; le previsioni non dipendono da T. I file di input e di
output sono float32 little-endian grezzi (n x 336 x 7 e n x 96 x 7); tools/make_windows.py
richiede solo numpy.
Linux x86-64 (glibc). I programmi sono compilati per x86-64-v3 (AVX2, FMA, BMI2: Intel Haswell e
successivi, AMD Zen e successivi); le istruzioni FMA non vengono usate. PatchTST chiama erff dalla
libreria C di sistema: con una glibc la cui erff differisce da quella di glibc 2.39 (Ubuntu 24.04)
le sue previsioni possono differire da expected.sha256, mentre l’originale e la build TAKT
continuano a coincidere byte per byte.
Equivalenza
- In ogni round misurato le previsioni delle due build erano identiche al byte (conteggi nella tabella).
- Entrambe le build riproducono, byte per byte, le previsioni salvate in una nostra esecuzione precedente: DLinear su quattro dataset e PatchTST su due, split di test e di validazione, 1, 8 e 32 thread: 72 file su 72.
tools/make_windows.pyriproduce byte per byte le finestre dei data loader ufficiali di LTSF-Linear / PatchTST (tutti e quattro i dataset, test e validazione; a questo scopo è stato reimplementato il parser predefinito dei float di pandas).
Ricostruzione dell’originale
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
Con Rust 1.96.0 da rustup (con il componente rust-src) questa procedura riproduce
bin/ltsf-dlinear-original e bin/ltsf-patchtst-original bit per bit sulla nostra macchina; con
un’altra configurazione cambiano i percorsi incorporati, non le previsioni.
Confronto precedente con PyTorch (non riproducibile con questo pacchetto)
Il 2 ottobre 2026 abbiamo misurato gli stessi modelli sulla stessa macchina in condizioni diverse:
senza riservare il banco (39 CPU logiche condivise con altri lavori); entrambe le versioni Rust
compilate come librerie condivise per la CPU nativa e chiamate nello stesso processo; PyTorch 2.14
su CPU, eager e torch.compile (Inductor), batch da 64 o 512. Per ogni implementazione il numero di
thread (1, 8 o 32) e la dimensione del batch sono stati scelti in base alla latenza minima sullo
split di validazione, poi è stato cronometrato lo split di test (mediana di 5).
| Modello | Dataset | Rust originale | Build TAKT | PyTorch più veloce (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× |
Con un solo thread l’ordine era diverso: PyTorch con Inductor e i batch era più veloce della build
TAKT (DLinear ETTh1: 37,9 ms contro 111,4 ms; PatchTST ETTh1: 2,03 s contro 5,78 s). In
quell’esecuzione la build TAKT era 5,6–5,9× (DLinear) e 2,2–2,5× (PatchTST) più veloce del Rust
originale con i 32 thread selezionati, e 7,2–7,3× e 2,3× con un thread. Tutti i numeri di
quell’esecuzione sono in measurements.json (earlier_measurement_2026_10_02).
Contenuto
bin/:ltsf-dlinear-original,ltsf-dlinear-takt,ltsf-patchtst-original,ltsf-patchtst-takt(codice Rust a collegamento statico, pesi compilati all’interno, simboli rimossi);source/: la versione originale: driver a riga di comando, codice del modello generato e pesi;tools/:make_windows.py(finestre di input),score.py(MSE/MAE),run_bench.sh(benchmark);expected.sha256,measurements.json,SHA256SUMS,LICENSE.
I dati ETT non sono inclusi (CC BY-ND 4.0, scaricati da tools/make_windows.py). Il sorgente della
build TAKT non è pubblicato: consegniamo build.