Potenzia Rust Code 7x con SIMD: regole chiave |  Verso la scienza dei dati |  medio

 | Intelligenza-Artificiale

Ricordiamo che la regola 6, da Parte 1mostra come rendere gli algoritmi SIMD di Rust completamente generici per tipo e LANES. Successivamente dobbiamo scegliere il nostro algoritmo e impostare LANES.

In questa regola vedremo come utilizzare il popolare criterio cassa per confrontare e valutare i nostri algoritmi e opzioni. Nel contesto di range-set-blazevaluteremo:

  • 5 algoritmi: Regolare, Splat0, Splat1, Splat2, Ruota
  • 3 livelli di estensione SIMD — sse2 (128 bit), avx2 (256 bit), avx512f (512 bit)
  • 10 tipi di elementi — i8, u8, i16, u16, i32, u32, i64, u64, isize, usize
  • 5 numeri di corsia: 4, 8, 16, 32, 64
  • 4 lunghezze di ingresso — 1024; 10.240; 102.400; 1.024.000
  • 2 CPU: AMD 7950X con avx512fIntel i5–8250U con avx2

Il benchmark misura il tempo medio per eseguire ciascuna combinazione. Quindi calcoliamo il throughput in Mbyte/sec.

Guarda questo nuovo articolo complementare su come iniziare con Criterion. L’articolo mostra anche come spingere (abuso?) Criterion per misurare gli effetti delle impostazioni del compilatore, come il livello di estensione SIMD.

L’esecuzione dei benchmark risulta in un valore di 5000 righe *.csv file che inizia:

Group,Id,Parameter,Mean(ns),StdErr(ns)
vector,regular,avx2,256,i16,16,16,1024,291.47,0.080141
vector,regular,avx2,256,i16,16,16,10240,2821.6,3.3949
vector,regular,avx2,256,i16,16,16,102400,28224,7.8341
vector,regular,avx2,256,i16,16,16,1024000,287220,67.067
vector,regular,avx2,256,i16,16,32,1024,285.89,0.59509
...

Questo file è adatto per l’analisi tramite tabelle pivot dei fogli di calcolo o strumenti per frame di dati come polare.

Algoritmi e corsie

Ecco una tabella pivot di Excel che mostra, per ciascun algoritmo, il throughput (MByte/sec) rispetto alle corsie SIMD. La tabella calcola la velocità effettiva media tra i livelli di estensione SIMD, i tipi di elemento e la lunghezza di input.

Sul mio computer desktop AMD:

Su un computer portatile Intel:

Le tabelle mostrano che Splat1 e Splat2 funzionano meglio. Mostrano anche più corsie, sempre meglio fino a 32 o 64.

Come può, ad esempio,sse2 (128 bit di larghezza) elabora 64 corsie di i64 (larghezza 4096 bit)? La ruggine core::simd Il modulo rende possibile questa magia dividendo automaticamente ed efficientemente i 4096 bit in 32 blocchi da 128 bit ciascuno. L’elaborazione congiunta dei 32 blocchi da 128 bit (apparentemente) consente ottimizzazioni che vanno oltre l’elaborazione indipendente dei blocchi da 128 bit.

Livelli di estensione SIMD

Impostiamo LANES su 64 e confrontiamo i diversi livelli di estensione SIMD sulla macchina AMD. La tabella calcola la velocità effettiva media in base al tipo di elemento e alla lunghezza di input.

Sulla mia macchina AMD, quando utilizzo 64 corsie, sse2 è il più lento. Confrontoavx2 A avx512fi risultati sono contrastanti. Ancora una volta, gli algoritmi Splat1 e Splat2 funzionano meglio.

Tipi di elementi

Successivamente impostiamo il livello di estensione SIMD su avx512f e confrontare diversi tipi di elementi. Manteniamo LANES a 64 e throughput medio su tutta la lunghezza di input.

Vediamo che gli elementi bit per bit, 32 bit e 64 bit vengono elaborati più velocemente. (Tuttavia, per elemento, i tipi più piccoli sono più veloci.) Splat1 e Splat2 sono gli algoritmi più veloci, mentre Splat1 è leggermente migliore.

Lunghezza di input

Infine, impostiamo il tipo di elemento su i32 e vedere la lunghezza dell’input rispetto alla velocità effettiva.

Vediamo che tutti gli algoritmi SIMD fanno più o meno lo stesso con 1 milione di input. Apparentemente Splat1 fa meglio di altri algoritmi su input brevi.

Sembra anche che più corto sia più veloce che più lungo. Questo potrebbe essere il risultato della memorizzazione nella cache o potrebbe essere un artefatto dei benchmark che eliminano dati non allineati.

Conclusioni sui benchmark

