Küçük kare matrisler için en hızlı lineer sistem çözme (10x10)


9

Bazen küçük matrisler olarak adlandırılan küçük matrisler (10x10) için lineer sistem çözmenin cehennemini optimize etmekle çok ilgileniyorum . Bunun için hazır bir çözüm var mı? Matrisin nonsüler olduğu varsayılabilir.

Bu çözücü, Intel CPU üzerinde mikrosaniye cinsinden 100000 defadan fazla çalıştırılmalıdır. Bilgisayar oyunlarında kullanılan optimizasyon seviyesinden bahsediyorum. Meclise ve mimariye özgü kodlarsam veya hassasiyet veya güvenilirlik dengesizlik azaltımlarını inceler ve kayan nokta korsanları kullanırsam (-fast-math derleme bayrağını kullanırım, sorun değil). Çözüm yaklaşık% 20 oranında başarısız olabilir!

Eigen'nin kısmiPivLu, mevcut karşılaştırmamda en hızlı olanı, -O3 ve iyi bir derleyici ile optimize edildiğinde LAPACK'ten daha iyi performans gösteriyor. Ama şimdi özel bir lineer çözücüyü el işi yapmaktayım. Herhangi bir tavsiye büyük mutluluk duyacağız. Çözümümü açık kaynak haline getireceğim ve yayınlarda vb. Temel bilgileri kabul edeceğim.

İlgili: Blok diyagonal matris ile lineer sistemi çözme hızı Milyonlarca matrisi tersine çevirmenin en hızlı yöntemi nedir? https://stackoverflow.com/q/50909385/1489510


7
Bu gergin bir hedefe benziyor. Diyelim ki en hızlı Skylake-X Xeon Platinum 8180'i teorik tepe verimi 4 tek hassasiyetli TFLOP ile kullandığımızı ve bir 10x10 sisteminin yaklaşık 700 (kabaca 2n ** 3/3) kayan nokta işleminin çözülmesini gerektirdiğini varsayalım. Daha sonra 1M'lik bu tür bir grup teorik olarak 175 mikrosaniye içinde çözülebilir. Bu ışık hızını aşamaz. Şu anda elde ettiğiniz performansı mevcut en hızlı kodunuzla paylaşabilir misiniz? BTW, veri tek hassas mı yoksa çift hassasiyet mi?
njuffa

@njuffa evet 1 ms'ye kadar ulaşmayı hedefledim ama mikro başka bir hikaye. Mikro için sık sık ortaya çıkan benzer matrisleri tespit ederek partideki artan ters yapıyı değerlendirmeyi düşündüm. Perf, işlemciye bağlı olarak güncel olarak 10-500ms aralığındadır. Hassasiyet çift, hatta karmaşık çifttir. Tek hassasiyet yavaşlar.
rfabbri

@njuffa Hız için hassasiyeti azaltabilir veya
artırabilirim

2
Görünüşe göre hassasiyet / doğruluk sizin önceliğiniz değil. Hedefiniz için, nispeten az sayıda değerlendirmede kısaltılmış yinelemeli bir yöntem yararlı olabilir mi? Özellikle makul bir ilk tahmininiz varsa.
Spencer Bryngelson

1
Pivot yapıyor musunuz? Gauss yok etme yerine QR çarpanlarına ayırma yapabilir misiniz? SIMD talimatlarını kullanabilmeniz ve birden fazla sistemi aynı anda yapabilmeniz için sistemlerinizi araya ekliyor musunuz? Döngü ve dolaylı adresleme olmadan düz çizgi programlar yazıyor musunuz? Ne kadar doğruluk istiyorsunuz ve sisteminiz nasıl koşullandırılacak? Sömürülebilecek herhangi bir yapıları var mı?
Carl Christian

Yanıtlar:


7

Derleme sırasında satır ve sütun sayısının türe kodlandığı bir Eigen matris türü kullanmak , LAPACK üzerinde bir kenar sağlar; burada matris boyutu yalnızca çalışma zamanında bilinir. Bu ekstra bilgi, derleyicinin çok sayıda şube talimatını ortadan kaldırarak tam veya kısmi döngü açma yapmasına izin verir. Kendi çekirdeklerinizi yazmak yerine mevcut bir kitaplığı kullanmayı düşünüyorsanız, matris boyutunun C ++ şablon parametreleri olarak eklenebileceği bir veri türüne sahip olmak büyük olasılıkla gerekli olacaktır. Benim bildiğim sadece diğer kütüphane şudur yapar yangını o Eigen karşı kıyaslama değerinde olabilir bu yüzden.

