soluzione lineare più veloce per matrici quadrate piccole (10x10)


9

Sono molto interessato all'ottimizzazione della risoluzione del sistema lineare per piccole matrici (10x10), a volte chiamate minuscole matrici. C'è una soluzione pronta per questo? La matrice può essere considerata non singolare.

Questo solutore deve essere eseguito oltre 1 000 000 volte in microsecondi su una CPU Intel. Sto parlando del livello di ottimizzazione utilizzato nei giochi per computer. Non importa se lo codifico in assembly e specifici dell'architettura, o studio riduzioni di precisione o affidabilità e utilizzo hack in virgola mobile (utilizzo il flag di compilazione -ffast-math, nessun problema). La risoluzione può anche fallire per circa il 20% delle volte!

Il parziale PivLu di Eigen è il più veloce nel mio attuale benchmark, superando LAPACK quando ottimizzato con -O3 e un buon compilatore. Ma ora sono sul punto di creare a mano un risolutore lineare personalizzato. Qualsiasi consiglio sarebbe molto apprezzato. Renderò la mia soluzione open source e riconoscerò le informazioni chiave in pubblicazioni, ecc.

Correlati: Velocità di risoluzione del sistema lineare con matrice diagonale a blocchi Qual è il metodo più veloce per invertire milioni di matrici? https://stackoverflow.com/q/50909385/1489510


7
Questo sembra un obiettivo estensibile. Supponiamo di utilizzare il più veloce Skylake-X Xeon Platinum 8180 con un throughput di picco teorico di 4 TFLOP a precisione singola e che un sistema 10x10 richiede circa 700 (circa 2n ** 3/3) operazioni a virgola mobile per essere risolto. Quindi un lotto di 1M di tali sistemi potrebbe teoricamente essere risolto in 175 microsecondi. Questo è un numero di velocità della luce non superiore. Puoi condividere le prestazioni che stai attualmente ottenendo con il tuo codice esistente più veloce? A proposito, i dati sono precisione singola o doppia precisione?
njuffa,

@njuffa sì, ho mirato a raggiungere quasi 1ms ma micro è un'altra storia. Per micro ho considerato di sfruttare la struttura inversa incrementale nel batch rilevando matrici simili, che si verificano spesso. La perf è estremamente a una gamma di 10-500ms a seconda del processore. La precisione è doppia o addirittura doppia complessa. La precisione singola rallenta.
rfabbri,

@njuffa Posso ridurre o aumentare la precisione per la velocità
rfabbri

2
Sembra che la precisione / accuratezza non sia la tua priorità. Per il tuo obiettivo, forse è utile un metodo iterativo troncato in un numero relativamente piccolo di valutazioni? Soprattutto se hai un'ipotesi iniziale ragionevole.
Spencer Bryngelson

1
Fai perno? Potresti fare una fattorizzazione QR invece dell'eliminazione gaussiana. Interlacciate i vostri sistemi in modo da poter utilizzare le istruzioni SIMD ed eseguire più sistemi contemporaneamente? Scrivi programmi in linea retta senza loop e senza indirizzamento indiretto? Quale precisione vuoi e come condizionerò il tuo sistema? Hanno qualche struttura che potrebbe essere sfruttata?
Carl Christian

Risposte:


7

L'uso di un tipo di matrice Eigen in cui il numero di righe e colonne è codificato nel tipo in fase di compilazione ti dà un vantaggio su LAPACK, dove la dimensione della matrice è nota solo in fase di esecuzione. Queste informazioni aggiuntive consentono al compilatore di eseguire lo svolgimento di un ciclo completo o parziale, eliminando molte istruzioni di diramazione. Se stai cercando di utilizzare una libreria esistente anziché scrivere i tuoi kernel, avere probabilmente un tipo di dati in cui la dimensione della matrice può essere inclusa come parametri del modello C ++ sarà probabilmente essenziale. L'unica altra libreria che conosco fa questo è fiammata , quindi potrebbe valere la pena confrontarsi con Eigen.