Sulla base di questi benchmark, utilizzeremo l’algoritmo Splat1. Per ora imposteremo LANES su 32 o 64, ma per eventuali complicazioni vedere la regola successiva. Infine, consiglieremo agli utenti di impostare il livello di estensione SIMD almeno su avx2.

as_simd

Prima di aggiungere il supporto SIMD, RangeSetBlazeera il costruttore principale di from_iter:

let a = RangeSetBlaze::from_iter((1, 2, 3));

Le operazioni SIMD, tuttavia, funzionano meglio sugli array, non sugli iteratori. Inoltre, costruendo a RangeSetBlaze da un array è spesso una cosa naturale da fare, quindi ho aggiunto un nuovo file from_slice costruttore:

#(inline)
pub fn from_slice(slice: impl AsRef<(T)>) -> Self {
T::from_slice(slice)
}

Il nuovo costruttore esegue una chiamata in linea al proprio numero intero from_slice metodo. Per tutti i tipi interi, eccetto i128/u128questo successivo chiama:

let (prefix, middle, suffix) = slice.as_simd();

La ruggine è notturna as_simd Il metodo trasmuta in modo rapido e sicuro la fetta in:

  1. un non allineato prefix – con cui elaboriamo from_itercome prima.
  2. middleun array allineato di Simd strutturare pezzi
  3. Un non allineato suffix – con cui elaboriamo from_itercome prima.

Pensa a middle come suddividere i nostri interi di input in blocchi di dimensione 16 (o qualunque sia la dimensione impostata su LANES). Quindi iteriamo i blocchi attraverso our is_consecutive funzione, alla ricerca di sequenze di true. Ogni esecuzione diventa un singolo intervallo. Ad esempio, una sequenza di 160 singoli numeri interi consecutivi da 1000 a 1159 (incluso) verrebbe identificata e sostituita con un singolo Rust RangeInclusive 1000..=1159. Questo intervallo viene quindi elaborato da from_iter molto più veloce di from_iter avrebbe elaborato i 160 numeri interi individuali. Quando is_consecutive ritorna falsetorniamo all’elaborazione dei singoli numeri interi del pezzo from_iter.

i128/u128

Come gestire gli array di tipi that core::simd non gestisce, vale a direi128/u128? Per ora, li elaboro solo con la velocità più lenta from_iter.

Benchmark nel contesto

Come passaggio finale, confronta il tuo codice SIMD nel contesto del tuo codice principale, idealmente su dati rappresentativi.

IL range-set-blaze la cassa include già punti di riferimenti. Un benchmark misura le prestazioni acquisendo 1.000.000 di numeri interi con vari livelli di complessità. La dimensione media dei ciuffi varia da 1 (nessun grumo) a 100.000 ciuffi. Eseguiamo il benchmark con LANES impostato su 4, 8, 16, 32 e 64. Utilizzeremo l’algoritmo Splat1 e il livello di estensione SIMD avx512f.

Per ciascuna dimensione del cluster, le barre mostrano la velocità relativa di acquisizione di 1.000.000 di numeri interi. Per ogni dimensione del grumo, il più veloce LANES è impostato al 100%.

Vediamo che per i ciuffi di dimensione 10 e 100, LANES=4 è il migliore. Con cespi di dimensione 100.000, invece, LANES=4 è 4 volte peggiore del migliore. All’altro estremo, LANES=64 sembra buono con gruppi di dimensione 100.000, ma è 1,8 e 1,5 volte peggiore del migliore con 100 e 1000, rispettivamente.

Ho deciso di impostare LANES a 16. È il migliore per cespi di dimensione 1000. Inoltre non è mai più di 1,25 volte peggiore del migliore.

Con questa impostazione possiamo eseguire altri benchmark. La tabella seguente mostra varie librerie di set di intervalli (inclusi range-set-blaze) lavorando sullo stesso compito: ingerire 1.000.000 di numeri interi di varia complessità. IL y-axis è millisecondi dove inferiore è migliore.

Con cespi di dimensione 1000, l’esistente RangeSetBlaze::into_iter metodo (rosso) era già 30 volte più veloce di HashSet (arancione). Nota che la scala è logaritmica. Con avx512fil nuovo alimentato SIMD RangeSetBlaze::into_slice l’algoritmo (azzurro) è 230 volte più veloce di HashSet. Con sse2 (blu scuro), è 220 volte più veloce. Con avx2 (giallo), è 180 volte più veloce. Su questo benchmark, rispetto a RangeSetBlaze::into_iter, avx512f RangeSetBlaze::into_slice è 7 volte più veloce.

Dovremmo considerare anche il caso peggiore, ovvero l’acquisizione di dati senza grumi. Ho eseguito quel benchmark. Ha mostrato l’esistente RangeSetBlaze::into_iter è circa 2,2 volte più lento di HashSet. Il nuovo RangeSetBlaze::into_slice è 2,4 volte più lento di HashSet.