Kendi uygulamanızı yapmaya karar verirseniz, PETSc'nin blok CSR formatı için yararlı bir örnek olması için ne yaptığını bulabilirsiniz, ancak PETSc'nin kendisi aklınızdakiler için doğru araç olmayacaktır. Bir döngü yazmaktan ziyade, küçük matris vektörü ile çarpılan her işlemi açıkça yazarlar (depolarındaki bu dosyaya bakın ). Bu, bir döngü ile alabileceğiniz gibi şube talimatlarının bulunmadığını garanti eder. AVX talimatları içeren kod sürümleri, vektör uzantılarının gerçekte nasıl kullanılacağına iyi bir örnektir. Örneğin, bu fonksiyon kullanır__m256daynı anda dört çift üzerinde aynı anda çalışmak için veri türü. Vektör işlemlerini kullanarak tüm işlemleri açıkça yazarak kayda değer bir performans artışı elde edebilirsiniz, sadece matris-vektör çarpımı yerine LU çarpanlarına ayırma için. C kodunu el ile yazmak yerine, bir komut dosyası oluşturmak için daha iyi olurdu. Yönerge boru hatlarından daha iyi yararlanmak için bazı işlemleri yeniden sıraladığınızda kayda değer bir performans farkı olup olmadığını görmek de eğlenceli olabilir.

Daha hızlı bir sürüm bulmak için olası program dönüşümlerinin alanını rastgele keşfedecek olan STOKE aracından biraz kilometre alabilirsiniz .


tx. Zaten Harita <const Matrix <karmaşık, 10, 10>> AA (A) gibi özleri kullanıyorum. diğer şeyleri kontrol edecektir.
rfabbri

Eigen ayrıca AVX'e ve bunun için bir complex.h başlığına sahiptir. Neden bunun için PETSc? Bu durumda Eigen ile rekabet etmek zordur. Eigen'i sorunum için daha da uzmanlaştırdım ve bir sütun üzerinde maksimum almak yerine, 3 derece daha büyük başka bir tane bulduğunda hemen bir pivotu değiştiren yaklaşık bir pivot stratejisi ile uzmanlaştım.
rfabbri

1
@rfabbri Bunun için PETSc'yi kullanmanızı önermiyordum, sadece o örnekte yaptıklarının öğretici olabileceğini söylemiyordum. Cevabı daha açık hale getirmek için düzenledim.
Daniel Shapero

4

Başka bir fikir de üretken bir yaklaşım kullanmak olabilir (bir program yazan bir program). 10x10 sistemde açıklanamayan ** LU gerçekleştirmek için C / C ++ komutlarının sırasını veren bir (meta) program yazınız. Temel olarak k / i / j döngü yuvasını alıp O (1000) veya skaler aritmetik. Sonra oluşturulan programı hangi derleyici optimize hangi içine besleyin. Burada ilginç olduğunu düşündüğüm şey, döngülerin kaldırılması her veri bağımlılığını ve gereksiz alt ifadeyi ortaya çıkarıyor ve derleyiciye gerçek donanıma (örn. Yürütme birimi sayısı, tehlikeler / tezgahlar vb. ) üzerinde.

Tüm matrisleri (veya sadece birkaçını) biliyorsanız, skaler kod yerine SIMD intrinsics / fonksiyonlarını (SSE / AVX) çağırarak verimi artırabilirsiniz. Burada, tek bir örnekte herhangi bir paralellik peşinde koşmak yerine, örnekler arasında utanç verici paralellikten faydalanabilirsiniz. Örneğin, AVX256 intrinsics kullanarak 4 çift duyarlıklı LU'yu kayıt defterine "mat" olarak doldurarak ve hepsinde aynı işlemleri ** uygulayarak eşzamanlı olarak gerçekleştirebilirsiniz.

** Dolayısıyla odaklanmamış LU'ya odaklanın. Oynar, bu yaklaşımı iki şekilde bozar. İlk olarak, pivot seçimi nedeniyle dallar tanıtılır, yani veri bağımlılıklarınız tam olarak bilinmemektedir. İkincisi, farklı SIMD "yuvalarının" farklı şeyler yapmak zorunda kalacağı anlamına gelir, çünkü A örneği B örneğinden farklı bir şekilde dönebilir. Bu nedenle, bunlardan herhangi birini takip ederseniz, hesaplamadan önce matrislerinizi statik olarak döndürmenizi öneririm (en büyük girişe izin verin) her sütunun köşegenine).