Se decidi di implementare la tua implementazione, potresti scoprire che cosa fa PETSc per il suo formato CSR a blocchi per essere un esempio utile, sebbene PETSc stesso probabilmente non sarà lo strumento giusto per quello che hai in mente. Invece di scrivere un ciclo, scrivono ogni singola operazione per piccoli matrici-vettore si moltiplica esplicitamente (vedi questo file nel loro repository). Ciò garantisce che non ci siano istruzioni di diramazione come si potrebbe ottenere con un ciclo. Le versioni del codice con le istruzioni AVX sono un buon esempio di come utilizzare effettivamente le estensioni vettoriali. Ad esempio, questa funzione utilizza il__m256dtipo di dati per operare contemporaneamente su quattro doppie contemporaneamente. È possibile ottenere un notevole aumento delle prestazioni scrivendo esplicitamente tutte le operazioni utilizzando le estensioni vettoriali, solo per la fattorizzazione LU anziché la moltiplicazione matrice-vettore. Invece di scrivere a mano il codice C a mano, sarebbe meglio usare uno script per generarlo. Potrebbe anche essere divertente vedere se c'è una differenza di prestazioni apprezzabile quando si riordinano alcune operazioni per sfruttare meglio il pipeline di istruzioni.

Potresti anche ottenere un po 'di chilometraggio dallo strumento STOKE , che esplorerà casualmente lo spazio delle possibili trasformazioni del programma per trovare una versione più veloce.


tx. Uso già Eigen come Map <const Matrix <complex, 10, 10>> AA (A) con successo. controllerà le altre cose.
rfabbri

Eigen ha anche AVX e persino un'intestazione complex.h per questo. Perché PETSc per questo? In questo caso è difficile competere con Eigen. Ho specializzato Eigen ancora di più per il mio problema e con una strategia di pivot approssimativa che invece di prendere il massimo su una colonna, scambia immediatamente un pivot quando ne trova un altro di 3 ordini di grandezza più grande.
rfabbri

1
@rfabbri Non ti stavo suggerendo di usare PETSc per questo, solo che ciò che fanno in quel particolare caso potrebbe essere istruttivo. Ho modificato la risposta per renderlo più chiaro.
Daniel Shapero

4

Un'altra idea potrebbe essere quella di utilizzare un approccio generativo (un programma che scrive un programma). Autore di un (meta) programma che sputa la sequenza di istruzioni C / C ++ per eseguire ** LU non imperniati su un sistema 10x10 .. fondamentalmente prendendo il nido del ciclo k / i / j e appiattendolo in O (1000) o giù di lì aritmetica scalare. Quindi inserire quel programma generato in qualunque compilatore di ottimizzazione. Quello che penso sia interessante in questo caso, è la rimozione dei loop espone ogni dipendenza dai dati e sottoespressione ridondante e offre al compilatore la massima opportunità di riordinare le istruzioni in modo che si associno bene all'hardware effettivo (ad es. Numero di unità di esecuzione, rischi / stalle, quindi su).

Se ti capita di conoscere tutte le matrici (o anche solo alcune di esse), puoi migliorare il rendimento chiamando il intrinseco / le funzioni SIMD (SSE / AVX) invece del codice scalare. Qui sfrutteresti l'imbarazzante parallelismo tra le istanze, invece di inseguire qualsiasi parallelismo all'interno di una singola istanza. Ad esempio, è possibile eseguire contemporaneamente 4 LU a doppia precisione utilizzando intrinsecamente AVX256, impacchettando 4 matrici "attraverso" il registro ed eseguendo le stesse operazioni ** su tutte.

