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