matrisler çok küçük olduğundan, önceden ölçeklendirilmişlerse, pivotlama yapılabilir. Matrisleri bile önceden döndürmüyor. İhtiyacımız olan tek şey girişlerin birbirinin 2-3 büyüklüğünde olması.
rfabbri

2

Sorunuz iki farklı hususa yol açıyor.

İlk olarak, doğru algoritmayı seçmeniz gerekir. Bu nedenle, matrislerin herhangi bir yapıya sahip olup olmadığı sorusu dikkate alınmalıdır. Örneğin, matrisler simetrik olduğunda, bir Cholesky ayrışması LU'dan daha etkilidir. Yalnızca sınırlı miktarda doğruluğa ihtiyacınız olduğunda yinelemeli bir yöntem daha hızlı olabilir.

İkinci olarak, algoritmayı verimli bir şekilde uygulamanız gerekir. Bunu yapmak için algoritmanızın darboğazını bilmeniz gerekir. Uygulamanız bellek aktarım hızına veya hesaplama hızına bağlı mı? Sadece düşündüğün için10×10matrisiniz varsa, matrisiniz CPU önbelleğine tamamen sığmalıdır. Bu nedenle, döngü başına mümkün olduğunca çok hesaplama yapmak için SIMD birimlerini (SSE, AVX, vb.) Ve işlemcinizin çekirdeklerini kullanmalısınız.

Sonuçta, sorunuzun cevabı büyük ölçüde düşündüğünüz donanıma ve matrislere bağlıdır. Muhtemelen kesin bir cevap yoktur ve en uygun yöntemi bulmak için birkaç şey denemeniz gerekir.


Eigen şimdiye kadar ağır bir şekilde optimize ediyor, SEE, AVX, vb. Kullanıyor ve bir ön testte yinelemeli yöntemleri denedim ve yardımcı olmadılar. Intel MKL'yi denedim ama optimize edilmiş GCC bayrakları ile Eigen'den daha iyi değil. Şu anda Eigen'den daha iyi ve daha basit bir şey yapmaya ve yinelemeli yöntemlerle daha ayrıntılı testler yapmaya çalışıyorum.
rfabbri

1

Blok ters çevirme denerdim.

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

Eigen, muhtemelen elde edeceğiniz en iyi olan 4x4 matrisinin tersini hesaplamak için optimize edilmiş bir rutin kullanır. Bunu mümkün olduğunca kullanmayı deneyin.

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

Sol üst: 8x8. Sağ üst: 8x2. Sol alttaki: 2x8. Sağ alttaki: 2x2. Optimize edilmiş 4x4 ters çevirme kodunu kullanarak 8x8'i ters çevirin. Gerisi matris ürünleridir.

DÜZENLEME: 6x6, 6x4, 4x6 ve 4x4 bloklarını kullanmanın yukarıda tarif ettiğimden biraz daha hızlı olduğu gösterilmiştir.

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;
}

İşte bir milyon Eigen::Matrix<double,10,10>::Random()matris ve Eigen::Matrix<double,10,1>::Random()vektör kullanılarak bir bench mark çalışmasının sonuçları . Tüm testlerimde, tersim her zaman daha hızlıdır. Benim çözme rutinim tersi hesaplamak ve daha sonra bir vektör ile çarpmaktır. Bazen Eigen'den daha hızlı, bazen değil. Tezgah markalama yöntemim kusurlu olabilir (turbo güçlendirmeyi devre dışı bırakmadı, vb.). Ayrıca, Eigen'ın rastgele işlevleri gerçek verileri temsil etmeyebilir.

  • Eigen kısmi pivot tersi: 3036 milisaniye
  • 8x8 üst blok ile tersim: 1638 milisaniye
  • 6x6 üst blok ile tersim: 1234 milisaniye
  • Öz kısmi pivot çözme: 1791 milisaniye
  • 8x8 üst blok ile çözdüğüm: 1739 milisaniye
  • 6x6 üst blok ile çözdüğüm: 1286 milisaniye

Bir gazilyon 10x10 matrisini tersine çeviren sonlu bir eleman uygulamam olduğu için herkesin bunu daha da optimize edip edemeyeceğini görmekle çok ilgiliyim (ve evet, tersin bireysel katsayılarına ihtiyacım var, bu yüzden doğrudan doğrusal bir sistemi çözmek her zaman bir seçenek değildir) .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.