** Da qui l'attenzione sul LU non pubblicizzato. Il pivot rovina questo approccio in due modi. Innanzitutto, introduce i rami a causa della selezione del perno, il che significa che le dipendenze dei dati non sono così perfettamente conosciute. In secondo luogo, significa che diversi "slot" SIMD dovrebbero fare cose diverse, perché l'istanza A potrebbe ruotare in modo diverso rispetto all'istanza B. Quindi, se persegui uno di questi, ti suggerirei di ruotare staticamente le tue matrici prima del calcolo (permetti l'ingresso più grande di ogni colonna in diagonale).


poiché le matrici sono così piccole, forse il pivot può essere eliminato se sono pre-ridimensionate. Nemmeno pre-perno le matrici. Tutto ciò di cui abbiamo bisogno è che le voci siano entro 2-3 ordini di grandezza l'uno dall'altro.
rfabbri

2

La tua domanda porta a due diverse considerazioni.

Innanzitutto, devi scegliere l'algoritmo giusto. Quindi, dovrebbe essere considerata la questione se le matrici hanno una struttura. Ad esempio, quando le matrici sono simmetriche, una decomposizione di Cholesky è più efficiente di LU. Quando hai solo bisogno di una precisione limitata, un metodo iterativo può essere più veloce.

10×10

In tutto, la risposta alla tua domanda dipende fortemente dall'hardware e dalle matrici che consideri. Probabilmente non esiste una risposta definitiva e devi provare alcune cose per trovare un metodo ottimale.


Finora Eigen ha già ottimizzato pesantemente, utilizza SEE, AVX, ecc. E ho provato metodi iterativi in ​​un test preliminare e non mi hanno aiutato. Ho provato Intel MKL ma non meglio di Eigen con flag GCC ottimizzati. Attualmente sto cercando di creare qualcosa di meglio e più semplice di Eigen e di fare test più dettagliati con metodi iterativi.
rfabbri,

1

Vorrei provare l'inversione a blocchi.

https://en.wikipedia.org/wiki/Invertible_matrix#Blockwise_inversion

Eigen utilizza una routine ottimizzata per calcolare l'inverso di una matrice 4x4, che è probabilmente la migliore che otterrai. Prova a usarlo il più possibile.

http://www.eigen.tuxfamily.org/dox/Inverse__SSE_8h_source.html

In alto a sinistra: 8x8. In alto a destra: 8x2. In basso a sinistra: 2x8. In basso a destra: 2x2. Invertire 8x8 usando il codice di inversione 4x4 ottimizzato. Il resto sono prodotti a matrice.

EDIT: l'utilizzo dei blocchi 6x6, 6x4, 4x6 e 4x4 ha dimostrato di essere un po 'più veloce di quello che ho descritto sopra.

using namespace Eigen;

template<typename Scalar, int tl_size, int br_size>
Matrix<Scalar, tl_size + br_size, tl_size + br_size> blockwise_inversion(const Matrix<Scalar, tl_size, tl_size>& A, const Matrix<Scalar, tl_size, br_size>& B, const Matrix<Scalar, br_size, tl_size>& C, const Matrix<Scalar, br_size, br_size>& D)
{
    Matrix<Scalar, tl_size + br_size, tl_size + br_size> result;

    Matrix<Scalar, tl_size, tl_size> A_inv = A.inverse().eval();
    Matrix<Scalar, br_size, br_size> DCAB_inv = (D - C * A_inv * B).inverse();

    result.topLeftCorner<tl_size, tl_size>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<tl_size, br_size>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<br_size, tl_size>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<br_size, br_size>() = DCAB_inv;

    return result;
}

template<typename Scalar, int tl_size, int br_size>
Matrix<Scalar, tl_size + br_size, tl_size + br_size> my_inverse(const Matrix<Scalar, tl_size + br_size, tl_size + br_size>& mat)
{
    const Matrix<Scalar, tl_size, tl_size>& A = mat.topLeftCorner<tl_size, tl_size>();
    const Matrix<Scalar, tl_size, br_size>& B = mat.topRightCorner<tl_size, br_size>();
    const Matrix<Scalar, br_size, tl_size>& C = mat.bottomLeftCorner<br_size, tl_size>();
    const Matrix<Scalar, br_size, br_size>& D = mat.bottomRightCorner<br_size, br_size>();

    return blockwise_inversion<Scalar,tl_size,br_size>(A, B, C, D);
}