Quindi, a conti fatti, il nuovo codice SIMD offre un enorme vantaggio per i dati che si presume siano scomodi. Se il presupposto è sbagliato, il processo sarà più lento, ma non in modo catastrofico.

Con il codice SIMD integrato nel nostro progetto, siamo pronti per la spedizione, giusto? Purtroppo no. Dato che il nostro codice dipende ogni notte da Rust, dovremmo renderlo facoltativo. Vedremo come farlo nella prossima regola.

Il nostro bellissimo nuovo codice SIMD dipende da Rust ogni notte che può cambiare e cambia ogni notte. Richiedere agli utenti di dipendere da Rust ogni notte sarebbe crudele. (Inoltre, ricevere reclami quando le cose si rompono sarebbe fastidioso.) La soluzione è nascondere il codice SIMD dietro una funzione di carico.

Caratteristica, Caratteristica, Caratteristica — Nel contesto del lavoro con SIMD e Rust, la parola “caratteristica” viene utilizzata in tre modi diversi. Innanzitutto, “caratteristiche CPU/destinazione”: descrivono le capacità di una CPU, comprese le estensioni SIMD supportate. Vedere target-feature E is_x86_feature_detected!. In secondo luogo, “cancelli delle funzionalità notturne”: Rust controlla la visibilità delle nuove funzionalità linguistiche in Rust ogni notte presentano cancelli. Per esempio, #!(feature(portable_simd)). Terzo, “caratteristiche del carico”- consentono a qualsiasi contenitore o libreria di Rust di offrire/limitare l’accesso a parte delle proprie capacità. Li vedi nel tuo Cargo.toml quando, ad esempio, aggiungi una dipendenza a itertools/use_std.

Ecco i passaggi che il range-set-blaze crate impiega per rendere facoltativo il codice SIMD dipendente dalla notte:

  • In Cargo.tomldefinire una caratteristica del carico relativa al codice SIMD:
(features)
from_slice = ()
  • In cima lib.rs file, fai la notte portable_simd il cancello delle funzionalità, dipende da from_slicecaratteristica del carico:
#!(cfg_attr(feature = "from_slice", feature(portable_simd)))
  • Utilizza l’attributo di compilazione condizionale, ad esempio, #(cfg(feature = “from_slice”))per includere selettivamente il codice SIMD. Ciò include i test.
/// Creates a (`RangeSetBlaze`) from a collection of integers. It is typically many
/// times faster than (`from_iter`)(1)/(`collect`)(1).
/// On a representative benchmark, the speed up was 6×.
///
/// **Warning: Requires the nightly compiler. Also, you must enable the `from_slice`
/// feature in your `Cargo.toml`. For example, with the command:**
/// ```bash
/// cargo add range-set-blaze --features "from_slice"
/// ```
///
/// **Caution**: Compiling with `-C target-cpu=native` optimizes the binary for your current CPU architecture,
/// which may lead to compatibility issues on other machines with different architectures.
/// This is particularly important for distributing the binary or running it in varied environments.
/// (1): struct.RangeSetBlaze.html#impl-FromIterator<T>-for-RangeSetBlaze<T>
#(cfg(feature = "from_slice"))
#(inline)
pub fn from_slice(slice: impl AsRef<(T)>) -> Self {
T::from_slice(slice)
}
  • Come mostrato nei documenti sopra, aggiungi avvisi e precauzioni alla documentazione.
  • Utilizzo --features from_slice per verificare o testare il tuo codice SIMD.
cargo check --features from_slice
cargo test --features from_slice
  • Utilizzo --all-features per eseguire tutti i test, generare tutta la documentazione e pubblicare tutte le funzionalità del carico:
cargo test --all-features --doc
cargo doc --no-deps --all-features --open
cargo publish --all-features --dry-run

Quindi, ecco qua: nove regole per aggiungere operazioni SIMD al tuo codice Rust. La facilità di questo processo riflette il core::simd l’eccellente design della biblioteca. Dovresti sempre utilizzare SIMD ove applicabile? Alla fine sì, quando la biblioteca si sposta di notte da Rust a Stable. Per ora, utilizza SIMD laddove i suoi vantaggi in termini di prestazioni sono cruciali o rendine l’utilizzo facoltativo.

Idee per migliorare l’esperienza SIMD in Rust? La qualità di core::simd è già alto; la necessità principale è stabilizzarla.

Grazie per esserti unito a me in questo viaggio nella programmazione SIMD. Spero che se hai un problema appropriato al SIMD, questi passaggi ti aiuteranno ad accelerarlo.

Per favore segui Carl su Medium. Scrivo di programmazione scientifica in Rust e Python, machine learning e statistica. Tendo a scrivere circa un articolo al mese.

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *