← torna alla vetrina

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

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

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.

Testo sorgente: README.it.md

Telegram