template<typename Scalar>
Matrix<Scalar, 10, 10> invert_10_blockwise_8_2(const Matrix<Scalar, 10, 10>& input)
{
    Matrix<Scalar, 10, 10> result;

    const Matrix<Scalar, 8, 8>& A = input.topLeftCorner<8, 8>();
    const Matrix<Scalar, 8, 2>& B = input.topRightCorner<8, 2>();
    const Matrix<Scalar, 2, 8>& C = input.bottomLeftCorner<2, 8>();
    const Matrix<Scalar, 2, 2>& D = input.bottomRightCorner<2, 2>();

    Matrix<Scalar, 8, 8> A_inv = my_inverse<Scalar, 4, 4>(A);
    Matrix<Scalar, 2, 2> DCAB_inv = (D - C * A_inv * B).inverse();

    result.topLeftCorner<8, 8>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<8, 2>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<2, 8>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<2, 2>() = DCAB_inv;

    return result;
}

template<typename Scalar>
Matrix<Scalar, 10, 10> invert_10_blockwise_6_4(const Matrix<Scalar, 10, 10>& input)
{
    Matrix<Scalar, 10, 10> result;

    const Matrix<Scalar, 6, 6>& A = input.topLeftCorner<6, 6>();
    const Matrix<Scalar, 6, 4>& B = input.topRightCorner<6, 4>();
    const Matrix<Scalar, 4, 6>& C = input.bottomLeftCorner<4, 6>();
    const Matrix<Scalar, 4, 4>& D = input.bottomRightCorner<4, 4>();

    Matrix<Scalar, 6, 6> A_inv = my_inverse<Scalar, 4, 2>(A);
    Matrix<Scalar, 4, 4> DCAB_inv = (D - C * A_inv * B).inverse().eval();

    result.topLeftCorner<6, 6>() = A_inv + A_inv * B * DCAB_inv * C * A_inv;
    result.topRightCorner<6, 4>() = -A_inv * B * DCAB_inv;
    result.bottomLeftCorner<4, 6>() = -DCAB_inv * C * A_inv;
    result.bottomRightCorner<4, 4>() = DCAB_inv;

    return result;
}

Ecco i risultati di una corsa di riferimento usando un milione di Eigen::Matrix<double,10,10>::Random()matrici e Eigen::Matrix<double,10,1>::Random()vettori. In tutti i miei test, il mio inverso è sempre più veloce. La mia routine di risoluzione prevede il calcolo dell'inverso e quindi la sua moltiplicazione per un vettore. A volte è più veloce di Eigen, a volte no. Il mio metodo di marcatura al banco potrebbe essere difettoso (non disabilita il turbo boost, ecc.). Inoltre, le funzioni casuali di Eigen potrebbero non rappresentare dati reali.

  • Inverso del perno parziale di Eigen: 3036 millisecondi
  • Il mio inverso con blocco superiore 8x8: 1638 millisecondi
  • Il mio inverso con blocco superiore 6x6: 1234 millisecondi
  • Risoluzione pivot parziale di Eigen: 1791 millisecondi
  • La mia soluzione con blocco superiore 8x8: 1739 millisecondi
  • La mia soluzione con blocco superiore 6x6: 1286 millisecondi

Sono molto interessato a vedere se qualcuno può ottimizzarlo ulteriormente, poiché ho un'applicazione ad elementi finiti che inverte una matrice di gazillion 10x10 (e sì, ho bisogno di coefficienti individuali dell'inverso, quindi risolvere direttamente un sistema lineare non è sempre un'opzione) .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.